Ryu拓扑发现原理分析

Ryu拓扑发现的核心模块是ryu/topology目录下的switches.py,拓扑发现的应用是同目录下的dumper.py。在dumper.py中,会利用_CONTEXTS来实例化switches.py中的Switches类,然后将拓扑发现的相关信息通过日志方式(LOG.debug)显示。启动命令如下所示:

ryu-manager –verbose –observe-links ryu.topology.dumper 

或者

ryu-manager –verbose –observe-links ./ryu/topology/dumper.py

其中–verbose参数用于显示LOG.debug信息,–observe-links用于指明拓扑发现。


接下来对拓扑发现的核心模块switches.py进行分析。

1. Port类

存储端口相关信息,数据成员有:

self.dpid = dpid
self._ofproto = ofproto
self._config = ofpport.config
self._state = ofpport.state
self.port_no = ofpport.port_no
self.hw_addr = ofpport.hw_addr
self.name = ofpport.name

其中要特别注意的是dpid和port_no,即交换机ID和端口号,这两个信息在下发流表项时很重要。

2. Switch类

存储交换机相关信息,数据成员有:

self.dp = dp
self.ports = []

其中dp是Datapath类的实例,该类定义在在ryu/controller/controller.py,主要属性有:

self.socket = socket
self.address = address
self.is_active = True
self.id = None # datapath_id is unknown yet
self.ports = None

ports是一个由Port类实例组成的列表,存储该交换机的端口。

3. Link类

保存的是源端口和目的端口(都是Port类实例),数据成员有:

self.src = src
self.dst = dst

4. PortState类

该类继承自dict,保存了从port_no(int型)到port(OFPPort类实例)的映射。该类主要用作self.port_state字典的值(键是dpid),用于存储dpid对应的交换机的所有端口情况。

OFPPort类定义在ryu/ofproto目录下对应的ofproto_v1_X_parser.py中(X代表版本号),继承自一个namedtuple,保存有port_no等信息。

5. PortData类

保存每个端口与对应的LLDP报文数据,数据成员有:

self.is_down = is_down
self.lldp_data = lldp_data(这是LLDP报文的数据)
self.timestamp = None
self.sent = 0

每调用一次lldp_sent函数,便会把self.timestamp置为当前的时间(time.time()),并将self.sent加1;每调用一次lldp_received函数,便会把self.sent置为0。

6.PortDataState类

继承自dict类,保存从Port类到PortData类的映射。该类维护了一个类似双向循环链表的数据结构,并重写了__iter__(),使得遍历该类的实例(self.ports)时,会按照该双向循环链表从哨兵节点(self._root)后一个节点开始遍历。

包含一个add_port函数,传入port和lldp_data,port作键,构建的PortData类实例作为值。

包含一个lldp_sent(self,port)函数,根据传入的port(Port类实例)获得对应的PortData类实例port_data,然后调用port_data.lldp_sent()(该函数会设置时间戳),再调用self._move_last_key(port),把该port移到类似双向循环链表的数据结构中哨兵节点的前面(相当于下次遍历的末尾);最后返回port_data。

7. LinkState类

继承自dict,保存从Link类到时间戳的映射。数据成员self._map字典用于存储Link两端互相映射的关系。

8. LLDPPacket类

静态方法lldp_packet(dpid,port_no,dl_addr,ttl)用于构造LLDP报文,静态方法lldp_parse(data)用于解析LLDP包,并返回源DPID和源端口号。

9. Switches类

该类是Ryu拓扑发现的核心所在。Switches类是app_manager.RyuApp类的子类,当运行switches应用时会被实例化,其__init__函数主要包括:

self.name = ‘switches’
self.dps = {} # datapath_id => Datapath class
self.port_state = {} # datapath_id => ports
self.ports = PortDataState() # Port class -> PortData class
self.links = LinkState() # Link class -> timestamp
self.is_active = True

self.dps字典用于保存dpid到Datapath类实例的映射,会在_register函数中添加新成员,_unregister函数中删除成员。遍历该字典可以得到连接的所有交换机。

self.port_state字典中键为dpid,值为PortState类型。遍历该字典可以得到所有交换机对应的端口情况。当交换机连接时,会检查交换机的id是否在self.port_state中,不在则创建PortState类实例,把交换机的所有端口号和端口存储到该实例中;交换机断开时,会从self.port_state中删除。

self.ports是PortDataState类的实例,保存每个端口(Port类型)对应的LLDP报文数据(保存在PortData类实例中),遍历self.ports用于发送LLDP报文。

self.links是LinkState类的实例,保存所有连接(Link类型)到时间戳的映射。遍历self.links的键即可得到所有交换机之间的连接情况。

