SDN(三) RYU控制器相關筆記

1 引言

積跬步以至千里,積怠情以至深淵。

RYU是由日本NTT公司研發的開源SDN控制器,由Python語言編寫。支持OpenFlow1.0、1.2、1.3、1.4和1.5版本的協議。本人將在此文檔中持續更新關於RYU控制器的一些學習筆記,包括RYU的運行流程、部分源碼解讀、應用開發以及RYU具體使用。望讀者能在共同學習的同時,批評指正。

2 RYU源碼解讀

2.1 RYU源文件目錄結構

ryu/ryu目錄下的主要目錄內容如下:

(1) base:

base中有一個非常重要的文件:app_manager.py,其作用是RYU應用的管理中心,即對其他組件的管理。用於加載RYU應用程序,接受從APP發送過來的信息,同時也完成消息的路由。會被ryu-manager自動調用。

其主要的函數有app註冊、註銷、查找、並定義了RyuApp基類,定義了RYUAPP的基本屬性。包含name, threads, events, event_handlers和observers等成員,以及對應的許多基本函數。如:start(), stop()等。

這個文件中還定義了AppManager基類,用於管理APP。定義了加載APP等函數。不過如果僅僅是開發APP的話,這個類可以不必關心。

(2) controller: 實現controller和交換機之間的互聯和事件處理

controller文件夾中許多非常重要的文件,如events.py, ofp_handler.py, controller.py等。
在controller.py中定義了OpenFlowController基類,是控制器組件,管理與OF交換機連接的安全通道,接收OF消息,調用ofp_events,併發布相應的“事件”,以觸發訂閱了該“事件”的組件的處理邏輯。

在ofp_handler.py中定義了基本的handler,完成了基本的如:握手,錯誤信息處理和keep alive 等功能。更多的如packet_in_handler應該在app中定義。

在dpset.py文件中,定義了交換機端的一些消息,如端口狀態信息等,用於描述和操作交換機。如添加端口,刪除端口等操作。

(3) lib:網絡基本協議的實現和使用

lib中定義了我們需要使用到的基本的數據結構,如dpid, mac和ip等數據結構。在lib/packet目錄下,還定義了許多網絡協議,如ICMP, DHCP, MPLS和IGMP等協議內容。而每一個數據包的類中都有parser和serialize兩個函數。用於解析和序列化數據包。

lib目錄下,還有ovs, netconf目錄,對應的目錄下有一些定義好的數據類型

(4) ofproto

在這個目錄下,基本分爲兩類文件,一類是協議的數據結構定義,另一類是協議解析,也即數據包處理函數文件。

(5) topology:交換機和鏈路的查詢模塊

包含了switches.py等文件,基本定義了一套交換機的數據結構。event.py定義了交換上的事件。dumper.py定義了獲取網絡拓撲的內容。最後api.py向上提供了一套調用topology目錄中定義函數的接口。

(6) contrib:第三方庫

這個文件夾主要存放的是開源社區貢獻者的代碼。

(7) cmd:入口函數

定義了RYU的命令系統,爲controller的執行創建環境,接收和處理相關命令

(8) services

完成了BGP (路由技術) 和vrrp (交換技術) 的實現

(9) tests
tests目錄下存放了單元測試以及整合測試的代碼。

2.2 ryu/app目錄下源碼解讀及相關API的使用

2.2.1 ryu/app/simple_switch_13.py實現功能爲:傳統的2層交換機策略

對於OpenFlow交換機,接受RYU控制器的指令,並達到以下功能:對於接收到的數據包進行修改或針對指定端口進行轉發;對於接收到的數據包進行轉發到控制器的動作(packet-in);對於接收到來自控制器的數據包進行轉發到指定的端口(packet-out)

對於RYU來說,接收到任何一個OpenFlow消息,都會產生一個相對應事件,因此RYU應用必須實現事件管理以處理相對應的事件。

NT: simple_switch.py、 simple_switch_12.py、simple_switch_13.py、simple_switch_14.py和simple_switch_15.py分別對應OpenFlow1.0、1.2、1.4、1.5版本的。

from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.lib.packet import ether_types

