這次隔了一週才做出來,但卻對一些用法跟練習都熟了許多,最終還是要去看openflow Spec阿
這次的也比較有難度,光查API怎樣用其實就花了我不少的時間,但整體上還是有收穫的,為了怕忘掉趕緊筆記一下
作業要求
- Mininet:
- 修正 mininet script,switches 為鏈狀連接,頭尾各接一台 host,在執行時能夠加上參數指定中間 switch 數量
- hw2_ryuapp.py 應該要能直接運作在這個 hw3_net.py topo 上,若有錯誤請修正作業二
- Ryu:
- controller 對每個 switch 初始設置一條 flow entry: 當收到的封包型態為 LLDP 時發給 controller
- controller 讓 switch 每個使用中的 port 送出 LLDP 封包
- 設計一個資料結構存取 LLDP 得到的 topo
- 需要 packet in handler 取得 switch 收到 LLDP 封包中的資訊
mininet
首先我們先去解決mininet的修正,以下是最終程式碼
from mininet.log import setLogLevel,infofrom mininet.node import RemoteControllerfrom mininet.net import Mininetfrom mininet.cli import CLIfrom optparse import OptionParserdef SetParse():parser = OptionParser()parser.add_option(“-n”,” — number”,type=”int”,dest=”switch_num”,help=”write the switch number”,default=1 )return parser.parse_args()def MininetTopo(switch_num):net = Mininet()info(“Create host nodes.\n”)h1 = net.addHost(“h1”)h2 = net.addHost(“h2”)info(“Create switch node.\n”)#s1 = net.addSwitch(“s1”,failMode = ‘standalone’)#s1 = net.addSwitch(“s1”,failMode = ‘secure’,protocols = ‘OpenFlow13’)for sw in range(1,switch_num+1):name = “s”+str(sw)net.addSwitch(name,failMode = ‘secure’,protocols = ‘OpenFlow13’)info(“Create Links. \n”)for link in range(0,switch_num+1):if link is 0:net.addLink(h1,”s”+str(link+1))elif link is switch_num:net.addLink(h2,”s”+str(link))else:net.addLink(“s”+str(link),”s”+str(link+1))info(“Create controller ot switch. \n”)net.addController(controller=RemoteController,ip=’127.0.0.1',port=6633)info(“Build and start network.\n”)net.build()net.start()info(“Run the mininet CLI”)CLI(net)if __name__ == ‘__main__’:setLogLevel(‘info’)# Set Parse(options, args) = SetParse()print(options.switch_num)MininetTopo(options.switch_num)
跟作業二的差別就是它必須根據輸入的參數,拉成一條很長的switch串,像下面圖示
然後controller接在所有的switch上,
這邊要查許多有關mininet的api function,雖然我覺得官方說明的真的沒有很清楚,不過與其抱怨就不如趕快多多試試看
我利用optparse這個python的函式庫來幫助我建立command flag,在下command時我只需要加入參數 -n [number]就能增加我的switch數量,以下是定義的方式
def SetParse():
parser = OptionParser()
parser.add_option(“-n”,” — number”,type=”int”,dest=”switch_num”,help=”write the switch number”,default=1 )
return parser.parse_args()if __name__ == ‘__main__’:
setLogLevel(‘info’)
# Set Parse
(options, args) = SetParse()
print(options.switch_num)
MininetTopo(options.switch_num)
先定義好呼叫的方式,在運行main的時候輸入進來,並接著將數量往MininetTopo送過去
很多地方都個lab2重複了,這邊就挑重要的去說明,
info(“Create switch node.\n”)
…
for sw in range(1,switch_num+1):
name = “s”+str(sw)
net.addSwitch(name,failMode = ‘secure’,protocols = ‘OpenFlow13’)
這邊主要就是依據數量產生switch,並依據數字順序命名 ex, s1 ,s2 …
info(“Create Links. \n”)
for link in range(0,switch_num+1):
if link is 0:
net.addLink(h1,”s”+str(link+1))
elif link is switch_num:
net.addLink(h2,”s”+str(link))
else:
net.addLink(“s”+str(link),”s”+str(link+1))
而這裡的主要邏輯就是第一個switch連上h1,最後一個連上h2,其餘的就依序串在一起
,最後就可執行
$ sudo mn -c && sudo python sdn3_mininet.py -n 4
Create host nodes.
Create switch node.
Create Links.
Create controller ot switch.
Unable to contact the remote controller at 127.0.0.1:6633
Build and start network.
*** Configuring hosts
h1 h2
*** Starting controller
c0
*** Starting 4 switches
s1 s2 s3 s4 …
Run the mininet CLI*** Starting CLI:
mininet>
即可看到有4台switch被產生,記得如果關掉後要在產生一次要清一下紀錄 使用sudo mn -c
喔
然後這邊的要求是使用lab2的ryu app, h1要可以ping到h2,確保lab2的作業是正確的,這樣mininet的部份先告一個段落,接下來是ryu app的撰寫
Ryu App
這次的要求就是實做出利用lldp來得知網路拓樸的環境為何
我思考的步驟為,先將flow table設置好,讓lldp的封包會直接packetIn進controller,然後取得每一個switch port的資訊並記錄下來,並讓這些port都發一個lldp封包,這個封包只跳一次,在令一頭被接到後因為前面flow table的設定,會送上controller來分析,進而去做出一個拓樸
,先附上全部的程式碼
from ryu.base import app_manager
from ryu.ofproto import ofproto_v1_3
from ryu.controller.handler import set_ev_cls
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.lib.packet import ether_types,lldp,packet,ethernetclass MySwitch(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
link = []def __init__(self, *args,**kwargs):
super(MySwitch,self).__init__(*args,**kwargs)
self.mac_to_port = {} # Mac address is defined
@set_ev_cls(ofp_event.EventOFPSwitchFeatures,CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
datapath = ev.msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser#set if packet is lldp, send to controller
actions = [parser.OFPActionOutput(port=ofproto.OFPP_CONTROLLER,
max_len=ofproto.OFPCML_NO_BUFFER)]
inst = [parser.OFPInstructionActions(type_=ofproto.OFPIT_APPLY_ACTIONS,actions=actions)]
match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_LLDP)
mod = parser.OFPFlowMod(datapath=datapath,
priority=1,
match=match,
instructions=inst)
datapath.send_msg(mod)self.send_port_desc_stats_request(datapath)# send the requestdef add_flow(self, datapath, priority, match, actions):
ofproto = datapath.ofproto
parser = datapath.ofproto_parserinst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,actions)]mod = parser.OFPFlowMod(datapath=datapath,priority=priority,match=match,instructions=inst)
datapath.send_msg(mod)def send_port_desc_stats_request(self, datapath):
ofproto = datapath.ofproto
ofp_parser = datapath.ofproto_parser
req = ofp_parser.OFPPortDescStatsRequest(datapath, 0)
datapath.send_msg(req)# Send the lldp packet
def send_lldp_packet(self, datapath, port, hw_addr, ttl):
ofproto = datapath.ofproto
ofp_parser = datapath.ofproto_parserpkt = packet.Packet()
pkt.add_protocol(ethernet.ethernet(ethertype=ether_types.ETH_TYPE_LLDP,src=hw_addr ,dst=lldp.LLDP_MAC_NEAREST_BRIDGE))chassis_id = lldp.ChassisID(subtype=lldp.ChassisID.SUB_LOCALLY_ASSIGNED, chassis_id=str(datapath.id))
port_id = lldp.PortID(subtype=lldp.PortID.SUB_LOCALLY_ASSIGNED, port_id=str(port))
ttl = lldp.TTL(ttl=1)
end = lldp.End()
tlvs = (chassis_id,port_id,ttl,end)
pkt.add_protocol(lldp.lldp(tlvs))
pkt.serialize()
self.logger.info(“packet-out %s” % pkt)
data = pkt.data
actions = [ofp_parser.OFPActionOutput(port=port)]
out = ofp_parser.OFPPacketOut(datapath=datapath,
buffer_id=ofproto.OFP_NO_BUFFER,
in_port=ofproto.OFPP_CONTROLLER,
actions=actions,
data=data)
datapath.send_msg(out)@set_ev_cls(ofp_event.EventOFPPortDescStatsReply, MAIN_DISPATCHER)
def port_desc_stats_reply_handler(self, ev):
datapath = ev.msg.datapath
ofproto = datapath.ofproto
ofp_parser = datapath.ofproto_parser
ports = []
for stat in ev.msg.body:
if stat.port_no <=ofproto.OFPP_MAX:
ports.append({‘port_no’:stat.port_no,’hw_addr’:stat.hw_addr})
for no in ports:
in_port = no[‘port_no’]
match = ofp_parser.OFPMatch(in_port = in_port)
for other_no in ports:
if other_no[‘port_no’] != in_port:
out_port = other_no[‘port_no’]
self.send_lldp_packet(datapath,no[‘port_no’],no[‘hw_addr’],10)
actions = [ofp_parser.OFPActionOutput(out_port)]
self.add_flow(datapath, 1, match, actions)@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def packet_in_handler(self, ev):
msg = ev.msg
datapath = msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parserpkt = packet.Packet(data=msg.data)
dpid = datapath.id # switch id which send the packetin
in_port = msg.match[‘in_port’]pkt_ethernet = pkt.get_protocol(ethernet.ethernet)
pkt_lldp = pkt.get_protocol(lldp.lldp)
if not pkt_ethernet:
return
#print(pkt_lldp)
if pkt_lldp:
self.handle_lldp(dpid,in_port,pkt_lldp.tlvs[0].chassis_id,pkt_lldp.tlvs[1].port_id)#self.logger.info(“packet-in %s” % (pkt,))# Link two switch
def switch_link(self,s_a,s_b):
return s_a + ‘←->’ + s_b
def handle_lldp(self,dpid,in_port,lldp_dpid,lldp_in_port):
switch_a = ‘switch’+str(dpid)+’, port’+str(in_port)
switch_b = ‘switch’+lldp_dpid+’, port’+lldp_in_port
link = self.switch_link(switch_a,switch_b)# Check the switch link is existed
if not any(self.switch_link(switch_b,switch_a) == search for search in self.link):
self.link.append(link)print(self.link)
首先我們一樣先來看一開始的部份
class MySwitch(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
link = []def __init__(self, *args,**kwargs):
super(MySwitch,self).__init__(*args,**kwargs)
self.mac_to_port = {} # Mac address is defined
這邊一樣我們採用的是OpenFlow1.3版,之已有一個mac_to_port原本是想要紀錄下每個switch的mac,不過後來發現用不到,所以可以忽略,這邊都跟sdn lab2一樣,所以就不再做說明
@set_ev_cls(ofp_event.EventOFPSwitchFeatures,CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
datapath = ev.msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser#set if packet is lldp, send to controller
actions = [parser.OFPActionOutput(port=ofproto.OFPP_CONTROLLER,
max_len=ofproto.OFPCML_NO_BUFFER)]
inst = [parser.OFPInstructionActions(type_=ofproto.OFPIT_APPLY_ACTIONS,actions=actions)]
match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_LLDP)
mod = parser.OFPFlowMod(datapath=datapath,
priority=1,
match=match,
instructions=inst)
datapath.send_msg(mod)self.send_port_desc_stats_request(datapath)# send the request
這邊的主要說明就是告訴switch將lldp的封包送到controoler,match ethernet_type為lldp,然後送一個port 資訊要求給各個switch
def send_port_desc_stats_request(self, datapath):
ofproto = datapath.ofproto
ofp_parser = datapath.ofproto_parser
req = ofp_parser.OFPPortDescStatsRequest(datapath, 0)
datapath.send_msg(req)
這個為向port提供資訊的請求,主要上跟sdn lab2幾乎一樣,這邊也就不詳細說明程式碼的內容了,
接下來我們看接收到switch後的封包處理要怎麼完成
```
@set_ev_cls(ofp_event.EventOFPPortDescStatsReply, MAIN_DISPATCHER)
def port_desc_stats_reply_handler(self, ev):
datapath = ev.msg.datapath
ofproto = datapath.ofproto
ofp_parser = datapath.ofproto_parser
ports = []
for stat in ev.msg.body:
if stat.port_no <=ofproto.OFPP_MAX:
ports.append({‘port_no’:stat.port_no,’hw_addr’:stat.hw_addr})
for no in ports:
in_port = no[‘port_no’]
match = ofp_parser.OFPMatch(in_port = in_port)
for other_no in ports:
if other_no[‘port_no’] != in_port:
out_port = other_no[‘port_no’]
self.send_lldp_packet(datapath,no[‘port_no’],no[‘hw_addr’],10)
actions = [ofp_parser.OFPActionOutput(out_port)]
self.add_flow(datapath, 1, match, actions)
上面的decorator,就是說明收到EventOFPPortDescStatsReply回應,接下來我們要描述要怎樣處理這個封包
這邊跟lab2也基本上都一樣,唯一不一樣的是可以注意到
self.send_lldp_packet(datapath,no[‘port_no’],no[‘hw_addr’],10)
我們將其中收到的switch port的資訊,包裝進lldp並送給此port,並並設好port1轉好其餘的封包到port2(這邊是lab2的內容)
add_flow也是lab2的內容之一,這邊我們也不多做介紹了
好我們接下來終於來到這次的重頭戲 lldp封包,首先這是一個有歷史原因的protocol,我也是花了一點時間去了解它,起初,為了收集網路設備資訊,cisco開發了CDP(Cisco Discovery Protoco),如此以來,透過這個protocol,就能讓設備交換資訊,甚至可以掌握整個網路的拓樸資訊
詳情可以參考[使用CDP探索協定重建Cisco網路拓樸](http://www.netadmin.com.tw/article_content.aspx?sn=1111090010)
可是有個問題就發生了,若在這些裝置中,有些裝置並不屬於cisco開發的裝置,那麼這個CDP便會有一個中斷,而且只有cisco的設備能用,這樣是不是真的很困擾
所以lldp(Link Layer Discovery Protocol,鏈路層發現協議)誕生了,他要處理的事情幾乎跟CDP一樣,不同的是他是由IEEE802.1共同訂製的一個protocol,所以所有符合lldp的裝置都能透過這協議互相溝通
有關lldp相關的結構[在此](http://ryu.readthedocs.io/en/latest/library_packet_ref/packet_lldp.html)
看完了這些背景,我們超為整理一下在lldp的TLV(type, length, value的縮寫),tlv是lldp中主要的格式,
| TLV類型 | TLV資料部份長度 | TLV資料部份 |
| — — — — | — — — — | — — — — |
| 7 bits | 9 bits | 0–511 byte |
然後有幾種TLV type是規定必須存在的 Chassis ID,Port ID,Time To Live,End Of LLDPDU
# Send the lldp packet
def send_lldp_packet(self, datapath, port, hw_addr, ttl):
ofproto = datapath.ofproto
ofp_parser = datapath.ofproto_parserpkt = packet.Packet()
pkt.add_protocol(ethernet.ethernet(ethertype=ether_types.ETH_TYPE_LLDP,src=hw_addr ,dst=lldp.LLDP_MAC_NEAREST_BRIDGE))chassis_id = lldp.ChassisID(subtype=lldp.ChassisID.SUB_LOCALLY_ASSIGNED, chassis_id=str(datapath.id))
port_id = lldp.PortID(subtype=lldp.PortID.SUB_LOCALLY_ASSIGNED, port_id=str(port))
ttl = lldp.TTL(ttl=30)
end = lldp.End()
tlvs = (chassis_id,port_id,ttl,end)
pkt.add_protocol(lldp.lldp(tlvs))
pkt.serialize()
self.logger.info(“packet-out %s” % pkt)
data = pkt.data
actions = [ofp_parser.OFPActionOutput(port=port)]
out = ofp_parser.OFPPacketOut(datapath=datapath,
buffer_id=ofproto.OFP_NO_BUFFER,
in_port=ofproto.OFPP_CONTROLLER,
actions=actions,
data=data)
datapath.send_msg(out)
這邊我們就看到先產生一個packet然後加上ethernet type為lldp,一這要怎找?其實我卡這邊卡蠻久的,翻遍了整個doc就是看不到這項,原來是藏到ether_type這邊了,平時不讀書就是這樣XD
接著 我們利用ryu lldp函式去幫我們定義ChassisId
這邊我們讓chassis_id為此switch的id,
chassis_id = lldp.ChassisID(subtype=lldp.ChassisID.SUB_LOCALLY_ASSIGNED, chassis_id=str(datapath.id))
這邊的port_id為這個要送出lldp封包port的id
port_id = lldp.PortID(subtype=lldp.PortID.SUB_LOCALLY_ASSIGNED, port_id=str(port))
ttl配置
ttl = lldp.TTL(ttl=1)
最後就是End Of LLDPDU
end = lldp.End()
然後包起來後加到packet裡,並送出就完成了lldp封包的製作與發送
最後,我們要來看到收到後如何去分析裡面的資訊以及描述出網路拓樸
這邊是controller收到packet的時候,PacketIn的函式
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def packet_in_handler(self, ev):
msg = ev.msg
datapath = msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parserpkt = packet.Packet(data=msg.data)
dpid = datapath.id # switch id which send the packetin
in_port = msg.match[‘in_port’]pkt_ethernet = pkt.get_protocol(ethernet.ethernet)
pkt_lldp = pkt.get_protocol(lldp.lldp)
if not pkt_ethernet:
return
#print(pkt_lldp)
if pkt_lldp:
self.handle_lldp(dpid,in_port,pkt_lldp.tlvs[0].chassis_id,pkt_lldp.tlvs[1].port_id)#self.logger.info(“packet-in %s” % (pkt,))
這邊主要的內容就是去抓這是來自哪個switch與port,然後再把它裡面的封包資訊給解出來,如果是lldp封包,我們就送到handle_lldp去幫助我們完成最後網路拓樸的部份
我們將switchID,PortID 與 lldp封包裡面的 switchID,PortID一起送交給handle處理
self.handle_lldp(dpid,in_port,pkt_lldp.tlvs[0].chassis_id,pkt_lldp.tlvs[1].port_id)
簡單來說我們就是將這兩邊的資訊連結起來,若有重複(正反邊也是重複的一種,所以我們也要去避免)我們就不會在加入
# Link two switch
def switch_link(self,s_a,s_b):
return s_a + ‘←->’ + s_b
def handle_lldp(self,dpid,in_port,lldp_dpid,lldp_in_port):
switch_a = ‘switch’+str(dpid)+’, port’+str(in_port)
switch_b = ‘switch’+lldp_dpid+’, port’+lldp_in_port
link = self.switch_link(switch_a,switch_b)# Check the switch link is existed
if not any(self.switch_link(switch_b,switch_a) == search for search in self.link):
self.link.append(link)print(self.link)
到這邊就大功告成了,雖然描述起來好像沒有那麼困難,但是這些規則以及封包製作方式都花了不少時間去查,這次的作業真的蠻有趣也有難度
以上的程式碼都放在[github](https://github.com/kweisamx/ryu_train/tree/master/sdn3),若有任何錯誤或是有疑問的部份也都歡迎留言或是私訊我,謝謝