如果ryu-manager启动时加了–observe-links参数,则下面的self.link_discovery将为真,从而执行if下面的语句:

self.link_discovery = self.CONF.observe_links
if self.link_discovery:
self.install_flow = self.CONF.install_lldp_flow
self.explicit_drop = self.CONF.explicit_drop
self.lldp_event = hub.Event()
self.link_event = hub.Event()
self.threads.append(hub.spawn(self.lldp_loop))
self.threads.append(hub.spawn(self.link_loop))

综上所述,该初始化函数__init__()主要是创建用于存储相关信息的数据结构,创建两个事件,然后调用hub.spawn创建两个新线程执行self.lldp_loop和self.link_loop两个函数。

9.1 lldp_loop函数

lldp_loop函数里是一个while循环,只要self.is_active为真,就一直循环执行。(close函数会把self.is_active置为False,该函数在离开模块时自动被调用)。

(1)执行self.lldp_event.clear(),将Event类实例lldp_event的_cond属性设为False,用于线程间同步。

提到线程同步,常用的函数有:

Event.wait()

Event对象的wait的方法只有内部信号为真的时候才会很快的执行并完成返回。当Event对象的内部信号标识为假时,则wait方法一直等待其为真时才返回。同时可以对wait设置timeout,当达到timeout设置的时间的时候就可以完成返回或执行。

Event.set()

将标识位设为Ture

Event.clear()

将标识伴设为False。

Event.isSet()

判断标识位是否为Ture。

(2)创建ports_now和ports两个列表,分别存储尚未发送过LLDP报文的端口和已发送过LLDP报文并且超时的端口。

(3)遍历self.ports(PortDataState类的实例),获得key(Port类实例)和data(PortData类实例),如果data.timestamp为None(该端口还没发送过LLDP报文),则将key(端口)加入ports_now列表;否则,计算下次应该发送LLDP报文的时间expire,如果已经超时,则放到ports列表,否则就是还没到发送时间,停止遍历(发送LLDP报文时是按序发的,找到第一个未超时的端口,后面的端口肯定更没有超时,因为后面端口上次发送LLDP是在前一端口之后,前一个都没超时后面的自然也没超时)。

(4)遍历ports_now列表,对每个端口调用self.send_lldp_packet(port),发送LLDP报文。

send_lldp_packet函数执行过程如下:

a. 调用PortDataState类的lldp_sent函数,该函数会设置时间戳,移动相应端口在双向循环链表中的位置,最后返回PortData类实例port_data;

b. 如果该端口已经down掉,直接返回,否则执行下一步;

c. 根据port.dpid得到对应的Datapath类实例dp,如果不存在,则直接返回,否则执行下一步;

d. 发送LLDP报文。具体地:(1)生成actions:从port.port_no端口发出消息;(2)生成PacketOut消息:datapath指定为上一步得到的dp,actions为前面的,data为步骤a中返回的port_data的lldp_data;

(5)遍历ports列表,对每个端口调用self.send_lldp_packet(port),发送LLDP报文。

9.2 link_loop函数

link_loop函数也是一个while循环,只要self.is_active为真,就一直循环执行;

(1)执行self.link_event.clear(),将Event类实例link_event的_cond属性设为False,用于线程间同步;

(2)创建deleted列表;

(3)遍历self.links(LinkState类实例),获得link(Link类实例)和timestamp时间戳。如果已经超时,且该link对应的源端口是否在self.ports中,并且发送LLDP次数已超过self.LINK_LLDP_DROP,则添加到deleted列表中;

(4)遍历deleted列表,执行:

a. 对其中的每条需要删除的link调用link_down函数(该函数会删除self.links中link对应的项目,并删除self.links._map中link对应的项目),并触发EventLinkDelete事件。

b. 得到link对应的反向link,如果反向link不在deleted列表中,则将self.links中反向link的时间戳置为超时的事件,并将对端端口从self.ports的双向循环链表中移动到哨兵节点的后面(下次检查的开头),以便尽早检查反向link是否也断开了。

9.3 state_change_handler

该函数用于处理EventOFPStateChange事件,当交换机连接或者断开时会触发该事件。

如果状态是MAIN_DISPATCHER:

(1)从ev.datapath获得Datapath类实例dp,如果该dp的dpid已经在self.dps里有,则报出重复链接的警告。

(2)调用_register(),将dp.id和dp添加到self.dps中;如果该dp.id不在self.port_state中,则创建该dp.id对应的PortState实例,并遍历dp.ports.values,将所有port(OFPPort类型)添加到该PortState实例中。