# 繼承ryu.base.app_manager.RyuApp
class SimpleSwitch13(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]  # 指定OpenFlow 1.3版本

    def __init__(self, *args, **kwargs):
        super(SimpleSwitch13, self).__init__(*args, **kwargs)
        self.mac_to_port = {}  # 定義MAC地址列表
	# set_ev_cls指定事件類別得以接受消息和交換機狀態作爲參數
	# 其中事件類別名稱爲ryu.controller.ofp_event.EventOFP+<OpenFlow消息名稱>
	# 例如:在 Packet-In 消息的狀態下的事件名稱爲EventOFPPacketIn
	# 對於交換機的狀態來說,可指定以下中的一項
	# ryu.controller.handler.HANDSHAKE_DISPATCHER 交換 HELLO 消息
	# ryu.controller.handler.CONFIG_DISPATCHER	  接收SwitchFeatures消息
	# ryu.controller.handler.MAIN_DISPATCHER	  一般狀態
	# ryu.controller.handler.DEAD_DISPATCHER	  連線中斷
    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
    	# ev.msg 是用來存儲對應事件的 OpenFlow 消息類別實體
    	# msg.datapath是用來存儲OpenFlow交換機的 ryu.controller.controller.Datapath 類別所對應的實體
        datapath = ev.msg.datapath  
        ofproto = datapath.ofproto  # ofproto表示使用的OpenFlow版本所對應的ryu.ofproto.ofproto_v1_3
        parser = datapath.ofproto_parser  # 和ofproto一樣,有對應版本ryu.ofproto.ofproto_v1_3_parser
        
		# 下發table-miss流表項,讓交換機對於不會處理的數據包通過packet-in消息上交給Ryu控制器!!!
        # 匹配數據包
        # 若數據包沒有 match 任何一個普通 Flow Entry 時,則觸發 Packet-In
        match = parser.OFPMatch()  
        # 通過預留端口ofproto.OFPP_CONTROLLER,將packet-in消息發送給controller,並通過ofproto.OFPCML_NO_BUFFE指明Racket-in消息的原因是table miss
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        # 執行 add_flow() 方法以發送 Flow Mod 消息
        self.add_flow(datapath, 0, match, actions)

    def add_flow(self, datapath, priority, match, actions, buffer_id=None):
    	# 新增流表項
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
		# Apply Actions 是用來設定那些必須立即執行的 action 所使用
        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                             actions)]
        # 通過 Flow Mod 消息將 Flow Entry 新增到 Flow table 中
        if buffer_id:
            mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id,
                                    priority=priority, match=match,
                                    instructions=inst)
        else:
            mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
                                    match=match, instructions=inst)
        datapath.send_msg(mod)

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        # If you hit this you might want to increase
        # the "miss_send_length" of your switch
        if ev.msg.msg_len < ev.msg.total_len:
            self.logger.debug("packet truncated: only %s of %s bytes",
                              ev.msg.msg_len, ev.msg.total_len)
        # 爲了接收處理未知目的地的數據包,需要執行Packet-In 事件管理
        msg = ev.msg  # 每一個事件類ev中都有msg成員,用於攜帶觸發事件的數據包
        datapath = msg.datapath  # 已經格式化的msg其實就是一個packet_in報文,msg.datapath直接可以獲得packet_in報文的datapath結構
        # datapath用於描述一個交換網橋,也是和控制器通信的實體單元。
        # datapath.send_msg()函數用於發送數據到指定datapath。
        # 通過datapath.id可獲得dpid數據。
        ofproto = datapath.ofproto  # datapath.ofproto對象是一個OpenFlow協議數據結構的對象,成員包含OpenFlow協議的數據結構,如動作類型OFPP_FLOOD
        parser = datapath.ofproto_parser  # datapath.ofp_parser則是一個按照OpenFlow解析的數據結構。

		# 更新Mac地址表
        in_port = msg.match['in_port']

        pkt = packet.Packet(msg.data)
        eth = pkt.get_protocols(ethernet.ethernet)[0]

        if eth.ethertype == ether_types.ETH_TYPE_LLDP:
            # ignore lldp packet
            return
        dst = eth.dst
        src = eth.src

        dpid = datapath.id
        self.mac_to_port.setdefault(dpid, {})

        self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)

        # learn a mac address to avoid FLOOD next time.
        self.mac_to_port[dpid][src] = in_port

		# 判斷轉發的數據包的連接端口
		# 目的 MAC 位址若存在於 MAC 地址表,則判斷該連接端口號碼爲輸出。
		# 反之若不存在於 MAC 地址表則 OUTPUT action 類別的實體並生成 flooding( OFPP_FLOOD )給目的連接端口使用。
        if dst in self.mac_to_port[dpid]:
            out_port = self.mac_to_port[dpid][dst]
        else:
            out_port = ofproto.OFPP_FLOOD

        actions = [parser.OFPActionOutput(out_port)]

        # install a flow to avoid packet_in next time
        if out_port != ofproto.OFPP_FLOOD:
            match = parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
            # verify if we have a valid buffer_id, if yes avoid to send both
            # flow_mod & packet_out
            if msg.buffer_id != ofproto.OFP_NO_BUFFER:
                self.add_flow(datapath, 1, match, actions, msg.buffer_id)
                return
            else:
                self.add_flow(datapath, 1, match, actions)

		# 在 MAC 地址表中找尋目的 MAC 地址,若是有找到則發送 Packet-Out 訊息,並且轉送數據包。
        data = None
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data

        out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
                                  in_port=in_port, actions=actions, data=data)
        datapath.send_msg(out)

可結合Mininet進行測試,相關操作如下:
創建topo:

sudo mn --topo single,3 --mac --switch ovsk --controller remote -x

查看ovs-vswitchd的配置信息:

ovs-vsctl show

查看datapath的信息:

ovs-dpctl show 

設定交換機s1的Openflow版本:

ovs-vsctl set Bridge s1 protocols=OpenFlow13

查看交換機s1的流表信息:

ovs-ofctl -O OpenFlow13 dump-flows s1

主機h1網絡抓包:

tcpdump -en -i h1-eth0 

NT:實驗過程可結合ifconfig 命令用來查看和配置網絡設備!!!例如:

ifconfig eth0 up        # 啓動 <br>ifcfg etho up         # 啓動
ifconfig eth0 down      # 關閉<br>ifcfg eth0 down        # 關閉
ifconfig eth0 hw ether 00:AA:BB:CC:DD:EE                # 修改MAC地址
ifconfig eth0 reload    # 重啓
ifconfig eth0 add 33ffe:3240:800:1005::2/64              # 爲網卡eth0配置IPv6地址 
ifconfig eth0 del 33ffe:3240:800:1005::2/64              # 爲網卡eth0刪除IPv6地址
ifconfig eth0 192.168.25.166 netmask 255.255.255.0 up    # 爲網卡eth0配置IPv4地址
ifconfig eth0:ws arp									 # 啓用ARP協議
ifconfig eth0:ws -arp									 # 關閉ARP協議
ifconfig eth0 mtu 1500									 # 設置最大傳輸單元

2.2.2 ryu/app/simple_monitor_13.py實現功能爲:定期檢查網絡狀態

當網絡發生異常時,需快速找到原因,並且儘快回覆原狀。而找出網絡中的錯誤、發現真正的原因需要清楚地知道網絡地狀態,假設網絡中某個端口正處於高流量的狀態,不論是因爲它是一個不正常的狀態或是任何原因導致,變成一個由於沒有持續監控所發生的問題。

from operator import attrgetter

from ryu.app import simple_switch_13
from ryu.controller import ofp_event
from ryu.controller.handler import MAIN_DISPATCHER, DEAD_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.lib import hub


class SimpleMonitor13(simple_switch_13.SimpleSwitch13):
	# 定期的向交換機發出要求以取得需要統計的數據
    def __init__(self, *args, **kwargs):
        super(SimpleMonitor13, self).__init__(*args, **kwargs)
        self.datapaths = {}
        self.monitor_thread = hub.spawn(self._monitor)  # 建立一個綠色線程,運行監控程序
        
	# EventOFPStatureChange的信息類用來監測交換器的連線中斷,會被觸發在#Dathpath狀態改變時
	# 參數二表示 一般狀態和連線中斷狀態
    @set_ev_cls(ofp_event.EventOFPStateChange,
                [MAIN_DISPATCHER, DEAD_DISPATCHER])
    def _state_change_handler(self, ev):   
    	# 通過判斷當前狀態從監測列表添加或移除當前datapath
    	# 連線中斷狀態用於確認連線中的交換機可以持續被監控
        datapath = ev.datapath
        # 當datapath的狀態變成MAIN_DISPATCHER時,代表交換機已經被註冊,並且正處於被監視的狀態
        if ev.state == MAIN_DISPATCHER:
            if datapath.id not in self.datapaths:
                self.logger.debug('register datapath: %016x', datapath.id)
                self.datapaths[datapath.id] = datapath
        # 當datapath的狀態變成DEAD_DISPATCHER時,代表註冊狀態已經解除
        elif ev.state == DEAD_DISPATCHER:
            if datapath.id in self.datapaths:
                self.logger.debug('unregister datapath: %016x', datapath.id)
                del self.datapaths[datapath.id]

    def _monitor(self):
        while True:
        	# 不斷地註冊的向交換機發送要求取得的統計信息
            for dp in self.datapaths.values():
                self._request_stats(dp)
            # 每隔10s查詢一次當前的監視datapath名單中的各個#datapath狀況
            hub.sleep(10)
	
	
    def _request_stats(self, datapath):
        self.logger.debug('send stats request: %016x', datapath.id)
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
		# 向指定的datapath發送OFPFlowStatsRequest和OFPStatsResquest消息類實體,即對相關統計信息進行
		# 請求
		# OFPFlowStatsRequest主要用來對交換機的Flow Entry取得統計信息
		# 對於交換即發出的要求可以使用table ID、output port、cookie 值和 match 條件來限定範圍,但是以下實現的是取得所有的 Flow Entry。
        req = parser.OFPFlowStatsRequest(datapath)
        datapath.send_msg(req)
        
		# OFPPortStatsRequest 是用來取得關於交換機的端口相關信息以及統計信息。
		# 使用的使用可以指定端口號,以下使用OFPP_ANY,目的是要取得所有的端口統計信息。
        req = parser.OFPPortStatsRequest(datapath, 0, ofproto.OFPP_ANY)
        datapath.send_msg(req)
	
	# 對FlowStatsReply消息的回覆進行事件處理
    @set_ev_cls(ofp_event.EventOFPFlowStatsReply, MAIN_DISPATCHER)
    def _flow_stats_reply_handler(self, ev):
    	# body中存放了OFPFlowStats的列表,存儲了每一個Flow Entry的統計資料,並作爲OFPFlowStatsRequest的迴應
        body = ev.msg.body  

        self.logger.info('datapath         '
                         'in-port  eth-dst           '
                         'out-port packets  bytes')
        self.logger.info('---------------- '
                         '-------- ----------------- '
                         '-------- -------- --------')
        # 對各個優先級非0的流表項按接收端口和目的MAC地址進行排序後遍歷
        for stat in sorted([flow for flow in body if flow.priority == 1],
                           key=lambda flow: (flow.match['in_port'],
                                             flow.match['eth_dst'])):
            # 對交換機的datapath.id,目的MAC地址,輸出端口和包以及字節流量進行打印
            self.logger.info('%016x %8x %17s %8x %8d %8d',
                             ev.msg.datapath.id,
                             stat.match['in_port'], stat.match['eth_dst'],
                             stat.instructions[0].actions[0].port,
                             stat.packet_count, stat.byte_count)
                             
	# 對PortStatsReply消息的回覆事件進行處理
    @set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER)
    def _port_stats_reply_handler(self, ev):
        body = ev.msg.body

        self.logger.info('datapath         port     '
                         'rx-pkts  rx-bytes rx-error '
                         'tx-pkts  tx-bytes tx-error')
        self.logger.info('---------------- -------- '
                         '-------- -------- -------- '
                         '-------- -------- --------')
        # 根據端口號進行排序並遍歷
        for stat in sorted(body, key=attrgetter('port_no')):
        	# 打印交換機id,端口號和接收及發送的包的數量字節數和錯誤數
            self.logger.info('%016x %8x %8d %8d %8d %8d %8d %8d',
                             ev.msg.datapath.id, stat.port_no,
                             stat.rx_packets, stat.rx_bytes, stat.rx_errors,
                             stat.tx_packets, stat.tx_bytes, stat.tx_errors)

