POX 控制器 API (二)—— OpenFlow 篇

使用 POX 的主要目的之一是开发 OpenFlow 控制应用程序 - 也就是说,POX 充当 OpenFlow 交换机的控制器。

由于 POX 经常与 OpenFlow 一起使用,因此有一种特殊的需求加载机制,它通常会检测你何时尝试使用 OpenFlow,并使用默认值加载与 OpenFlow 相关的组件。如果需求加载没有检测到你正在尝试使用它,你可以调整组件以明确你要使用它(只需在启动函数中访问 core.openflow),或者只需指定在命令行指定以 “openflow” 开头的组件。

POX OpenFlow API 的主要部分是 OpenFlow “nexus” 对象。通常,有一个这样的注册为 core.openflow 的对象作为上述需求加载过程的一部分。

实际与 OpenFlow 交换机通信的 POX 组件是 openflow.of_01(01 表示该组件使用 OpenFlow 1.0 版本)。需求加载功能通常会使用默认值初始化此组件(侦听端口6633)。但是同样地,你可以自动调用它来更改选项,或者多次运行它(例如,侦听普通 TCP 和 SSL 或多个端口)。

DPID

OpenFlow 规范指定每个数据路径(交换机)都具有唯一的数据路径 ID(DPID),它有 64 位,并且在握手期间通过 ofp_switch_features 消息从交换机传送到控制器。48 位是以太网地址,16 位是“实现者定义的”(通常是零)。由于 OpenFlow Connection 对象(下面讨论)绑定到特定交换机,因此使用 .dpid 属性时在 Connection 对象上可以使用 DPID,使用 .eth_addr 属性时可以使用相应的以太网地址。

POX 内部将 DPID 视为 Python 整数类型。不过这对我们来说并不好。如果将它们打印出来,它们只是一个十进制数,可能不容易查看或与以太网地址相关联。因此,POX 定义了格式化 DPID 的特定方式,该方法在 pox.lib.util.dpid_to_str() 中实现。在 16 个“实现者定义”位为 0 的常见情况下传递 DPID 时,DPID 值是一个看起来非常像以太网地址的字符串,除了用破折号代替冒号作为分隔符。如果实现者定义的位非零,则将它们视为十进制数,并附加在上述字符串之后,例如 “00-00-00-00-00-05 | 123”。

默认情况下,Mininet 以直接的方式为交换机分配 DPID。如果一个交换机是 “s3”,那么它的 DPID 将是 3。当与 --mac 选项一起使用时,这可能会有问题。--mac 选项以相同的方式为主机分配 MAC 地址。如果主机为 “h3”,则其 MAC 将为 00:00:00:00:00:03,与 “s3” 的 MAC 地址相同。这是混淆和问题的根源,因为 MAC 通常被认为应该是唯一的。

与数据路径(交换机)通信

当通信是从控制器到交换机时,控制器代码将 OpenFlow 消息发送到特定交换机(稍后会详细介绍)。当消息来自交换机时,它们在 POX 中表示为“事件”,你可以为它编写事件处理程序——通常会有一个与交换机可能发送的每种消息类型相对应的事件类型。

在 POX 中,你可以通过两种方式与数据路径进行通信:通过该特定数据路径的 Connection 对象,或通过管理该数据路径的 OpenFlow Nexus。连接到 POX 的每个数据路径都有一个 Connection 对象,通常有一个 OpenFlow Nexus 管理所有连接。在正常配置中,有一个 OpenFlow 连接,通过 core.openflow 可以获取。Connections 和 Nexus 之间有很多重叠。任何一个都可用于向交换机发送消息,并且大多数事件都在两者上引发。有时使用其中一个更方便。如果你的应用程序对所有交换机的事件感兴趣,那么收听
Nexus 可能是有意义的,这会引发所有交换机的事件。如果你只对一个交换机感兴趣,那么收听特定连接可能是有意义的。

Connection 对象

每次交换机连接到 POX 时,都会有一个关联的 Connection 对象。 如果您的代码具有对该 Connection 对象的引用,则可以使用其 send() 方法将消息发送到数据路径。

Connection 对象能够将命令发送到交换机并作为交换机事件的源,还具有许多其他有用的属性。我们在这里列出一些:

