这一篇重点介绍ryu来实现网络聚合的功能,建议先阅读关于LACP的知识,可以参考上一篇博文:https://blog.csdn.net/weixin_40042248/article/details/116395325?spm=1001.2014.3001.5501
一、拓扑构建
在介绍程序之前,先介绍一下实验拓扑的构建。拓扑如下图所示,其中h1和交换机连接的两条链路绑定在一个逻辑平面之中,接下来的实验就是针对这两个链路进行。
这个拓扑的构建,本实验选择namespace进行,新建h1-h4四个ns空间,然后新建s1作为ovs,具体的构建和配置命令如下,各位可以新建.sh文件,将以下命令全部复制进去,然后执行就行。
ip netns add h1
ip netns add h2
ip netns add h3
ip netns add h4
ovs-vsctl add-br s1
ip link add h1-eth0 type veth peer name s1-eth1
ip link add h1-eth1 type veth peer name s1-eth2
ip link add h2-eth0 type veth peer name s1-eth3
ip link add h3-eth0 type veth peer name s1-eth4
ip link add h4-eth0 type veth peer name s1-eth5
ip link set h1-eth0 netns h1
ip link set h1-eth1 netns h1
ip link set h2-eth0 netns h2
ip link set h3-eth0 netns h3
ip link set h4-eth0 netns h4
ovs-vsctl add-port s1 s1-eth1
ovs-vsctl add-port s1 s1-eth2
ovs-vsctl add-port s1 s1-eth3
ovs-vsctl add-port s1 s1-eth4
ovs-vsctl add-port s1 s1-eth5
ip link set s1-eth1 up
ip link set s1-eth2 up
ip link set s1-eth3 up
ip link set s1-eth4 up
ip link set s1-eth5 up
ip netns exec h2 ip link set lo up
ip netns exec h2 ip link set h2-eth0 up
ip netns exec h2 ip addr add 10.0.0.2/24 dev h2-eth0
ip netns exec h3 ip link set lo up
ip netns exec h3 ip link set h3-eth0 up
ip netns exec h3 ip addr add 10.0.0.3/24 dev h3-eth0
ip netns exec h4 ip link set lo up
ip netns exec h4 ip link set h4-eth0 up
ip netns exec h4 ip addr add 10.0.0.4/24 dev h4-eth0
然后输入命令ip netns exec h1 bash 进入h1的空间之中,首先新建文件/etc/modprobe.d/bonding.conf,并写入命令:
alias bond0 bonding
options bonding mode=4
然后执行命令
modprobe bonding
接下来,在h1中执行如下命令,下列命令是将h1的两个网卡绑定在一个逻辑平面之中,并统一设置一个ip地址。
ip link add bond0 type bond
ip link set bond0 address 02:01:02:03:04:08
ip link set h1-eth0 down
ip link set h1-eth1 down
ip link set h1-eth0 address 00:00:00:00:00:11
ip link set h1-eth0 address 00:00:00:00:00:22
ip link set h1-eth0 master bond0
ip link set h1-eth1 master bond0
ip addr add 10.0.0.1/24 dev bond0
ip link set bond0 up
最后,设置s1的远程控制器,命令:ovs-vsctl set-controller s1 tcp:127.0.0.1:6633
至此,本次实验的拓扑设置完成。
(说明:控制器运行simple_switch_13.py,拓扑使用上述拓扑,也可以正常的进行通信,但是倘若,将h1的其中一个端口移除逻辑平面,由于控制器无法得知端口状态的改变,所以流表项并不会更新,接下来的通信就可能会中断,也就达不到LACP可以利用冗余防止单点失效的效果,所以在SDN网络架构下,通过利用ryu程序实现LACP的功能,就需要控制器可以获知端口的状态情况,进而动态的改变流表项)
二、程序
在ryu的ryu/ryu/lib/lacplib.py中是ryu关于lacp的函数库,里面已经实现了各种lacp的连接和操作,我们只需要针对库进行调用,然后加入流表的添加和删除动作即可。
关于lacplib.py 的介绍,各位看官可以参考ryubook第四章的相关内容,可在链接(https://github.com/Yang-Jianlin/ryu/tree/master/spec_book)中下载。
本博文主要讲解一下,如何编写ryu应用程序在控制器中运行,实现LACP的功能。
新建一个类NetLacp,内容如下:
这里版本选择了openflow1.3,_CONTEXTS的含义主要是为该应用程序的运行添加依赖库,例如一个simple_switch.py的应用,如果没有OFPHandler应用作为数据收发和解析的基础的话,是无法运行的;
初始化中,add()方法在lacplib之中,作用就是将id为0000000000000001的交换机的1、2端口整合为一个逻辑平面,也就是和拓扑的h1的h1-eth0、h1-eth1对应,若要组成多个逻辑平面,只需要重复的调用add()方法即可。
class NetLacp(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
# 添加该应用程序运行所需要的依赖程序
_CONTEXTS = {'lacplib': lacplib.LacpLib}
def __init__(self, *args, **kwargs):
super(NetLacp, self).__init__(*args, **kwargs)
self._lacp = kwargs['lacplib']
# 将交换机01的1、2端口绑定为一个逻辑平面
# 若网络中存在着多个逻辑平面,多个LACP,就需要调用多次add()
self._lacp.add(dpid=str_to_dpid('0000000000000001'), ports=[1, 2])
self.mac_table = {}
由于是在控制器实现LACP功能,所以在控制器和交换机完成握手动作后,就需要向交换机下发table-miss流表项(默认流表项),代码如下,add_flow()是添加流表项的方法,关于这一段代码的详细解析,参见:https://blog.csdn.net/weixin_40042248/article/details/115749340
def add_flow(self, datapath, priority, match, actions):
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
command = ofp.OFPFC_ADD
inst = [ofp_parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
req = ofp_parser.OFPFlowMod(datapath=datapath, command=command,
priority=priority, match=match, instructions=inst)
datapath.send_msg(req)
# 当控制器和交换机开始的握手动作完成后,进行table-miss(默认流表)的添加
# 关于这一段代码的详细解析,参见:https://blog.csdn.net/weixin_40042248/article/details/115749340
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
msg = ev.msg
datapath = msg.datapath
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
# add table-miss
match = ofp_parser.OFPMatch()
actions = [ofp_parser.OFPActionOutput(ofp.OFPP_CONTROLLER, ofp.OFPCML_NO_BUFFER)]
self.add_flow(datapath=datapath, priority=0, match=match, actions=actions)
接下来,需要自定义packet_in的处理方法,用于packet_in消息的处理,控制器根据收到的交换机的消息进行转发表的学习和流表的下发等操作,虽然在lacplib中已经包含packet_in_handler方法了,但是lacp库中的函数不包含对消息的处理,仅仅是对LACP data unit的处理。
所以,构建packet_in_handler()函数,代码如下:
# 需要对流表和流表项做一系列的操作,这里和自学习交换机的packet_in一个道理
@set_ev_cls(lacplib.EventPacketIn, MAIN_DISPATCHER)
def packet_in_handler(self, ev):
global src, dst
msg = ev.msg
datapath = msg.datapath
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
dpid = datapath.id
# msg实际上是json格式的数据,通过解析,找出in_port
# 可用print(msg)查看详细数据
in_port = msg.match['in_port']
# 接下来,主要是解析出源mac地址和目的mac地址
pkt = packet.Packet(msg.data)
for p in pkt.protocols:
if p.protocol_name == 'ethernet':
src = p.src
dst = p.dst
print('src:{0} dst:{1}'.format(src, dst))
# 字典的样式如下
# {'dpid':{'src':in_port, 'dst':out_port}}
self.mac_table.setdefault(dpid, {})
# 转发表的每一项就是mac地址和端口,所以在这里不需要额外的加上dst,port的对应关系,其实返回的时候目的就是源
self.mac_table[dpid][src] = in_port
# 若转发表存在对应关系,就按照转发表进行;没有就需要广播得到目的ip对应的mac地址
if dst in self.mac_table[dpid]:
out_port = self.mac_table[dpid][dst]
else:
out_port = ofp.OFPP_FLOOD
actions = [ofp_parser.OFPActionOutput(out_port)]
# 如果执行的动作不是flood,那么此时应该依据流表项进行转发操作,所以需要添加流表到交换机
if out_port != ofp.OFPP_FLOOD:
match = ofp_parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
self.add_flow(datapath=datapath, priority=1,
match=match, actions=actions)
data = None
if msg.buffer_id == ofp.OFP_NO_BUFFER:
data = msg.data
# 控制器指导执行的命令
out = ofp_parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)
最后,程序需要对端口的状态改变做出反应,也就是说,在本例中,h1-eth0和h1-eth1的状态改变,即从逻辑平面bond0中移除后,控制器需要对移除做出反应,改变相应的流表项,这里对流表项的处理相对简单,只需要移除对应的流表项,然后重新进行自学习即可。代码如下:
# 删除流表的方法,本应用中,在物理平面的状态改变(所属的逻辑平面变化)之后,
# 需要将对于的流表项清除,否则达不到LACP的效果
def del_flow(self, datapath, match):
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
req = ofp_parser.OFPFlowMod(datapath=datapath,
command=ofp.OFPFC_DELETE,
out_port=ofp.OFPP_ANY,
out_group=ofp.OFPG_ANY,
match=match)
datapath.send_msg(req)
# 当交换机的状态出现变化时,就需要对流表做一些操作
@set_ev_cls(lacplib.EventSlaveStateChanged, MAIN_DISPATCHER)
def slave_state_changed_handler(self, ev):
# 对于这四行解析之所以这样,不同于之前的ev.msg,
# 可以打开lacplib.EventSlaveStateChanged中看一下初始化函数就明白了
datapath = ev.datapath
dpid = datapath.id
port = ev.port
enabled = ev.enabled
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
if dpid in self.mac_table:
for mac in self.mac_table[dpid]:
match = ofp_parser.OFPMatch(eth_dst=mac)
self.del_flow(datapath=datapath, match=match)
del self.mac_table[dpid]
self.mac_table.setdefault(dpid, {})
至此,实验的所有准备条件已经编写完成,关于实验的步骤各位在阅读https://blog.csdn.net/weixin_40042248/article/details/116395325?spm=1001.2014.3001.5501之后,可以自行进行实验,也可以参考ryubook相关内容的讲解,这里不再赘述,如有问题,读者留言或者私聊。
全部代码附录:
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.lib.dpid import str_to_dpid
from ryu.lib.packet import packet
from ryu.ofproto import ofproto_v1_3
from ryu.lib import lacplib
from ryu.controller.handler import set_ev_cls
from ryu.controller.handler import MAIN_DISPATCHER
from ryu.controller.handler import CONFIG_DISPATCHER
'''
应用程序主要实现网络聚合(LACP)的功能,
'''
class NetLacp(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
# 添加该应用程序运行所需要的依赖程序
_CONTEXTS = {'lacplib': lacplib.LacpLib}
def __init__(self, *args, **kwargs):
super(NetLacp, self).__init__(*args, **kwargs)
self._lacp = kwargs['lacplib']
# 将交换机01的1、2端口绑定为一个逻辑平面
# 若网络中存在着多个逻辑平面,多个LACP,就需要调用多次add()
self._lacp.add(dpid=str_to_dpid('0000000000000001'), ports=[1, 2])
self.mac_table = {}
def add_flow(self, datapath, priority, match, actions):
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
command = ofp.OFPFC_ADD
inst = [ofp_parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
req = ofp_parser.OFPFlowMod(datapath=datapath, command=command,
priority=priority, match=match, instructions=inst)
datapath.send_msg(req)
# 删除流表的方法,本应用中,在物理平面的状态改变(所属的逻辑平面变化)之后,
# 需要将对于的流表项清除,否则达不到LACP的效果
def del_flow(self, datapath, match):
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
req = ofp_parser.OFPFlowMod(datapath=datapath,
command=ofp.OFPFC_DELETE,
out_port=ofp.OFPP_ANY,
out_group=ofp.OFPG_ANY,
match=match)
datapath.send_msg(req)
# 当控制器和交换机开始的握手动作完成后,进行table-miss(默认流表)的添加
# 关于这一段代码的详细解析,参见:https://blog.csdn.net/weixin_40042248/article/details/115749340
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
msg = ev.msg
datapath = msg.datapath
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
# add table-miss
match = ofp_parser.OFPMatch()
actions = [ofp_parser.OFPActionOutput(ofp.OFPP_CONTROLLER, ofp.OFPCML_NO_BUFFER)]
self.add_flow(datapath=datapath, priority=0, match=match, actions=actions)
# 需要对流表和流表项做一系列的操作,这里和自学习交换机的packet_in一个道理
@set_ev_cls(lacplib.EventPacketIn, MAIN_DISPATCHER)
def packet_in_handler(self, ev):
global src, dst
msg = ev.msg
datapath = msg.datapath
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
dpid = datapath.id
# msg实际上是json格式的数据,通过解析,找出in_port
# 可用print(msg)查看详细数据
in_port = msg.match['in_port']
# 接下来,主要是解析出源mac地址和目的mac地址
pkt = packet.Packet(msg.data)
for p in pkt.protocols:
if p.protocol_name == 'ethernet':
src = p.src
dst = p.dst
print('src:{0} dst:{1}'.format(src, dst))
# 字典的样式如下
# {'dpid':{'src':in_port, 'dst':out_port}}
self.mac_table.setdefault(dpid, {})
# 转发表的每一项就是mac地址和端口,所以在这里不需要额外的加上dst,port的对应关系,其实返回的时候目的就是源
self.mac_table[dpid][src] = in_port
# 若转发表存在对应关系,就按照转发表进行;没有就需要广播得到目的ip对应的mac地址
if dst in self.mac_table[dpid]:
out_port = self.mac_table[dpid][dst]
else:
out_port = ofp.OFPP_FLOOD
actions = [ofp_parser.OFPActionOutput(out_port)]
# 如果执行的动作不是flood,那么此时应该依据流表项进行转发操作,所以需要添加流表到交换机
if out_port != ofp.OFPP_FLOOD:
match = ofp_parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
self.add_flow(datapath=datapath, priority=1,
match=match, actions=actions)
data = None
if msg.buffer_id == ofp.OFP_NO_BUFFER:
data = msg.data
# 控制器指导执行的命令
out = ofp_parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)
# 当交换机的状态出现变化时,就需要对流表做一些操作
@set_ev_cls(lacplib.EventSlaveStateChanged, MAIN_DISPATCHER)
def slave_state_changed_handler(self, ev):
# 对于这四行解析之所以这样,不同于之前的ev.msg,
# 可以打开lacplib.EventSlaveStateChanged中看一下初始化函数就明白了
datapath = ev.datapath
dpid = datapath.id
port = ev.port
enabled = ev.enabled
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
if dpid in self.mac_table:
for mac in self.mac_table[dpid]:
match = ofp_parser.OFPMatch(eth_dst=mac)
self.del_flow(datapath=datapath, match=match)
del self.mac_table[dpid]
self.mac_table.setdefault(dpid, {})
github地址:https://github.com/Yang-Jianlin/ryu/blob/master/ryu/app/lacp_yjl.py