摘要
本文使用基于RYU REST API对Mininet仿真SDN网络环境进行功能实现。实现方法包括python脚本、命令行交互(curl)以及浏览器。其中,python的web交互使用urllib3库实现。
0. 引言
初次接触REST风格编程,本文重点在于python脚本的RYU REST API交互。经浏览,并未发现当前有相似功能具体实现的介绍,部分已有博文记录内容亦无法实现。本文写作目的出于学习以及空白填补。
I. REST API简介
表示性状态转移(REpresentational State Transfer, REST)作为一种编程风格/架构,最初于2000年由Roy Fieling在博士论文中提出。REST的定义并没有严格规则的协议,而可由六种设计原则作为特征:统一化接口,用户-服务器模式,缓存性,分层性,无状态性以及需求性编程。所有服务、请求对象等内容均视作“(网络)资源”。其中,需求性编程为非必须的。具体介绍可参考1。
1
图1 REST编程六原则
II. 实现步骤
2.1 基本操作实现
- SDN环境启动
a. 启动ryu rest api 服务:
ryu-manager 你的ryucontroller.py ofctl_rest.py rest_topology.py --obvserve-links --verbose
上述启动三个ryu app。第一个为自定义应用,第二、三个为辅助rest功能的app。
(理论上来说想用什么功能启动什么相关rest api 辅助app即可。但在本次仿真中发现,即使不启用rest_topology.py也可成功读取交换机、端口等状态内容)
b. 启动任意Mininet拓扑(本文选择树形拓扑):
mn --controller=remote --topo=tree, depth=2,fanout=2
- REST API交互
a. 自定义python ryu rest app编写方式:使用urllib3完成。
如果使用requests库执行,会报错说递归调用次数超过最大限制。(暂未深究具体原因,挖个坑,可能是因为重复导入?希望得到指教)urllib3执行web请求服务格式如下:
import urllib3
import json
url = "你的url"
http = urllib3.PoolManager()
flow_data = {ryu controller指令操作字典}
flow_data_js = json.dumps(flow_data) # 将字典转化为json数据格式
# 其中,'你的指令'可以为:'POST'/ 'GET'/'PUT'等等
res = http.request('你的指令', url, body=flow_data_js)
print(res)
具体命令执行细节参考RYU documentation, Built-in Ryu applications, ryu.app.ofctl_rest案例即可。注意,字典格式的指令编写完成后务必转化为json。
图2. Ryu Documentation说明页节选
b. 插件/命令行交互
使用超级用户模式(sudo)在命令行执行curl指令将失败,普通用户模式可成功
详细请参考https://cloud.tencent.com/developer/ask/sof/214426/answer/101862604
firefox restclient、Edge PostWoman或Edge开发者模式下的网络控制台都可完成;或者在命令行使用 curl -X 你的指令 (-i) url (-d MessageBody) 。其中,你的指令 以及 MessageBody与前文python script 的 http.request中内容一样。
2.2 简单2层交换机实现
使用python脚本利用RYU REST API执行流表下发功能,实现SDN简单2层交换机功能。说明:
- 实验中2未深究交换机与controller连接的端口相应int值,所以在SDN初始化过程中使用的add_flow2函数并非基于REST API(rest下add flow需要int端口号)
- 浏览ryu.app.ofctl_rest未发现packet out对应的rest api。故packet out使用传统非rest方式完成。
- 本功能实现是结合ryu/ryu/app/simple_switch_13.py 基于样例ryu/ryu/app/simple_switch_rest_13.py基础上修改完成的。实际上所做的直接改动就是把传统流表下发的相关部分修改成了rest风格语句(例如actions和match)。
一个广在博客间流传的python-ryu rest交互代码是错误的。错误内容包括但不限于:使用了过时的urllib2、功能语句调用有误(未转为json格式)及其他细节等
# Copyright (C) 2016 Nippon Telegraph and Telephone Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# launch in cmd with:
# ryu-manager learning/rest_13.py /home/james/Downloads/ryu/ryu/app/ofctl_rest.py /home/james/Downloads/ryu/ryu/app/rest_topology.py --observe-links
import json
from ryu.app import simple_switch_13
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.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
from ryu.lib.packet import packet, ethernet, ether_types
from ryu.ofproto import ofproto_v1_3
from ryu.base import app_manager
# import requests
import urllib3
simple_switch_instance_name = 'simple_switch_api_app'
url = '/simpleswitch/mactable/{dpid}'
# simple_switch_13.SimpleSwitch13
class SimpleSwitchRest13(app_manager.RyuApp): # 不要像样例一样直接继承。不然流表下发功能就还是用的原来的传统方式。
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
_CONTEXTS = {'wsgi': WSGIApplication}
def __init__(self, *args, **kwargs):
super(SimpleSwitchRest13, self).__init__(*args, **kwargs)
self.switches = {}
self.mac_to_port = {}
wsgi = kwargs['wsgi']
wsgi.register(SimpleSwitchController,
{simple_switch_instance_name: self})
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
print("config_dispatcher")
datapath = ev.msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
# install table-miss flow entry
match = parser.OFPMatch()
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
self.add_flow2(datapath, 0, match, actions)
#super(SimpleSwitchRest13, self).switch_features_handler(ev)
self.switches[datapath.id] = datapath
self.mac_to_port.setdefault(datapath.id, {})
def add_flow2(self, datapath, priority, match, actions, buffer_id=None):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
actions)]
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)
def add_flow(self, datapath, priority, match, actions, buffer_id=None):
http = urllib3.PoolManager()
url_add_flow = r"http://127.0.0.1:8080/stats/flowentry/add"
dpid = datapath.id
flow_data = {'dpid':dpid,'priority':priority,'match':match,'actions':actions}
print(flow_data)
flow_data_js = json.dumps(flow_data)
print(flow_data_js)
# res = requests.post(url_add_flow, flow_data)
res = http.request('POST', url_add_flow, body=flow_data_js)
print(res)
return res
@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
# print("aaaa")
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)
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]
if eth.ethertype == ether_types.ETH_TYPE_LLDP:
# ignore lldp packet
return
dst = eth.dst
src = eth.src
dpid = format(datapath.id, "d").zfill(16)
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
# just change relative stuffs will be fine
# actions = [parser.OFPActionOutput(out_port)]
actions = [{"type":"OUTPUT", "port":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)
match = {"in_port":in_port, "eth_dst":dst, "eth_src":src} # 需要的match匹配关键词像这样如假包换填进去即可。
# 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)
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=[parser.OFPActionOutput(out_port)], data=data)
datapath.send_msg(out)
# 这一部分是示例自带的,无需改动。
class SimpleSwitchController(ControllerBase):
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('simpleswitch', url, methods=['GET'],
requirements={'dpid': dpid_lib.DPID_PATTERN})
def list_mac_table(self, req, **kwargs):
simple_switch = self.simple_switch_app
dpid = kwargs['dpid']
if dpid not in simple_switch.mac_to_port:
return Response(status=404)
mac_table = simple_switch.mac_to_port.get(dpid, {})
body = json.dumps(mac_table)
return Response(content_type='application/json', text=body)
@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 = 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', text=body)
except Exception as e:
return Response(status=500)
III. 实验结果以及讨论
- Client端
图3给出了postwoman交互成功结果。
图3. PostWoman交互(以查看所有交换机为例)
对于Edge使用自带的开发者模式-网络控制台执行请求,由于本人未有web开发经验,实验中试错发现:1. 必须在地址栏输入带有控制器运行端口地址(默认应为localhost:8080 或 127.0.0.1:8080),方可成功(图4)否则失败(图5);2. 不知道为什么在请求发送后请求界面一直在loading,无法显示响应数据,而Network处可观察到响应数据。
图4. Edge开发者模式执行请求成功(以请求端口信息为例)
图5. Edge开发者模式执行请求失败(以请求端口信息为例)
-
Server端
无论请求成功(200)或失败(如404),ryu app运行的命令行串口均会对请求逐一给出相应反馈状态(图6)。
图6. ryu app反馈 -
基本交换机app
执行2.2部分python脚本,在mininet窗口运行pingall
,主机之间可实现相互通信,执行sh ovs-ofctl dump-flows s1
也可观察到被通过REST接口成功下发的流表。由此,REST API流表下发功能实现成功(图7)。
图7. Mininet连通性测试 -
其他
实验中并未对开发者模式-网络控制台请求的fetch栏目下的诸选项(图8)进行深究。希望得到指教。
图8. fetch栏目选项