(3)调用_get_switch(),如果dp.id在self.dps中,则创建一个Switch类实例,并把self.port_state中对应的端口都添加到该实例中,最终返回该实例。

(4)如果交换机没有重复连接,触发EventSwitchEnter事件。

(5)如果没设置self.link_discovery,返回;否则执行下一步。

(6)如果设置了self.install_flow,则根据OpenFlow版本生成相应流表项,使得收到的LLDP报文(根据目的MAC地址匹配)上报给控制器。

(7)如果交换机没有重复连接,则遍历(3)中得到的switch.ports的所有端口,如果端口port不是被保留的,则调用self._port_added(port),该函数会调用LLDPPacket.lldp_packet()函数生成LLDP报文数据lldp_data(用于和“port.is_down()”一起构造PortData类实例),然后调用PortDataState类的add_port(port,lldp_data)。

add_port()函数会检查port是否在self.ports中,不在则将该port添加到双向循环链表中哨兵节点的后面(下次检查的开头),并把port和对应的PortData类实例(该端口对应的LLDP报文数据)添加到self.ports中。

(8)调用self.lldp_event.set()

如果状态是DEAD_DISPATCHER:

(1)如果dp.id为None,即握手之前交换机就断开连接了,则直接返回;否则执行下一步。

(2)调用_get_switch()获得Switch实例;

(3)调用_unregister(),从self.dps和self.port_state中删除该dpid对应的数据;

(4)触发EventSwitchLeave事件。

(5)如果没有设置link_discovery,返回;否则执行下一步。

(6)遍历switch.ports中的每个端口port,如果不是保留端口,则调用PortDataState类的del_port(),将self.ports中port对应的数据删除;调用Switches类的_link_down()。_link_down函数执行如下操作:

a. 调用LinkState类的port_deleted函数。在port_deleted函数里,首先调用get_peer()获得对端端口,然后生成两个Link对象(src->dst和dst->src),并将这两个对象从self.links中删除(反向Link可能不存在);删除src->dst和dst->src之间的映射(存储在_map字典中)。最后返回传入的port对应的对端port和传入的port本身。

b. 根据返回的“传入的port对应的对端port和传入的port本身”,创建Link对象,触发EventLinkDelete事件(如果反向连接也存在,会触发两次EventLinkDelete事件)。

c. 调用self.ports.move_front(dst),该函数会从self.ports中得到dst对应的PortData类实例port_data,如果port_data不为None,则调用clear_timestamp函数将其timestamp属性置为None,并将dst移动到双向循环链表中哨兵节点的后面(下次检查的开头)。

(7)调用self.lldp_event.set()。

9.4 port_status_handler

该函数用于处理EventOFPPortStatus事件,该事件是交换机主动发给控制器的。

如果原因为“添加”:

(1)在self.port_state里dp.id对应的PortState实例中添加该端口,并触发EventPortAdd事件。

(2)如果没有设置self.link_discovery,则返回;否则执行下一步。

(3)调用_get_port函数,该函数首先根据传入的dpid得到Switch实例,然后遍历实例的ports列表,找到并返回传入的端口号对应的端口(Port类实例)。如果找到了端口并且端口不是保留的,则调用_port_added(),该函数会获得LLDP相关的数据部分(用于构造PortData类实例),然后调用PortDataState类的add_port(),该函数会将Port和对应的PortData映射关系存储到self.ports中;调用self.lldp_event.set()。

如果原因为“删除”:

(1)在self.port_state里该dpid对应的PortState实例中删除该端口,并触发EventPortDelete事件。

(2)如果没有设置self.link_discovery,则返回;否则执行下一步。

(3)调用_get_port函数,该函数首先根据传入的dpid得到Switch实例,然后遍历实例的ports列表,找到并返回传入的端口号对应的端口(Port类实例)。如果找到了端口并且端口不是保留的,则:

a. 调用del_port(),将该端口及对应的PortData从self.ports删除;

b. 调用_link_down(),该函数会调用LinkState类的port_deleted函数,并返回传入的port对应的对端port和传入的port本身。在port_deleted函数里,首先调用get_peer()获得对端端口,然后生成两个Link对象(src->dst和dst->src),并将这两个对象从self.links中删除(反向Link可能不存在);删除src->dst和dst->src之间的映射(存储在_map字典中)。根据返回的“传入的port对应的对端port和传入的port本身”,创建Link对象,触发EventLinkDelete事件(如果反向连接也存在,会触发两次EventLinkDelete事件)。调用self.ports.move_front();

c. 调用self.lldp_event.set()。如果原因为“修改”:

(1)修改self.port_state里该dpid对应的PortState实例值,并触发EventPortModify事件。

(2)如果没有设置self.link_discovery,则返回;否则执行下一步。

(3)调用_get_port函数,该函数首先根据传入的dpid得到Switch实例,然后遍历实例的ports列表,找到并返回传入的端口号对应的端口(Port类实例)。如果找到了端口并且端口不是保留的:

a. 调用PortDataState类的set_down(),该函数会调用Port类的is_down(),检测端口是否已关闭;获得Port对应的PortData实例,调用PortData的set_down函数,将对应的is_down修改为当前状态(布尔值);调用PortData的clear_timestamp(),将对应的timestamp修改为None。如果检测端口没有关闭,调用_move_front_key()。set_down函数返回是否已关闭的检测结果。如果检测结果是已关闭,则调用_link_down()。

b. 调用self.lldp_event.set()。

9.5 packet_in_handler

该函数用于处理EventOFPPacketIn事件。

(1)如果没有设置self.link_discovery,直接返回;否则执行下一步。

(2)尝试调用LLDPPacket.lldp_parse(msg.data)来按照LLDP报文格式解码收到的报文,获得源交换机dpid和源端口号(该LLDP报文从哪台交换机的哪个端口发出的)。如果不是LLDP报文格式,返回;否则执行下一步。

(3)获得目的交换机的dpid和目的端口(上报Packet_In消息的交换机dpid和接收到LLDP报文的端口号)。

(4)调用_get_port函数,得到源端口对应的Port类实例。如果不存在或者该实例的dpid跟目的dpid相同,则直接返回;否则执行下一步。

(5)调用PortDataState类的lldp_received函数,该函数会再调用PortData类的lldp_received函数,将对应的self.sent值置为0。

(6)调用_get_port函数,得到目的端口对应的Port类实例。如果不存在该实例,则返回;否则执行下一步。

(7)调用LinkState类的get_peer函数,得到源端口原先对应的目的端口。如果该目的端口存在,且与现在解析得到的目的端口不同,则说明原先的链路已断开,触发EventLinkDelete事件。

(8)根据源端口和目的端口构造Link类实例,如果该实例不存在于self.links,则说明是新链路,触发EventLinkAdd事件。

(9)调用LinkState类的update_link函数,该函数会将上一步构造的Link类实例加上时间戳存储到self.links中,并构造逆向链路,返回逆向链路是否在self.links中的布尔值。如果逆向链路还不存在,那很有可能会马上存在,因此调用PortDataState类的move_front函数,将目的端口移动到双向循环链表中哨兵节点的后面(下次检查的头部),尽早检查;调用self.lldp_event.set()。

(10)如果设置了self.explicit_drop,则调用_drop_packet函数。

10. 拓扑发现概述

Switches类的初始化函数__init__()创建用于存储相关信息的数据结构(self.dps、self.port_state、self.ports和self.links),创建两个事件(self.lldp_event 和self.link_event),然后调用hub.spawn创建两个新线程执行self.lldp_loop和self.link_loop两个函数。其他工作就交给事件触发和事件处理函数了。

交换机连接时触发EventOFPStateChange事件,在对应的处理函数state_change_handler中会把连接上的交换机存储到self.dps中,并把交换机的端口情况存储到self.port_state中,并生成相应的LLDP报文数据,存储在self.ports中(键为Port类型,值为PortData类型,PortData类的数据成员lldp_data存储LLDP报文数据)。

lldp_loop函数会不停遍历self.ports,并在需要的时候由send_lldp_packet函数执行发送LLDP报文的操作。

当LLDP报文被送回到控制器时,触发EventOFPPacketIn事件,对应的处理函数packet_in_handler会解析LLDP报文,得到交换机之间的连接信息(Link类),存储到self.links中。

link_loop函数会遍历self.links,及时检查链路是否还是活的。

后记:

实际使用Ryu获取拓扑信息时,更好的方式是使用Ryu提供的REST API,具体方法将在下文中介绍。但分析switches.py的过程对了解Ryu的工作机制和应用编写方法还是蛮有用的。

作者:何妍 

来源:51CTO

时间: 2024-09-20 01:10:50

Ryu拓扑发现原理分析的相关文章

提高SDN控制器的拓扑发现性能

SDN网络的一大特点就是资源由控制器集中管理,控制器管理网络,最基本的当然需要知道网络的拓扑,而网络拓扑可能时时发生变化,所以控制器需要时时监测,对于整个网络来说,控制器担负了太多的计算任务,所以如果能够帮助控制器减压,则会提高整个网络的性能.本篇文章将以ryu控制器为例,首先介绍传统网络和现在SDN网络的拓扑发现原理,然后介绍改进算法,最后讲解改写后的代码逻辑. 一. LLDP拓扑发现原理 传统网络中的链路发现协议为LLDP(Link Layer Discovery Protocol),LLD