成员 描述
dpid 交换机的数据路径标识符
ofnexus 对与此连接关联的 nexus 对象的引用
ports 交换机上的端口。由于这些可能在连接的生命周期内发生变化,POX 会尝试跟踪此类更改。但是总有可能这些已经过时。此属性是对特殊 PortCollection 对象的引用。这个对象有点像字典,其中值是 p_phy_port 对象,键是灵活的_可以通过 OpenFlow 端口号(ofp_phy_port 的 .port_no)、它们的以太网地址(ofp_phy_port 的 .hw_addr)或它们的端口名称(ofp_phy_port 的 .name,例如 “eth0”)查找端口。
send(msg) 一种用于向交换机发送 OpenFlow 消息的方法。
sock 连接到对等的套接字。这是一个 Python 套接字对象,因此您可以使用 connection.sock.getpeername() 检索连接的交换机端的地址。

除了其属性和 send() 方法之外,Connection 对象还会引发与特定数据路径相对应的事件,例如当数据路径断开连接或发送通知时。你可以通过在关联的 Connection 上注册事件侦听器来为特定数据路径上的事件创建处理程序。

获取对 Connection 对象的引用

如果希望使用任何上述 Connection 对象的属性,需要引用与你感兴趣的数据路径相关联的 Connection 对象。有三种主要方法可以获得这个引用:

  • 在 nexus 上侦听 ConnectionUp 事件——这些事件将新的 Connection 对象传递进来;
  • 使用 nexus 的 getConnection() 方法通过交换机的 DPID 查找连接;
  • 通过 nexus 的 connections 属性枚举所有的连接。

第一种方法中,你可以在自己的组件类中使用代码来跟踪连接并存储对它们的引用。它通过在 OpenFlow 连接上侦听 ConnectionUp 事件来完成此操作。此事件包括对新连接的引用,该连接将添加到其自己的连接集中。以下代码演示了这一点(注意,更完整的实现是使用 ConnectionDown 事件从集合中删除关闭的 connection)。

class MyComponent (object):
    def __init__ (self):
        self.connections = set()
        core.openflow.addListeners(self)
 
    def _handle_ConnectionUp (self, event):
        self.connections.add(event.connection) # See ConnectionUp event documentation
OpenFlow Nexus:core.openflow

OpenFlow 连接本质上是一组 OpenFlow 连接的管理器。通常,有一个管理所有交换机的连接的 nexus,通过 core.openflow 来使用。

在这里,我们列出了一个 nexus 的一些属性:

属性 描述
miss_send_len 当数据包与交换机上的任何流表都不匹配时,交换机将在 packet-in 消息中将数据包转发到控制器。为了节省带宽,交换机实际上不会发送整个数据包,而只会发送前 miss_send_len 字节。通过在此处调整此值,随后连接的任何交换机将配置为仅发送此字节数。默认值为 128 字节。
clear_flows_of_connect 当为 True(默认值)时,POX 将删除新连接的交换机第一个表上的所有流。
connections 一个特殊的集合(见下文),包含此 nexus 正在处理的所有连接的引用。
getConnection(<dpid>) 通过交换机的 DPID 获取其 Connection 对象。
sendToDPID(<dpid>, <msg>) 向特定的交换机发送 OpenFlow 消息,如果交换机未连接,丢弃该消息(并记录一条 warning)。类似于执行 core.openflow.getConnection(dpid).send(msg)

connections 集合本质上是一个字典,其中键是 DPID,值是 Connection 对象。但是如果执行迭代操作,它将迭代 Connections 而不是 DPID,这与普通字典不同。要迭代 DPID,可以使用 .iter_dpids() 方法。此外,您可以使用 “in” 运算符来检查 Connection 是否在此集合中以及 DPID 是否在集合中,并且 .dpids() 属性与 .keys() 实际上相同。

与 Connection 对象一样,也可以在 nexus 对象本身上设置事件侦听器。Connection 对象仅引发与该特定 Connection 关联的数据路径相关的事件,而 nexus 对象会引发与其管理的任何连接相关的事件。

OpenFlow 事件:响应交换机

