生动的SDN基础内容介绍(五)--SDN北向协议/接口和意图驱动
- 背景
- 北向协议
- REST API
- REST API在Ryu中的实现
- 意图驱动
- 总结
背景
经过前几章的讲解,现在回过头来再看看SDN的架构
自底向上看一遍:
1、网络设备就是交换机,也就是上一章提到的OVS。
2、南向协议就是OpenFlow协议。
3、SDN控制器有很多种,我们之前关注的是Ryu。
现在就剩北向协议和应用平面了。
这章会关注北向协议和意图驱动,下章会以网络测量为例子说一下应用平面。
北向协议
北向协议/接口的定义:
SDN北向接口是通过控制器向上层业务应用开放的接口,其目标是使得业务应用能够便利地调用底层的网络资源和能力。
通过北向接口,网络业务的开发者能以软件编程的形式调用各种网络资源;同时上层的网络资源管理系统可以通过控制器的北向接口全局把控整个网网络的资源状态,并对资源进行统一调度。
因为北向接口是直接为业务应用服务的,因此其设计需要密切联系业务应用需求,具有多样化的特征。
同时,北向接口的设计是否合理、便捷,以便能被业务应用广泛调用,会直接影响到SDN控制器厂商的市场前景。
打住!说了这么多其实可以总结为一句话:
北向协议就是用户与控制器沟通的渠道。
那怎么理解呢:
用户说我现在想删除一个交换机里面的一个流表项,具体的删除逻辑是控制器干的事情,用户只需要告诉控制器他想删掉交换机1的匹配域为“目的ip:10.0.0.1”的那个流表项。
好,用户只表达了三个需求:
1、交换机1
2、匹配域为“目的ip:10.0.0.1”
3、删除这个流表项
具体怎么干用户才懒得去弄,直接让控制器去决定怎么删除,具体的删除逻辑是什么样子的。
按前几章的比喻其实就是,甲方给领导提了三个要求,领导想怎么干就怎么干,只要最后完成了这三个要求就可以。
也就是说北向协议将底层的具体实现抽象化了。
北向协议有很多,也没有规定说是一定要用哪个,因为Ryu用的是REST API,而且REST API在当今的软件开发中也是一种很主流的风格,因此这里我只介绍一下REST API。
REST API
REST API不是一个什么新的技术,它只是一种风格,把之前章节的部分内容搬过来。
REST (Representational State Transfer) API,这是Roy Fielding在2000年提出的一种架构风格或设计原则。REST的原则是将系统中的所有事物抽象为URL表示的资源。客户端可以通过 HTTP 请求(如 GET、POST、PUT、DELETE等)来操作资源。服务器收到请求后会根据不同的方法采取不同的响应,最终将数据以XML、JSON等格式返回给客户端。
说白了就是用URL表示资源然后用GET/POST等常见的动作来对资源进行操作。
我认为REST API为什么被大家喜爱主要有两个优点:
1、简单:
这种设计风格十分简洁易懂,因为大家都接触过HTTP协议,对URL请求也不陌生,以上一章用过的http://127.0.0.1:8080/stats/flow/1为例,很容易就能理解这是对flow流表的查询,最后的1就表示是对交换机1的查询,结合起来就是对交换机1的流表进行查询,是不是很容易理解。
还有就是使用上的简单,大家已经习惯了client-server形式的资源请求,在使用相似的REST API的时候就很容易了。
2、通用性:
由于互联网的快速发展,各种客户端应用和服务器应用层出不穷,REST API可以通过一套一致的接口为各种应用提供服务。换到SDN这里来说就是不管上层应用是啥,只要你们按着我提供好的接口给我发http请求,我就帮你干活。
尽管在SDN里没有明确的client-server结构,但是也可以这么抽象地理解:
用户,也就是应用平面,是client
控制器,也就是帮用户干活的,是server
然后REST API就可以完美地契合进去:
REST API在Ryu中的实现
ofctl_rest.py实现了很多REST API,上一章已经介绍过了,详情见
https://www.freesion.com/article/8090870623/
但是这些都是Ryu预定义好了的,我们如果想自己定义新的该怎么办?
比如说我的毕设需要我自己实现一些REST API来完成应用层与控制层的通信。
让我们看一下Ryu控制器给我们写好了的示例:simple_switch_rest_13.py
详情见
https://osrg.github.io/ryu-book/en/html/rest_api.html
# 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.
import json
from ryu.app import simple_switch_13
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_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
simple_switch_instance_name = 'simple_switch_api_app'
url = '/simpleswitch/mactable/{dpid}'
class SimpleSwitchRest13(simple_switch_13.SimpleSwitch13):
_CONTEXTS = {'wsgi': WSGIApplication}
def __init__(self, *args, **kwargs):
super(SimpleSwitchRest13, self).__init__(*args, **kwargs)
self.switches = {}
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):
super(SimpleSwitchRest13, self).switch_features_handler(ev)
datapath = ev.msg.datapath
self.switches[datapath.id] = datapath
self.mac_to_port.setdefault(datapath.id, {})
def set_mac_to_port(self, dpid, entry):
mac_table = self.mac_to_port.setdefault(dpid, {})
datapath = self.switches.get(dpid)
entry_port = entry['port']
entry_mac = entry['mac']
if datapath is not None:
parser = datapath.ofproto_parser
if entry_port not in mac_table.values():
for mac, port in mac_table.items():
# from known device to new device
actions = [parser.OFPActionOutput(entry_port)]
match = parser.OFPMatch(in_port=port, eth_dst=entry_mac)
self.add_flow(datapath, 1, match, actions)
# from new device to known device
actions = [parser.OFPActionOutput(port)]
match = parser.OFPMatch(in_port=entry_port, eth_dst=mac)
self.add_flow(datapath, 1, match, actions)
mac_table.update({entry_mac: entry_port})
return mac_table
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)
让我们看看它干了哪几件事:
1、WSGI:
想实现一个简单的REST接口,只需让程序代码满足Python的WSGI标准即可。
WSGI又是啥?
https://zhuanlan.zhihu.com/p/95942024 WSGI,Web Server Gateway Interface,是一套接口标准协议/规范,让Web服务器和Python Web应用程序之间可以正常的通信。
那么为啥要有WSGI呢?因为Web框架和Web服务器都有很多种,如果不定义一套接口的话会导致各说各的,没办法有效地通信。
Ryu中已经为我们实现了WSGI,在app的目录下有个文件叫wsgi.py。
在实现REST API的时候需要引入以下几个类:
from ryu.app.wsgi import ControllerBase
from ryu.app.wsgi import Response
from ryu.app.wsgi import route
from ryu.app.wsgi import WSGIApplication
然后还需要进行WSGI的注册:
class SimpleSwitchRest13(simple_switch_13.SimpleSwitch13):
_CONTEXTS = {'wsgi': WSGIApplication}
def __init__(self, *args, **kwargs):
super(SimpleSwitchRest13, self).__init__(*args, **kwargs)
self.switches = {}
wsgi = kwargs['wsgi']
wsgi.register(SimpleSwitchController,
{simple_switch_instance_name: self})
SimpleSwitchController是用来接收HTTP请求,处理路由的类。
这部分代码将SimpleSwitchController注册到WSGIApplication的一个实例里,同时需要传入参数{simple_switch_instance_name: self}让SimpleSwitchController与SimpleSwitchRest13连接上。
连接上之后可以在SimpleSwitchController中对SimpleSwitchRest13进行一系列的操作。
2、定义全局变量:
需要定义两个全局变量
simple_switch_instance_name = 'simple_switch_api_app'
url = '/simpleswitch/mactable/{dpid}'
第一个是为了帮助SimpleSwitchController对SimpleSwitchRest13进行操作。
第二个是定义需要处理的url,其中{dpid}是变量。
如果我们请求http://127.0.0.1:8080/simpleswitch/mactable/1,那么dpid就为1。
3、SimpleSwitchController类
SimpleSwitchController类是对路由和HTTP请求进行处理
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]
simple_switch_app让SimpleSwitchController可以对SimpleSwitchRest13进行操作。
@route('simpleswitch', url, methods=['GET'],
requirements={'dpid': dpid_lib.DPID_PATTERN})
def list_mac_table(self, req, **kwargs):
这里定义了路由,
也就是说只要我们发送请求形如“http://127.0.0.1/simpleswitch/mactable/数字”的GET请求就会调用list_mac_table()函数:
simple_switch = self.simple_switch_app
dpid = kwargs['dpid']
这两行表明SimpleSwitchController可以对SimpleSwitchRest13进行操作,并且dpid是用户通过url传入的参数。
如果我们想定义新的路由及处理方式的话只需要编写2、3步的代码即可。
如想添加查询当前存在的任务的功能:
定义全局变量
urlQuery='/measurement/query/tasks'
在SimpleSwitchController中增加路由
@route('measurement', urlQuery, methods=['GET'])#下发查询任务的指令
def setQuery(self, req, **kwargs):
simple_switch = self.simple_switch_app
tasks_info=simple_switch.queryTasks()
return Response(content_type='application/json', text=tasks_info)
意图驱动
实现了北向协议后,用户可以向控制器发出一个个指令了,但是人们还不满足于此。
就像那个经典的段子描述的一样:
人类因为太懒了,所以才会创造一个个工具帮助人类干活。
没错,用户觉得光这样还是太麻烦、太累了。
用户想,我这发一个指令就得发一次HTTP请求,那如果我要打一套组合拳发一系列的指令,那我岂不是得手动发好多条指令。不行,这样太麻烦,那怎么办呢?
用户想,那我就找个方法,只要把我想干的事描述一下,应用就发送用于完成这件事的一系列指令。用户拍了拍应用的脑袋,说:你已经是一个成熟的应用了,该学会自己下发指令了。
没错,这种思想就是意图驱动的核心,就是为了让人们可以更方便地管理网络。
用户在应用层编排网络管理任务的时候,仍需要了解底层的相关实现细节,起码得知道每条指令是干啥的,需要用什么指令才能完成想干的事情。但是用意图驱动之后,应用会将用户的意图映射为一条条指令,然后将这些指令从应用平面下发到控制平面。
也就是说意图是将北向协议再次抽象化了。
这样还有一个好处,就是可以让非专业人士对网络进行有效的管理。
如果没有意图驱动,那么只有网络工程师可以编排任务,但是现在只要说一句“我想看看谁在用校园网看b站”,那么应用会自动将这个意图拆分成多个指令:
1、应用先查询b站的ip。
2、让控制平面下发流表项,匹配域为“源ip为b站服务器,或者目的ip为b站服务器”。
3、控制平面在一定时间内查询,然后把ip与同学相对应。
4、控制平面汇报给应用。
这样很复杂的事情一句话就解决了,当然,这是因为我们已经在应用平面实现了意图的映射,这些都是我们制定好的规则。
目前业内对意图驱动网络,也就是基于意图的网络intent-based networking(IBN)还没有一套标准。也是近几年网络方向的一个研究热点。
我推荐把东北大学李福亮老师的《基于意图的网络研究综述》来作为IBN的入门。
IBN不是一个新的技术,而是一种新的理念,将已有的技术结合起来对网络进行更方便的管理。下图是李福亮老师这篇论文给出的IBN体系结构:
可以看到的是IBN的结构和SDN很像,因此可以在SDN的基础上实现IBN。
IBN的实施步骤可分为意图表达、意图转化、策略验证、意图发布与执行、网络信息的实时反馈。(更详细的内容请看李福亮老师的这篇论文)
1、意图表达:
表达意图的方法有很多,最流行的是用自然语言书写意图。对于自然语言输入,大多数研究使用语义的形式来获取意图。
2、意图转化:
意图的转化是IBN的核心任务,实现了从用户意图到网络策略的转化。
目前,意图的转化主要采用自然语言处理NLP的方法,通过关键词抽取、词法分析、意图语义挖掘,生成相应的网络策略(原语)。
3、策略验证:
为了保证IBN的鲁棒性,通过意图转换得到的网络策略不能直接下发,需要在发布前进行策略的验证。目前,策略的验证主要考虑资源的可用性、策略的冲突和策略的正确性。
检查资源的可用性是指根据系统中现有的资源决定是否可以发布该策略。对于策略冲突的验证,需要确定要发布的策略与系统中当前策略之间是否存在冲突,如果存在,则需要消除检测到的冲突。
对策略正确性的验证是指该策略是否可以按预期在网络中实现。
4、意图发布与执行:
在验证网络策略后,IBN自动将策略发送到网络基础设施并配置相应的转发规则。由于IBN需要将集中的意图转换为分布式的全局网络配置,整个策略下发的过程可以由SDN实现。
5、网络信息的实时反馈:
策略下发到网络后,需要实时监控网络的状态信息,保证网络的转发行为符合意图。此外,由于网络的状态也在不断变化,IBN需要根据当前网络状态自动优化和调整策略,保证网络始终满足任务需求。
总结
我毕设的时候实现了一套意图驱动的分布式SDN协同网络测量系统。
只不过因为时间比较紧,最后只基于策略库实现了意图的映射,以后如果有机会的话可以尝试用NLP来映射。
下一章会以网络测量为例,说一下应用平面以及SDN在网络测量方向的应用。