Ryubook中給出了OFPFlowStats的JSON格式的全部信息,下面給出OFPPortStats的JSON數據信息:
在這裏插入圖片描述

2.2.3 /ryu/app/simple_switch_rest_13.py

關於ofctl_rest.py的用法參考鏈接

RYU本身提供了一個類似WSGI的web服務器功能。藉助這個功能,我們可以創建一個REST API。基於創建的REST API,可以快速的將RYU系統與其他系統或者是瀏覽器相連接。

REST:表徵性狀態傳輸(英文:Representational State Transfer,簡稱REST)是Roy Fielding博士在2000年他的博士論文中提出來的一種軟件架構風格。REST架構風格中,資源是通過URI來描述的。對資源的操作採用了HTTP的GET,POST,PUT和DELETE方法相對應。資源的表現形式可以是json或xml。REST的架構是Client-Server架構,同時鏈接是無狀態的。所以要求在傳輸的過程中需要包含狀態信息。

代碼解析:

import json

from ryu.app import simple_switch_13
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.app.wsgi import ControllerBase
from ryu.app.wsgi import Response
from ryu.app.wsgi import route
from ryu.app.wsgi import WSGIApplication
from ryu.lib import dpid as dpid_lib

simple_switch_instance_name = 'simple_switch_api_app'
# 指定url爲如下方式,其中,{dpid} 的部分必須與 ryu/lib/dpid.py
url = '/simpleswitch/mactable/{dpid}'


class SimpleSwitchRest13(simple_switch_13.SimpleSwitch13):
	"""
		更新交換機的MAC地址表
	"""
    _CONTEXTS = {'wsgi': WSGIApplication}  # 用來建立Ryu中WSGI網頁服務器所對應的類別,因此可通過wsgi Key來取得網頁服務器的實體

    def __init__(self, *args, **kwargs):
        super(SimpleSwitchRest13, self).__init__(*args, **kwargs)
        self.switches = {}
        wsgi = kwargs['wsgi']  # 通過上一步設置的_CONTEXTS成員變量,可以通過kwargs進行實例化一個WSGIApplication。
        wsgi.register(SimpleSwitchController,
                      {simple_switch_instance_name: self})  # 使用register方法註冊該服務到controller類上。

	# 重寫父類的switch_features_handler函數
    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        super(SimpleSwitchRest13, self).switch_features_handler(ev)
        datapath = ev.msg.datapath
        self.switches[datapath.id] = datapath  # 存儲datapath到switches
        self.mac_to_port.setdefault(datapath.id, {})  # 初始化MAC地址表

    def set_mac_to_port(self, dpid, entry):
    	"""
    		該方法將MAC地址和端口註冊到指定的交換機。該方法主要被REST API的PUT方法所調用。
    	"""
    	# 獲取MAC table
        mac_table = self.mac_to_port.setdefault(dpid, {})
         # 獲取datapath,如果爲None,證明沒有該交換機
        datapath = self.switches.get(dpid)

        entry_port = entry['port']
        entry_mac = entry['mac']

        if datapath is not None:
            parser = datapath.ofproto_parser
            # # 如果entry_port不在mac_table中
            if entry_port not in mac_table.values():
				 # 下發流表
                for mac, port in mac_table.items():

                    # from known device to new device
                    actions = [parser.OFPActionOutput(entry_port)]
                    match = parser.OFPMatch(in_port=port, eth_dst=entry_mac)
                    self.add_flow(datapath, 1, match, actions)

                    # from new device to known device
                    actions = [parser.OFPActionOutput(port)]
                    match = parser.OFPMatch(in_port=entry_port, eth_dst=mac)
                    self.add_flow(datapath, 1, match, actions)
				# 添加entry_mac, entry_port到mac_table
                mac_table.update({entry_mac: entry_port})
        return mac_table