大多数 OpenFlow 相关事件都是由直接响应从交换机收到的消息而引发的。一般 OpenFlow 相关事件具有以下三个属性:

属性 类型 描述
connection Connection 到相关交换机的连接
dpid long 相关交换机的数据路径 ID(使用 dpid_to_str() 将其格式化以便显示)。
ofp ofp_header subclass 产生此事件的 OpenFlow 消息对象。

接下来介绍一些 OpenFlow 模块和拓扑模块提供的一些事件。下面是一个非常简单的 POX 组件,它侦听来自所有交换机的 ConnectionUp 事件,并在发生消息时记录消息。你可以将其放入一个文件(例如 ext/connection_watcher.py),然后运行它(使用 ./pox.py connection_watcher)并观察交换机连接。

from pox.core import core
from pox.lib.util import dpid_to_str
 
log = core.getLogger()
 
class MyComponent (object):
    def __init__ (self):
        core.openflow.addListeners(self)
 
    def _handle_ConnectionUp (self, event):
        log.debug("Switch %s has come up.", dpid_to_str(event.dpid))
 
def launch ():
    core.registerNew(MyComponent)
ConnectionUp

此事件因建立与交换机的新控制信道而触发。

另请注意,虽然大多数 OpenFlow 事件在 Connection 和 OpenFlow nexus 上都会触发,但 ConnectionUp 事件仅在 nexus 上触发。这是有道理的,因为 ConnectionUp 事件后 Connection 才存在。

附加属性信息(除标准 OpenFlow 事件属性外):

属性 类型 描述
ofp ofp_switch_features 包含有关交换机的信息,例如支持的操作类型(例如字段重写是否可用)和端口信息(例如 MAC 地址和名称)(这也可以在 Connection 的 features 属性中实现)。

此事件可以按如下所示进行处理:

def _handle_ConnectionUp (self, event):
    print "Switch %s has come up." % event.dpid
ConnectionDown

与 ConnectionUp 不同,此事件在 nexus 和 Connection 本身都会引发。此事件没有 .ofp 属性。

PortStatus

当控制器从交换机收到 OpenFlow 端口状态消息(ofp_port_status)时,会引发 PortStatus 事件,这表示端口已更改。因此,其 .ofp 属性是 ofp_port_status。

class PortStatus (Event):
    def __init__ (self, connection, ofp):
        Event.__init__(self)
        self.connection = connection
        self.dpid = connection.dpid
        self.ofp = ofp
        self.modified = ofp.reason == of.OFPPR_MODIFY
        self.added = ofp.reason == of.OFPPR_ADD
        self.deleted = ofp.reason == of.OFPPR_DELETE
        self.port = ofp.desc.port_no

小例子:

def _handle_PortStatus (self, event):
    if event.added:
        action = "added"
    elif event.deleted:
        action = "removed"
    else:
        action = "modified"
    print "Port %s on Switch %s has been %s." % (event.port, event.dpid, action)
FlowRemoved

当控制器从交换机接收到 OpenFlow 流删除消息(ofp_flow_removed)时,会引发 FlowRemoved 事件,这些消息是由于超时或显式删除而在交换机上删除表条目时发送的。只有在该流设置了 OFPFF_SEND_FLOW_REM 标志时才会发送此类通知。

虽然你可以像往常一样通过事件的 .ofp 属性直接访问 ofp_flow_removed,但为方便起见,该事件有几个属性:

attribute type meaning
idleTimeout bool 由于闲置(没有流量匹配的时间超过设定值)而被删除
hardTimeout bool 由于硬超时(被插入表中的总时间超过设定值)而被删除
timeout bool 由于无论何种超时而被删除
deleted bool 被显式删除
统计事件

当控制器从交换机接收到 OpenFlow 统计回复消息(ofp_stats_reply/OFPT_STATS_REPLY)时,会引发统计事件,该消息是响应控制器发送的统计请求的。

POX 控制器 API (二)—— OpenFlow 篇_第1张图片
PacketIn

当控制器从交换机收到 OpenFlow 数据包输入消息(ofp_packet_in/OFPT_PACKET_IN)时触发,指示到达交换机端口的数据包未能匹配,或者匹配条目的操作指定将数据包发送到控制器。

