RYU REST API 使用记录

摘要

本文使用基于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
REST编程六原则

1
图1 REST编程六原则

II. 实现步骤

2.1 基本操作实现

  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
  1. 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。
Ryu Documentation说明页节选
图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层交换机功能。说明:

  1. 实验中2未深究交换机与controller连接的端口相应int值,所以在SDN初始化过程中使用的add_flow2函数并非基于REST API(rest下add flow需要int端口号)
  2. 浏览ryu.app.ofctl_rest未发现packet out对应的rest api。故packet out使用传统非rest方式完成。
  3. 本功能实现是结合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. 实验结果以及讨论

  1. Client端
    图3给出了postwoman交互成功结果。
    PostWoman交互(以查看所有交换机为例)
    图3. PostWoman交互(以查看所有交换机为例)

对于Edge使用自带的开发者模式-网络控制台执行请求,由于本人未有web开发经验,实验中试错发现:1. 必须在地址栏输入带有控制器运行端口地址(默认应为localhost:8080 或 127.0.0.1:8080),方可成功(图4)否则失败(图5);2. 不知道为什么在请求发送后请求界面一直在loading,无法显示响应数据,而Network处可观察到响应数据。
在这里插入图片描述
图4. Edge开发者模式执行请求成功(以请求端口信息为例)

在这里插入图片描述

图5. Edge开发者模式执行请求失败(以请求端口信息为例)

  1. Server端
    无论请求成功(200)或失败(如404),ryu app运行的命令行串口均会对请求逐一给出相应反馈状态(图6)。
    ryu app反馈
    图6. ryu app反馈

  2. 基本交换机app
    执行2.2部分python脚本,在mininet窗口运行pingall,主机之间可实现相互通信,执行sh ovs-ofctl dump-flows s1也可观察到被通过REST接口成功下发的流表。由此,REST API流表下发功能实现成功(图7)。
    Mininet连通性测试
    图7. Mininet连通性测试

  3. 其他
    实验中并未对开发者模式-网络控制台请求的fetch栏目下的诸选项(图8)进行深究。希望得到指教。
    在这里插入图片描述
    图8. fetch栏目选项


  1. https://restfulapi.net/ ↩︎ ↩︎

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值