class SimpleSwitchController(ControllerBase):
	"""
		定義收到HTTP請求時所需要回應的操作
	"""
    def __init__(self, req, link, data, **config):
        super(SimpleSwitchController, self).__init__(req, link, data, **config)
        self.simple_switch_app = data[simple_switch_instance_name]
	# 藉助route裝飾器關聯方法和URL, 
	# 第一個參數:任何自定義名稱; 
	# 第二個參數:指明URL;
	# 第三個參數:指定http方法;
	# 第四個參數:指明指定位置的格式,URL(/simpleswitch/mactable/{dpid} 匹配DPID_PATTERN的描述
	# 當使用GET方式訪問到該REST API接口時,調用list_mac_table函數!!
    @route('simpleswitch', url, methods=['GET'],
           requirements={'dpid': dpid_lib.DPID_PATTERN})
    def list_mac_table(self, req, **kwargs):

        simple_switch = self.simple_switch_app
        # 獲取{dpid}
        dpid = dpid_lib.str_to_dpid(kwargs['dpid'])
		# 如果沒有dpid,返回404
        if dpid not in simple_switch.mac_to_port:
            return Response(status=404)
		# 獲取mac_table
        mac_table = simple_switch.mac_to_port.get(dpid, {})
        body = json.dumps(mac_table)
        return Response(content_type='application/json', body=body)

	# 使用PUT方式設置mac_table
    @route('simpleswitch', url, methods=['PUT'],
           requirements={'dpid': dpid_lib.DPID_PATTERN})
    def put_mac_table(self, req, **kwargs):

        simple_switch = self.simple_switch_app
        dpid = dpid_lib.str_to_dpid(kwargs['dpid'])
        try:
            new_entry = req.json if req.body else {}
        except ValueError:
            raise Response(status=400)

        if dpid not in simple_switch.mac_to_port:
            return Response(status=404)

        try:
            mac_table = simple_switch.set_mac_to_port(dpid, new_entry)
            body = json.dumps(mac_table)
            return Response(content_type='application/json', body=body)
        except Exception as e:
            return Response(status=500)

2.2.4 ryu/app/gui_topology/gui_topology.py實現功能爲:定期檢查網絡狀態

ryu4.3版本和ryu3.14版本打開gui方式有所不同,本人安裝版本爲4.3。進入gui_topology文件夾進行,並運行如下命令

ryu-manager --observe-links gui_topology.py

用mininet模擬了一個深度爲3的樹狀網絡拓撲,並連接到ryu,命令如下:

sudo mn --controller remote --mac --topo tree,2,2

連接成功後,訪問http://localhost:8080(我的ryu和mininet均運行在本機),即可看到如下界面:
在這裏插入圖片描述
物理實驗,打開一個openflow交換機,訪問http://localhost:8080,看到界面如下:
在這裏插入圖片描述

2.2.5 /ryu/app/simple_switch_lacp_13.py實現的功能爲:鏈路聚合

網絡聚合( Link Aggregation )是由 IEEE802.1AX-2008 所制定的,多條實體線路合併爲一條邏輯線路(即,多個物理端口匯聚在一起,形成一個邏輯端口),以實現出/入流量吞吐量在各成員端口的負荷分擔,交換機根據用戶配置的端口負荷分擔策略決定網絡封包從哪個成員端口發送到對端的交換機。當交換機檢測到其中一個成員端口的鏈路發生故障時,就停止在此端口上發送封包,並根據負荷分擔策略在剩下的鏈路中重新計算報文的發送端口,故障端口回覆再次擔任收發端口。

因此鏈路聚合的實現可以讓網絡中特定的裝置間通信速度提升、同時確保支援能力、提升容錯的功能。可通過LACP( Link Aggregation Control Protocol )協議動態方法實現。

from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER
from ryu.controller.handler import MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib import lacplib
from ryu.lib.dpid import str_to_dpid
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.app import simple_switch_13


