資訊人筆記

Work hard, Have fun, Make history!

使用者工具

網站工具


ccis_lab:sdn:hw4

SDN:Lab 練習四

0x01 作業要求

  • 學習如何撰寫與使用 Ryu RESTful API
  • 使用 RESTful API 請求取得 SDN:Lab 作業三 Topo

0x01 Ryu Application

hw4_ryuapp.py
#!/usr/bin/env python
# -*- coding: utf8 -*-
# 2016.08.04 kshuang
 
 
from ryu.base import app_manager
from ryu.ofproto import ofproto_v1_3
from ryu.controller.handler import set_ev_cls
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller import ofp_event
from ryu.ofproto import ofproto_v1_3_parser
from ryu.lib.packet import ethernet
from ryu.lib.packet import ether_types
from ryu.lib.packet import lldp
from ryu.lib.packet import packet
from ryu import utils
from ryu.app.wsgi import ControllerBase, WSGIApplication, route
from ryu.lib import dpid as dpid_lib
from webob import Response
import json
 
myryu_instance_name = 'MyRyu'
url = '/lldp/{dpid}'
 
class MyRyu(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    normal_port = []
    lldp_topo = {}
    _CONTEXTS = {'wsgi': WSGIApplication}
 
    def __init__(self, *args, **kwargs):
        super(MyRyu, self).__init__(*args, **kwargs)
        wsgi = kwargs['wsgi']
        wsgi.register(MyRyuAPI, {myryu_instance_name: self})
 
    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        self.send_port_stats_request(datapath)
 
    def send_port_stats_request(self, datapath):
        ofp = datapath.ofproto
        ofp_parser = datapath.ofproto_parser
        req = ofp_parser.OFPPortDescStatsRequest(datapath, 0, ofp.OFPP_ANY)
        datapath.send_msg(req)
 
    @set_ev_cls(ofp_event.EventOFPPortDescStatsReply, MAIN_DISPATCHER)
    def port_stats_reply_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
 
        # LLDP packet to controller
        match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_LLDP)
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER)]
        self.add_flow(datapath, 0, match, actions)
 
        for stat in ev.msg.body:
            if stat.port_no < ofproto.OFPP_MAX:
                self.normal_port.append(stat.port_no)
                self.send_lldp_packet(datapath, stat.port_no, stat.hw_addr)
 
        if len(self.normal_port) == 2:
            # port B to port A
            match = parser.OFPMatch(in_port=self.normal_port[0])
            actions = [parser.OFPActionOutput(self.normal_port[1])]
            self.add_flow(datapath, 0, match, actions)
 
            # port A to port B
            match = parser.OFPMatch(in_port=self.normal_port[1])
            actions = [parser.OFPActionOutput(self.normal_port[0])]
            self.add_flow(datapath, 0, match, actions)
 
        # clear port record after add flow entry
        self.normal_port = []
 
    def add_flow(self, datapath, priority, match, actions):
        ofp = datapath.ofproto
        parser = datapath.ofproto_parser
        inst = [parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
        mod = parser.OFPFlowMod(datapath=datapath, priority=priority, command=ofp.OFPFC_ADD, match=match, instructions=inst)
        datapath.send_msg(mod)
 
    def send_lldp_packet(self, datapath, port_no, hw_addr):
        ofp = datapath.ofproto
        pkt = packet.Packet()
        pkt.add_protocol(ethernet.ethernet(ethertype=ether_types.ETH_TYPE_LLDP, src=hw_addr, dst=lldp.LLDP_MAC_NEAREST_BRIDGE))
 
        tlv_chassis_id = lldp.ChassisID(subtype=lldp.ChassisID.SUB_LOCALLY_ASSIGNED, chassis_id=str(datapath.id))
        tlv_port_id = lldp.PortID(subtype=lldp.PortID.SUB_LOCALLY_ASSIGNED, port_id=str(port_no))
        tlv_ttl = lldp.TTL(ttl=10)
        tlv_end = lldp.End()
        tlvs = (tlv_chassis_id, tlv_port_id, tlv_ttl, tlv_end)
        pkt.add_protocol(lldp.lldp(tlvs))
 
        pkt.serialize()
 
        data = pkt.data
        parser = datapath.ofproto_parser
        actions = [parser.OFPActionOutput(port=port_no)]
        out = parser.OFPPacketOut(datapath=datapath, buffer_id=ofp.OFP_NO_BUFFER, in_port=ofp.OFPP_CONTROLLER, actions=actions, data=data)
        datapath.send_msg(out)
 
    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def packet_in_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        port = msg.match['in_port']
        pkt = packet.Packet(data=msg.data)
        pkt_ethernet = pkt.get_protocol(ethernet.ethernet)
 
        if not pkt_ethernet:
            return
 
        pkt_lldp = pkt.get_protocol(lldp.lldp)
        if pkt_lldp:
            self.handle_lldp(datapath, port, pkt_ethernet, pkt_lldp)
 
    def handle_lldp(self, datapath, port, pkt_ethernet, pkt_lldp):
        self.lldp_topo.setdefault(int(datapath.id), {})
        port_connect = {}
        self.lldp_topo[datapath.id].setdefault(int(port), [int(pkt_lldp.tlvs[0].chassis_id), int(pkt_lldp.tlvs[1].port_id)])
        print self.lldp_topo.get(datapath.id, {})
 
    @set_ev_cls(ofp_event.EventOFPErrorMsg, MAIN_DISPATCHER)
    def error_msg_handler(self, ev):
        msg = ev.msg
        self.logger.debug('OFPErrorMsg received: type=0x%02x code=0x%02x message=%s', msg.type, msg.code, utils.hex_array(msg.data))
 
 
class MyRyuAPI(ControllerBase):
    def __init__(self, req, link, data, **config):
        super(MyRyuAPI, self).__init__(req, link, data, **config)
        self.myryu_instance = data[myryu_instance_name]
 
    @route('simplelldp', url, methods=['GET'])
    def list_switch_connect(self, req, **kwargs):
        myryu = self.myryu_instance
        dpid = int(kwargs['dpid'])
 
        if dpid == 0:
            lldp_table = myryu.lldp_topo.items()
            body = json.dumps(lldp_table, indent=4, sort_keys=True) + '\n'
            return Response(content_type='application/json', body=body)
 
        if dpid not in myryu.lldp_topo:
            return Response(status=404, body='switch not found\n')
 
        lldp_table = myryu.lldp_topo.get(dpid)
        body = json.dumps(lldp_table, indent=4, sort_keys=True) + '\n'
        return Response(content_type='application/json', body=body)

本次內容多基於 SDN:Lab 作業三 在新增內容,故僅探討新增部分

透過 wsgi 註冊 API 處理類別

myryu_instance_name = 'MyRyu'
url = '/lldp/{dpid}'
 
class MyRyu(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    normal_port = []
    lldp_topo = {}
    _CONTEXTS = {'wsgi': WSGIApplication}
 
    def __init__(self, *args, **kwargs):
        super(MyRyu, self).__init__(*args, **kwargs)
        wsgi = kwargs['wsgi']
        wsgi.register(MyRyuAPI, {myryu_instance_name: self})

首先我們在 _CONTEXTS 中加入了 'wsgi' 和 WSGIApplication 鍵值對

在 __init__ 時可以透過 wsgi 這個 key 去 kwargs 中取得 WSGIApplication 這個物件參照

接著將 MyRyuAPI 註冊進 WSGIApplication,MyRyuAPI 是我們自己撰寫的類別,這在下面會探討

register 這個 function 可以參考 source code

class WSGIApplication(object):
    def __init__(self, **config):

...

def register(self, controller, data=None):
        def _target_filter(attr):
            if not inspect.ismethod(attr) and not inspect.isfunction(attr):
                return False
            if not hasattr(attr, 'routing_info'):
                return False
            return True

這邊第二個參數是 data, 我們以 dict 把 MyRyu 本身的物件參照對應 myryu_instance_name 鍵傳入,讓後面 MyRyuAPI 可透過這個物件參照取得前面 MyRyu 中的資料


MyRyuAPI

class MyRyuAPI(ControllerBase):
    def __init__(self, req, link, data, **config):
        super(MyRyuAPI, self).__init__(req, link, data, **config)
        self.myryu_instance = data[myryu_instance_name]
 
    @route('simplelldp', url, methods=['GET'])
    def list_switch_connect(self, req, **kwargs):
        myryu = self.myryu_instance
        dpid = int(kwargs['dpid'])
 
        if dpid == 0:
            lldp_table = myryu.lldp_topo.items()
            body = json.dumps(lldp_table, indent=4, sort_keys=True) + '\n'
            return Response(content_type='application/json', body=body)
 
        if dpid not in myryu.lldp_topo:
            return Response(status=404, body='switch not found\n')
 
        lldp_table = myryu.lldp_topo.get(dpid)
        body = json.dumps(lldp_table, indent=4, sort_keys=True) + '\n'
        return Response(content_type='application/json', body=body)

這個類別負責處理 API 的請求與回應,繼承 ControllerBase

self.myryu_instance = data[myryu_instance_name] 取得前面 MyRyu 物件參照

@route('simplelldp', url, methods=['GET']) 這邊參數可以參考 source code

def route(name, path, methods=None, requirements=None):
    def _route(controller_method):
        controller_method.routing_info = {
            'name': name,
            'path': path,
            'methods': methods,
            'requirements': requirements,
        }
        return controller_method
    return _route

  • 第一個參數為字串,可自訂
  • 第二個參數為 API request path, 我們使用前面寫的 url = '/lldp/{dpid}',其中 dpid 是可變的
  • 第三個參數是請求方法,這邊為 GET
  • 第四個參數可以用於指定可動變數的形式,這邊我們不使用

在 list_switch_connect function 中,我們首先取出 request URL 中的 dpid

因為 dpid 是從 1 開始的,我這邊把 0 設為請求全部

若 dpid 不在我們的資料中則回應 404 switch not found

若 dpid 存在我們的資料中則回應該 switch 經過 LLDP 探索後,自身連接其他 switch 的情況


0x02 測試

Mininet 和 RyuApp 執行後,可以透過 curl http://your.ryu.server.ip:8080/lldp/{dpid}

$ curl http://your.ryu.server.ip:8080/lldp/{dpid}

正確運作的話應該就會看到 RESTful API 的回傳資料了


0x03 參考資料

ccis_lab/sdn/hw4.txt · 上一次變更: 127.0.0.1