【转】网络拓扑发现原理研究

http://hi.baidu.com/renyijiu/blog/item/4ad68246b18aec0d6b63e5d3.html     1.背景描述     随着信息时代的到来,对计算机网络的依赖使得计算机网络本身运行的可靠性变得至关重要,对网络管理也就有了更高的要求.     按照OSI的定义,网络管理主要包括五个功能域:故障管理.配置管理.性能管理.安全管理和计费管理.在五大功能域中,配置管理是基础,它的主要功能包括发现网络的拓扑结构.监视和管理网络设备的配置情况.其它的各项功能都

搜索引擎判断网站是否作弊的原理分析(二)

承接搜索引擎判断网站是否作弊的原理分析(一) 广州SEO陈永继续为大家分析信任传播模型.不信任传播模型及异常发现模型3个代表算法,它们分别是TrustRank算法.BadRank算法和SpamRank算法. 我们先详细介绍TrustRank算法 TrustRank算法属于信任传播模型,基本遵循信任传播模型的流程,即算法流程如下两个步骤组成. 步骤一:确定值得信任的网页集合 TrustRank算法需要靠人工审核来判断某个网页应该被放入网页集合,考虑到人工审核工作量大,所以提出了两种初选信任网页集合

IOS开发:Cocos2d触摸分发原理分析

  触摸是iOS程序的精髓所在,良好的触摸体验能让iOS程序得到非常好的效果,例如Clear.鉴于同学们只会用cocos2d的 CCTouchDispatcher 的 api 但并不知道工作原理,但了解触摸分发的过程是极为重要的.毕竟涉及到权限.两套协议等的各种分发. 本文以cocos2d-iphone源代码为讲解.cocos2d-x 于此类似,就不过多赘述了. 零.cocoaTouch的触摸 在讲解cocos2d触摸协议之前,我觉得我有必要提一下CocoaTouch那四个方法.毕竟cocos2

web上存漏洞及原理分析、防范方法(文件名检测漏洞)

我们通过前篇:<web上存漏洞及原理分析.防范方法(安全文件上存方法) >,已经知道后端获取服务器变量,很多来自客户端传入的.跟普通的get,post没有什么不同.下面我们看看,常见出现漏洞代码.1.检测文件类型,并且用用户上存文件名保存 复制代码 代码如下: if(isset($_FILES['img'])) { $file = save_file($_FILES['img']); if($file===false) exit('上存失败!'); echo "上存成功!"

ToyBricks简介以及原理分析

ToyBricks背景 我始终认为,在高内聚,低耦合的原则下,进行组件化,模块化,插件化都是移动应用开发的趋势. 为什么这么说呢?下面我们举个栗子:大家都知道,以前Android应用开发中,可以使用HttpClient或者HttpUrlConnection来进行http访问.这里假设有一个耦合严重,但代码量巨大的项目,使用了基于HttpClient封装的loopj/android-async-http来进行http访问.但是,后来,Google明确支持使用HttpUrlConnection.此时

ShellShock漏洞原理分析

9月24日,广泛存在于Linux的bash漏洞曝光.因为此漏洞可以执行远程命令,所以极为危.危害程度可能超过前段时间的心脏流血漏洞. 漏洞编号CVE-2014-6271,以及打了补丁之后被绕过的CVE-2014-7169,又称ShellShock漏洞. 漏洞起因: 要是用一句话概括这个漏洞,就是代码和数据没有正确区分. 此次漏洞很像SQL注入,通过特别设计的参数使得解析器错误地执行了参数中的命令.这其实是所有解析性语言都可能存在的问题. 1 env x='() { :;}; echo vulne

AbstractQueuedSynchronizer的介绍和原理分析(转)

  简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态.然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作: java.util.concurrent.locks.Abst

PDO防注入原理分析以及使用PDO的注意事项总结_php技巧

本文详细讲述了PDO防注入原理分析以及使用PDO的注意事项,分享给大家供大家参考.具体分析如下: 我们都知道,只要合理正确使用PDO,可以基本上防止SQL注入的产生,本文主要回答以下两个问题: 为什么要使用PDO而不是mysql_connect? 为何PDO能防注入? 使用PDO防注入的时候应该特别注意什么?  一.为何要优先使用PDO? PHP手册上说得很清楚:Prepared statements and stored procedures Many of the more mature d