class SimpleSwitchLacp13(simple_switch_13.SimpleSwitch13):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    _CONTEXTS = {'lacplib': lacplib.LacpLib}

    def __init__(self, *args, **kwargs):
        super(SimpleSwitchLacp13, self).__init__(*args, **kwargs)
        self.mac_to_port = {}
        # 通過成員變量_CONTEXTS,可以通過kwargs進行實例化一個lacplib的應用程序。
        self._lacp = kwargs['lacplib']
        # 通過LACP的add()方法來完成初始化設置,
        # 以下代碼的初始化設置爲:
        # datapath ID爲0000000000000001的OpenFlow交換機的端口1和端口2整合爲一個網絡聚合羣組
        self._lacp.add(
            dpid=str_to_dpid('0000000000000001'), ports=[1, 2])

    def del_flow(self, datapath, match):
    	"""
    		當端口有效/無效狀態變更時,被邏輯鏈路所使用的實體鏈路會因爲數據包的通過而改變狀態。
    		因此,需要刪除已經被記錄的流表項
    	"""
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        mod = parser.OFPFlowMod(datapath=datapath,
                                command=ofproto.OFPFC_DELETE,
                                out_port=ofproto.OFPP_ANY,
                                out_group=ofproto.OFPG_ANY,
                                match=match)
        datapath.send_msg(mod)
	
    @set_ev_cls(lacplib.EventPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
    	"""
    		需要由lacplib.EventPacketIn來裝飾自定義的Packet-In函數
    	"""
        msg = ev.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        in_port = msg.match['in_port']

        pkt = packet.Packet(msg.data)
        eth = pkt.get_protocols(ethernet.ethernet)[0]

        dst = eth.dst
        src = eth.src

        dpid = datapath.id
        self.mac_to_port.setdefault(dpid, {})

        self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)

        # learn a mac address to avoid FLOOD next time.
        self.mac_to_port[dpid][src] = in_port

        if dst in self.mac_to_port[dpid]:
            out_port = self.mac_to_port[dpid][dst]
        else:
            out_port = ofproto.OFPP_FLOOD

        actions = [parser.OFPActionOutput(out_port)]

        # install a flow to avoid packet_in next time
        if out_port != ofproto.OFPP_FLOOD:
            match = parser.OFPMatch(in_port=in_port, eth_dst=dst)
            self.add_flow(datapath, 1, match, actions)

        data = None
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data

        out = 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):
    	"""
    		當端口的狀態變更爲有效或者無效時,需要通過EventSlaveStateChanged事件來進行處理
    	"""
        datapath = ev.datapath
        dpid = datapath.id
        port_no = ev.port
        enabled = ev.enabled
        self.logger.info("slave state changed port: %d enabled: %s",
                         port_no, enabled)
        if dpid in self.mac_to_port:
            for mac in self.mac_to_port[dpid]:
                match = datapath.ofproto_parser.OFPMatch(eth_dst=mac)
                self.del_flow(datapath, match)
            del self.mac_to_port[dpid]
        self.mac_to_port.setdefault(dpid, {})

2.2.6 /ryu/app/simple_switch_stp_13.py實現功能爲:生成樹協議

生成樹是爲了防止網絡的拓撲中出現環路而產生廣播風暴、佔用交換機大量資源的技術。生成樹有許多種類,例如STP、RSTP、PVST+、MSTP等不同的種類。詳細介紹參考博客

下邊將介紹Ryu中的STP協議的實現

STP可以消除網絡中的環路。其基本理論依據是根據網絡拓撲構建(生成)無迴路的連通圖(就是樹),從而保證數據傳輸路徑的唯一性,避免出現環路導致報文流量的增加和循環。STP是工作在OSI第二層(Data Link Layer)的協議,通過在交換機之間傳遞特殊的消息並進行分佈式的計算,來決定在一個有環路的網絡中,某臺交換機的某個端口應該被阻塞,用這種方法來避免掉環路。

STP作用: 消除環路:通過阻斷冗餘鏈路來消除網絡中可能存在的環路;鏈路備份:當活動路徑發生故障時,激活備份鏈路,及時恢復網絡連通性。

from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib import dpid as dpid_lib
from ryu.lib import stplib
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.app import simple_switch_13