除了通常的 OpenFlow 事件属性外:

  • port(int)——数据包进入的端口号
  • data(字节)——原始分组数据
  • parsed(数据包子类)——pox.lib.packet 的解析版本
  • ofp(ofp_packet_in)——导致此事件的 OpenFlow 消息
ErrorIn
POX 控制器 API (二)—— OpenFlow 篇_第2张图片
BarrierIn

OpenFlow 消息

POX 包含与 OpenFlow 协议元素对应的类和常量,这些在文件 pox/openflow/libopenflow_01.py 中定义。

ofp_packet_out:从交换机发送数据包

此消息的主要目的是指示交换机发送数据包(或将其排入队列)。它也可以用于指示交换机丢弃缓冲分组(不指定任何动作即可)。

POX 控制器 API (二)—— OpenFlow 篇_第3张图片
ofp_flow_mod:流表修改(重要!)
  • cookie(int):此流规则的标识符(可选);
  • command(int):以下值之一:
    • OFPFC_ADD:向数据路径添加规则(默认);
    • OFPFC_MODIFY:修改任何匹配规则;
    • OFPFC_MODIFY_STRICT:修改严格匹配通配符值的规则;
    • OFPFC_DELETE:删除任何匹配的规则;
    • OFPFC_DELETE_STRICT:删除严格匹配通配符值的规则。
  • idle_timeout(int):如果在 “idle_timeout” 秒内未匹配,则规则将过期。值 OFP_FLOW_PERMANENT(默认)表示没有 idle_timeout;
  • hard_timeout(int):规则将在 'hard_timeout' 秒后过期。值 OFP_FLOW_PERMANENT(默认)表示它永不过期;
  • priority(int):规则匹配的优先级,数字越大优先级越高。注意:完全匹配将具有最高优先级
  • buffer_id(int):新流将应用的数据路径上的缓冲区。没有就设成 None。对流删除没有意义;
  • out_port(int):该字段用于匹配 DELETE 命令。OFPP_NONE 可用于表示没有限制;
  • flags(int):可以设置以下标志位的整数位域:
    • OFPFF_SEND_FLOW_REM:当规则到期时,将流删除的消息发送到控制器;
    • OFPFF_CHECK_OVERLAP:安装时检查重叠条目。如果存在,则向控制器发送错误;
    • OFPFF_EMERG:将此流程视为紧急流程,仅在交换机控制器连接断开时使用它。
  • actions(list):动作定义如下,每个所需的动作对象随后被附加到该列表中,并按顺序执行;
  • match(ofp_match):匹配规则的匹配结构(见下文)。

【例子:安装表条目】

# Traffic to 192.168.101.101:80 should be sent out switch port 4
 
# One thing at a time...
msg = of.ofp_flow_mod()
msg.priority = 42
msg.match.dl_type = 0x800    # 以太网协议类型
msg.match.nw_dst = IPAddr("192.168.101.101")
msg.match.tp_dst = 80
msg.actions.append(of.ofp_action_output(port = 4))
self.connection.send(msg)
 
# Same exact thing, but in a single line...
self.connection.send( of.ofp_flow_mod( action=of.ofp_action_output( port=4 ),
                                       priority=42,
                                       match=of.ofp_match( dl_type=0x800,
                                                           nw_dst="192.168.101.101",
                                                           tp_dst=80 )))

【例子:清除所有交换机上的表】

# create ofp_flow_mod message to delete all flows
# (note that flow_mods match all flows by default)
msg = of.ofp_flow_mod(command=of.OFPFC_DELETE)
 
# iterate over all connected switches and delete all their flows
for connection in core.openflow.connections: # _connections.values() before betta
  connection.send(msg)
  log.debug("Clearing all flows from %s." % (dpidToStr(connection.dpid),))
ofp_stats_request:请求来自交换机的统计信息

【例子:Web 流量统计】
从交换机请求流表并转储有关 Web 流量的信息。此例与 forwarding.l2_learning 组件一起运行,可以粘贴到 POX 交互式解释器中。

import pox.openflow.libopenflow_01 as of
log = core.getLogger("WebStats")
 
