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 的回傳資料了