class SimpleSwitch13(simple_switch_13.SimpleSwitch13):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    _CONTEXTS = {'stplib': stplib.Stp}

    def __init__(self, *args, **kwargs):
        super(SimpleSwitch13, self).__init__(*args, **kwargs)
        self.mac_to_port = {}
        self.stp = kwargs['stplib']

        # Sample of stplib config.
        #  please refer to stplib.Stp.set_config() for details.
        config = {dpid_lib.str_to_dpid('0000000000000001'):
                  {'bridge': {'priority': 0x8000}},
                  dpid_lib.str_to_dpid('0000000000000002'):
                  {'bridge': {'priority': 0x9000}},
                  dpid_lib.str_to_dpid('0000000000000003'):
                  {'bridge': {'priority': 0xa000}}}
        self.stp.set_config(config)

    def delete_flow(self, datapath):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        for dst in self.mac_to_port[datapath.id].keys():
            match = parser.OFPMatch(eth_dst=dst)
            mod = parser.OFPFlowMod(
                datapath, command=ofproto.OFPFC_DELETE,
                out_port=ofproto.OFPP_ANY, out_group=ofproto.OFPG_ANY,
                priority=1, match=match)
            datapath.send_msg(mod)

    @set_ev_cls(stplib.EventPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        in_port = msg.match['in_port']

        pkt = packet.Packet(msg.data)
        eth = pkt.get_protocols(ethernet.ethernet)[0]

        dst = eth.dst
        src = eth.src

        dpid = datapath.id
        self.mac_to_port.setdefault(dpid, {})

        self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)

        # learn a mac address to avoid FLOOD next time.
        self.mac_to_port[dpid][src] = in_port

        if dst in self.mac_to_port[dpid]:
            out_port = self.mac_to_port[dpid][dst]
        else:
            out_port = ofproto.OFPP_FLOOD

        actions = [parser.OFPActionOutput(out_port)]

        # install a flow to avoid packet_in next time
        if out_port != ofproto.OFPP_FLOOD:
            match = parser.OFPMatch(in_port=in_port, eth_dst=dst)
            self.add_flow(datapath, 1, match, actions)

        data = None
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data

        out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
                                  in_port=in_port, actions=actions, data=data)
        datapath.send_msg(out)

    @set_ev_cls(stplib.EventTopologyChange, MAIN_DISPATCHER)
    def _topology_change_handler(self, ev):
        dp = ev.dp
        dpid_str = dpid_lib.dpid_to_str(dp.id)
        msg = 'Receive topology change event. Flush MAC table.'
        self.logger.debug("[dpid=%s] %s", dpid_str, msg)

        if dp.id in self.mac_to_port:
            self.delete_flow(dp)
            del self.mac_to_port[dp.id]

    @set_ev_cls(stplib.EventPortStateChange, MAIN_DISPATCHER)
    def _port_state_change_handler(self, ev):
        dpid_str = dpid_lib.dpid_to_str(ev.dp.id)
        of_state = {stplib.PORT_STATE_DISABLE: 'DISABLE',
                    stplib.PORT_STATE_BLOCK: 'BLOCK',
                    stplib.PORT_STATE_LISTEN: 'LISTEN',
                    stplib.PORT_STATE_LEARN: 'LEARN',
                    stplib.PORT_STATE_FORWARD: 'FORWARD'}
        self.logger.debug("[dpid=%s][port=%d] state=%s",
                          dpid_str, ev.port_no, of_state[ev.port_state])

3 RYU的運行

3.1 RYU基本操作

a、RYU使用命令

使用命令格式如下:

ryu-manager <yourapp>

如,進入ryu/app目錄,運行simple_switch_13.py

ryu-manager simple_switch_13.py

b、RYU啓動參數

ryu-manager 啓動ryu, 如果不加任何參數,則默認啓動ofphandler模塊。
ryu-manager –h 查看幫助信息
--verbose 打印詳細debug信息
--version 顯示程序的版本號並退出
--observe-links 自動下發LLDP,用於拓撲發現
--ofp-tcp-listen-port  修改ryu的openflow tcp監聽端口
…

說明:若在啓動RYU時使用了–observe-links參數,則RYU會收到非常大量的包含LLDP協議報文的PacketIn消息如果不對這一PacketIn消息進行特殊處理的話,很容易導致Ryu奔潰,無法正常工作!!!所以,爲了避免這一問題,當你計劃使用–observe-links啓動Ryu時,在你處理PacketIn消息的函數開頭,建議包含如下代碼,即可解決問題:

 if eth.ethertype == ether_types.ETH_TYPE_LLDP:
 	# ignore lldp packet
 	return

3.2 RYU運行流程

RYU的main函數在ryu/cmd/maneger.py文件中,main函數主要內容如下代碼。

  • 在main函數中,首先從CONF文件中讀取出app list。如果ryu-manager命令中不帶任何參數,則默認應用爲ofp_handler應用
    NT: ofp_handler應用主要用於處理OpenFlow消息,完成了基本的消息處理,如hello_handler,用於處理hello報文
  • 緊接着實例化一個AppManager對象,調用load_apps函數將應用加載。調用create_contexts函數創建對應的contexts
  • 然後調用instantiate_apps函數將app_list和context中的app均實例化。啓動wsgi架構,提供web應用
  • 最後將所有的應用作爲任務,作爲coroutine的task去執行,joinall使得程序必須等待所有的task都執行完成纔可以退出程序。最後調用close函數,關閉程序,釋放資源
def main(args=None, prog=None):
    _parse_user_flags()
    # 根據配置項的註冊,讀取配置文件/usr/loca/etc/ryu/ryu.conf的配置
    try:
        CONF(args=args, prog=prog,
             project='ryu', version='ryu-manager %s' % version,
             default_config_files=['/usr/local/etc/ryu/ryu.conf'])
    except cfg.ConfigFilesNotFoundError:
        CONF(args=args, prog=prog,
             project='ryu', version='ryu-manager %s' % version)

    log.init_log()  # 初始化打印log
    logger = logging.getLogger(__name__)
	# 根據配置文件的配置執行 log、pidfile
    if CONF.enable_debugger:
        msg = 'debugging is available (--enable-debugger option is turned on)'
        logger.info(msg)
    else:
        hub.patch(thread=True)

    if CONF.pid_file:
        with open(CONF.pid_file, 'w') as pid_file:
            pid_file.write(str(os.getpid()))
	# 啓動applist中的應用,若applist爲空,則啓動ofp_handler應用
    app_lists = CONF.app_lists + CONF.app
    # keep old behavior, run ofp if no application is specified.
    if not app_lists:
        app_lists = ['ryu.controller.ofp_handler']

    app_mgr = AppManager.get_instance()  # 在AppManager類中獲取實例
    app_mgr.load_apps(app_lists)  # 加載App
    contexts = app_mgr.create_contexts()  # 創建運行環境,"dpset"/"wsgi"
    services = []
    services.extend(app_mgr.instantiate_apps(**contexts))
	# 啓動App線程,App實例化
    # ryu.controller.dpset.DPSet / rest_firewall.RestFirewallAPI / ryu.controller.ofp_handler.OFPHandler
    webapp = wsgi.start_service(app_mgr)  # webapp啓動
    if webapp:
        thr = hub.spawn(webapp)
        services.append(thr)

    try:
        hub.joinall(services)  # 調用t.wait(),執行等待,wait()方法使當前線程暫停執行並釋放對象鎖標誌
    						   # 循環join,直到有異常或者外部中斷推遲
    except KeyboardInterrupt:
        logger.debug("Keyboard Interrupt received. "
                     "Closing RYU application manager...")
    finally:
        app_mgr.close()