# When we get flow stats, print stuff out
def handle_flow_stats (event):
    web_bytes = 0
    web_flows = 0
    for f in event.stats:
        if f.match.tp_dst == 80 or f.match.tp_src == 80:
            web_bytes += f.byte_count
            web_flows += 1
    log.info("Web traffic: %s bytes over %s flows", web_bytes, web_flows)
 
# Listen for flow stats
core.openflow.addListenerByName("FlowStatsReceived", handle_flow_stats)
 
# Now actually request flow stats from all switches
for con in core.openflow.connections: # make this _connections.keys() for pre-betta
    con.send(of.ofp_stats_request(body=of.ofp_flow_stats_request()))

匹配结构

匹配结构在类 ofp_match 中的 pox/openflow/libopenflow_01.py 中定义。ofp_match 属性有:

属性 含义
dl_dst 以太网目的地址
dl_src 以太网源地址
dl_type 以太网类型 / 长度(e.g. 0x0800 = IPv4)
dl_vlan VLAN ID
dl_vlan_pcp VLAN 优先级
in_port 数据包到达交换机的端口号
nw_dst IP 目的地址
nw_proto IP 协议(e.g., 6 = TCP)或低八位的 ARP 操作码
nw_src IP 源地址
nw_tos IP TOS/DS 位
tp_dst TCP/UDP 目的端口
tp_src TCP/UDP 源端口
部分匹配和通配符

IP 地址字段可以像其他字段一样完全通配,但也可以部分匹配。这允许你匹配整个子网。以下是一些等价的方法:

my_match.nw_src = "192.168.42.0/24"
my_match.nw_src = (IPAddr("192.168.42.0"), 24)
my_match.nw_src = "192.168.42.0/255.255.255.0"
my_match.set_nw_src(IPAddr("192.168.42.0"), 24)

特别要注意的是,在处理部分匹配时,nw_src 和 nw_dst 属性可能不明确,特别是在读取匹配结构时(例如在 flow_removed 消息或 flow_stats 回复中返回)。为了解决这个问题,你可以使用明确的 .get_nw_src().set_nw_src(),目的地址同理。它们返回一个元组,如 (IPAddr(“192.168.42.0”), 24)。

请注意,某些字段具有先决条件。这意味着您不能指定更高层的字段而不指定相应的下层字段。例如,如果不指定希望匹配 TCP 流量,则无法在 TCP 端口上创建匹配项。并且为了匹配 TCP 流量,您必须指定您希望匹配 IP 流量。例如,仅与 tp_dst = 80 的匹配是无效的。您还必须指定 nw_proto = 6(TCP)和 dl_type = 0x800(IPv4)。如果您违反此规则,则应收到警告消息 “Fields ignored due to unspecified prerequisites(由于未指定的先决条件而忽略字段)”。有关此主题的更多信息,请参阅 FAQ “I tried to install a table entry but got a different one. Why?”