NT: Datapath類在RYU中極爲重要,位於ryu/controller/controller.py中,每當一個datapath實體與控制器建立連接時,就會實例化一個Datapath的對象。Datapath爲OVS內核模塊,類似網橋,負責執行數據交換,也就是把從接受端口收到的數據包在流表中進行匹配,並執行匹配到的動作,一個Datapath關聯一個flow table,一個flow table包含多個條目。

3.3 RYU應用開發

ryu/base/app_manager.py文件中,實現了兩個類RyuApp和AppManager。

  • RyuApp類是RYU封裝好的APP基類,爲應用開發提供基本的模板,用戶只需要繼承該類,即可開發應用,而註冊對應的observer和handler都使用@derocator的形式,使得開發非常的簡單高效。
  • AppManager類用於Application的管理,加載應用,運行應用,消息路由等功能,是RYU應用的調度中心。

3.3.1 Event Handle

在實現RYU各項功能時,需要用到事件管理(Event Handle)。因爲對於RYU來說,接收到的任何一個OpenFlow消息都會產生一個相對應的事件。而在RYU的應用程序開發時,必須實現事件管理以處理相對應發生的事件。

Event Handle是一個擁有事件物件(Event Object)作爲參數,並且使用"ryu.controller.handler.set_ev_cls"裝飾的函數。

set_ev_cls 參數包括:指定事件類別得以接受消息、 交換機狀態,其中

  • 事件類別:名稱命名規則ryu.controller.ofp_event.EventOFP + <OpenFlow消息名稱>,例如在 Packet-In 消息的狀態下的事件名稱爲EventOFPPacketIn。OpenFlow消息在ryu\ofproto\ofproto_v1_X_parser中可以查看。詳細內容可參考RYU的API資料,或者這篇博客
  • 交換機狀態:對於交換機狀態來說,可指定以下其一
    ryu.controller.handler.HANDSHAKE_DISPATCHER 交換HELLO 消息
    ryu.controller.handler.CONFIG_DISPATCHER 接收 SwitchFeatures消息
    ryu.controller.handler.MAIN_DISPATCHER 一般狀態
    ryu.controller.handler.DEAD_DISPATCHER 連線中斷

因此,處理事件函數的標準模板如下:

@set_ev_cls(ofp_event.Event, DISPATCHER(s))
def your_function(self, ev):
...

簡單說,@set_ev_cls(ofp_event.Event, DISPATCHER(s))的含義就是,當接收DISPATCHER(s)情況的Event事件進行your_function處理。

3.3.2 源代碼其他細節說明

OpenFlow協議中的細節:Match、Instructions 和 Action 在 OpenFlow 協議中的細節參考鏈接

ofproto函數庫的使用:ofproto函數庫是用來產生和解析OpenFlow消息的函數庫。可參考鏈接

數據包處理協議:Ryu中提供了許多解析和包裝數據包的協議,比如ARP、BGP、IPV4等,可參考鏈接

of-config函數庫:of-config是用來管理OpenFlow交換機的一個通訊協議。of-config通訊協議被定義在 NETCONF(RFC 6241)標準中,可以對邏輯交換機的Port和Queue進行設定,參考鏈接

4 常見問題

4.1 ryu-manager運行報錯

當ryu-manager被多次執行,或者ryu的監聽端口6633被佔用時,會運行報錯。ryu控制器默認使用6633端口,因此應該首先查看哪個進程佔用了這個端口,執行命令sudo lsof -i :6633。若該端口被佔用,有如下兩種方案解決辦法:

  • 直接將佔用進程kill掉:sudo kill -9 pid(進程號)
  • 將ryu的端口號設爲其他不被佔用的端口,如5555端口:ryu-manager --ofp-tcp-listen-port 5555 -verbose

參考

本文部分內容參考:
https://www.cnblogs.com/zxqstrong/p/4789105.html
https://osrg.github.io/ryu-book/zh_tw/html/index.html

發佈了13 篇原創文章 · 獲贊 28 · 訪問量 6627
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章