ofp_match 方法
方法 描述
from_packet(packet, in_port=None, spec_frags=False) 类工厂。
clone() 返回一个此 ofp_match 的副本。
flip() 返回其源和目标相反的副本。
show() 返回一个字符串表示。
get_nw_src() 返回 IP 源地址和匹配位数组成的元组。例如:(IPAddr(“192.168.42.0”, 24)。注意,当第二个为 0 时,元组的第一个元素将为 None。
set_nw_src(IP and bits) 设置 IP 源地址和要匹配的位数。参数可以是两个(一个用于 IP,一个用于位计数),或者是 get_nw_src() 使用的格式化的元组。
get_nw_dst() 与 get_nw_src() 相同,但是是针对目标地址的。
set_nw_dst(IP and bits) 与 set_nw_src() 相同,但是是针对目标地址的。
定义现有数据包的匹配项

有一种基于现有数据包对象(即 pox.lib.packet 中的以太网对象)或现有的 ofp_packet_in 创建完全匹配的简单方法。这是使用 ofp_match.from_packet() 工厂方法实现的。

my_match = ofp_match.from_packet(packet, in_port)

packet 参数是解析的数据包或 ofp_packet_in,用于创建匹配。

【例子:匹配 Web 流量】

import pox.openflow.libopenflow_01 as of    # POX convention
import pox.lib.packet as pkt    # POX convention
my_match = of.ofp_match(dl_type = pkt.ethernet.IP_TYPE,
                        nw_proto = pkt.ipv4.TCP_PROTOCOL,
                        tp_dst = 80)

OpenFlow 操作

OpenFlow 操作应用于与数据路径上安装的规则匹配的数据包。这里的代码片段可以在 pox/openflow 的 libopenflow_01.py 中找到。

输出

通过物理或虚拟端口转发数据包。物理端口由其整数值引用,而虚拟端口具有符号名称。物理端口的端口号应小于 0xFF00。

  • port(int):此数据包的输出端口。值可以是实际端口号或以下虚拟端口之一:
    • OFPP_IN_PORT:发回收到数据包的端口。除了 OFPP_NORMAL 之外,这是将数据包发送回其传入端口的唯一方法;
    • OFPP_TABLE:执行流表中指定的操作。注意:仅适用于 ofp_packet_out 消息;
    • OFPP_NORMAL:通过正常的 L2 / L3 传统交换机配置进行处理(如果可用的话,取决于交换机);
    • OFPP_FLOOD:输出到除输入端口以及通过 OFPPC_NO_FLOOD 端口配置位禁用泛洪的端口之外的所有开放流端口;
    • OFPP_ALL:输出除输入端口以外的所有开放流端口;
    • OFPP_CONTROLLER:发送到控制器;
    • OFPP_LOCAL:输出到本地 openflow 端口;
    • OFPP_NONE:不输出。
POX 控制器 API (二)—— OpenFlow 篇_第4张图片
POX 控制器 API (二)—— OpenFlow 篇_第5张图片
POX 控制器 API (二)—— OpenFlow 篇_第6张图片
POX 控制器 API (二)—— OpenFlow 篇_第7张图片
POX 控制器 API (二)—— OpenFlow 篇_第8张图片
POX 控制器 API (二)—— OpenFlow 篇_第9张图片

【例子:发送一个 FlowMod】

msg = ofp_flow_mod()
msg.match = match
msg.idle_timeout = idle_timeout
msg.hard_timeout = hard_timeout
msg.actions.append(of.ofp_action_output(port = port))
msg.buffer_id = 
connection.send(msg)

【例子:发送一个 PacketOut】

msg = of.ofp_packet_out(in_port=of.OFPP_NONE)
msg.actions.append(of.ofp_action_output(port = outport))
msg.buffer_id = 
connection.send(msg)

交互式交换机会话示例

POX> INFO:openflow.of_01:[00-00-00-00-00-01 1] connected
POX> MySwitch.list_available_listeners()
INFO:samples.of_sw_tutorial_oo:SW_BADSWITCH
INFO:samples.of_sw_tutorial_oo:SW_LAZYHUB
INFO:samples.of_sw_tutorial_oo:SW_PAIRSWITCH
INFO:samples.of_sw_tutorial_oo:SW_IDEALPAIRSWITCH
INFO:samples.of_sw_tutorial_oo:SW_DUMBHUB
INFO:samples.of_sw_tutorial_oo:SW_PAIRHUB
POX> MySwitch.clear_all_flows()
DEBUG:samples.of_sw_tutorial_oo:Clearing all flows from 00-00-00-00-00-01.
POX> MySwitch.detach_packetin_listener()
DEBUG:samples.of_sw_tutorial_oo:Detaching switch SW_IDEALPAIRSWITCH.
POX> MySwitch.attach_packetin_listener('SW_LAZYHUB')
DEBUG:samples.of_sw_tutorial_oo:Attach switch SW_LAZYHUB.
POX> MySwitch.clear_all_flows()
DEBUG:samples.of_sw_tutorial_oo:Clearing all flows from 00-00-00-00-00-01.
POX> MySwitch.detach_packetin_listener()
DEBUG:samples.of_sw_tutorial_oo:Detaching switch SW_LAZYHUB.
POX> MySwitch.attach_packetin_listener('SW_BADSWITCH')
DEBUG:samples.of_sw_tutorial_oo:Attach switch SW_BADSWITCH.
···

你可能感兴趣的:(POX 控制器 API (二)—— OpenFlow 篇)