diff --git a/ryu/app/simple_switch_stp.py b/ryu/app/simple_switch_stp.py new file mode 100644 index 00000000..cfea26a7 --- /dev/null +++ b/ryu/app/simple_switch_stp.py @@ -0,0 +1,132 @@ +# Copyright (C) 2013 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 struct + +from ryu.base import app_manager +from ryu.controller.handler import set_ev_cls +from ryu.ofproto import ofproto_v1_0 +from ryu.lib import dpid as dpid_lib +from ryu.lib import stplib +from ryu.lib.mac import haddr_to_str + + +class SimpleSwitchStp(app_manager.RyuApp): + OFP_VERSIONS = [ofproto_v1_0.OFP_VERSION] + _CONTEXTS = {'stplib': stplib.Stp} + + def __init__(self, *args, **kwargs): + super(SimpleSwitchStp, self).__init__(*args, **kwargs) + self.mac_to_port = {} + self.stp = kwargs['stplib'] + + # Sample of stplib config. + # please refer to stplib.Stp.set_config() for details. + """ + config = {dpid_lib.str_to_dpid('0000000000000001'): + {'bridge': {'priority': 0x8000, + 'max_age': 10}, + 'ports': {1: {'priority': 0x80}, + 2: {'priority': 0x90}}}, + dpid_lib.str_to_dpid('0000000000000002'): + {'bridge': {'priority': 0x9000}}} + self.stp.set_config(config) + """ + + def add_flow(self, datapath, in_port, dst, actions): + ofproto = datapath.ofproto + + wildcards = ofproto_v1_0.OFPFW_ALL + wildcards &= ~ofproto_v1_0.OFPFW_IN_PORT + wildcards &= ~ofproto_v1_0.OFPFW_DL_DST + + match = datapath.ofproto_parser.OFPMatch( + wildcards, in_port, 0, dst, + 0, 0, 0, 0, 0, 0, 0, 0, 0) + + mod = datapath.ofproto_parser.OFPFlowMod( + datapath=datapath, match=match, cookie=0, + command=ofproto.OFPFC_ADD, idle_timeout=0, hard_timeout=0, + priority=ofproto.OFP_DEFAULT_PRIORITY, + flags=ofproto.OFPFF_SEND_FLOW_REM, actions=actions) + datapath.send_msg(mod) + + def delete_flow(self, datapath): + ofproto = datapath.ofproto + + wildcards = ofproto_v1_0.OFPFW_ALL + match = datapath.ofproto_parser.OFPMatch( + wildcards, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + + mod = datapath.ofproto_parser.OFPFlowMod( + datapath=datapath, match=match, cookie=0, + command=ofproto.OFPFC_DELETE) + datapath.send_msg(mod) + + @set_ev_cls(stplib.EventPacketIn, stplib.STP_EV_DISPATCHER) + def packet_in_handler(self, ev): + msg = ev.msg + datapath = msg.datapath + ofproto = datapath.ofproto + + dst, src, _eth_type = struct.unpack_from('!6s6sH', buffer(msg.data), 0) + + dpid = datapath.id + self.mac_to_port.setdefault(dpid, {}) + + self.logger.debug("packet in %s %s %s %s", + dpid, haddr_to_str(src), haddr_to_str(dst), + msg.in_port) + + # learn a mac address to avoid FLOOD next time. + self.mac_to_port[dpid][src] = msg.in_port + + if dst in self.mac_to_port[dpid]: + out_port = self.mac_to_port[dpid][dst] + else: + out_port = ofproto.OFPP_FLOOD + + actions = [datapath.ofproto_parser.OFPActionOutput(out_port)] + + # install a flow to avoid packet_in next time + if out_port != ofproto.OFPP_FLOOD: + self.add_flow(datapath, msg.in_port, dst, actions) + + out = datapath.ofproto_parser.OFPPacketOut( + datapath=datapath, buffer_id=msg.buffer_id, in_port=msg.in_port, + actions=actions) + datapath.send_msg(out) + + @set_ev_cls(stplib.EventTopologyChange, stplib.STP_EV_DISPATCHER) + def _topology_change_handler(self, ev): + dp = ev.dp + dpid_str = dpid_lib.dpid_to_str(dp.id) + msg = 'Receive topology change event. Flush MAC table.' + self.logger.debug("[dpid=%s] %s", dpid_str, msg) + + if dp.id in self.mac_to_port: + del self.mac_to_port[dp.id] + self.delete_flow(dp) + + @set_ev_cls(stplib.EventPortStateChange, stplib.STP_EV_DISPATCHER) + def _port_state_change_handler(self, ev): + dpid_str = dpid_lib.dpid_to_str(ev.dp.id) + of_state = {ofproto_v1_0.OFPPS_LINK_DOWN: 'DISABLE', + ofproto_v1_0.OFPPS_STP_BLOCK: 'BLOCK', + ofproto_v1_0.OFPPS_STP_LISTEN: 'LISTEN', + ofproto_v1_0.OFPPS_STP_LEARN: 'LEARN', + ofproto_v1_0.OFPPS_STP_FORWARD: 'FORWARD'} + self.logger.debug("[dpid=%s][port=%d] state=%s", + dpid_str, ev.port_no, of_state[ev.port_state]) diff --git a/ryu/lib/stplib.py b/ryu/lib/stplib.py new file mode 100644 index 00000000..3cbd55ab --- /dev/null +++ b/ryu/lib/stplib.py @@ -0,0 +1,1100 @@ +# Copyright (C) 2013 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 logging + +from ryu.base import app_manager +from ryu.controller import event +from ryu.controller import handler +from ryu.controller import ofp_event +from ryu.controller.handler import set_ev_cls +from ryu.exception import RyuException +from ryu.exception import OFPUnknownVersion +from ryu.lib import hub +from ryu.lib.dpid import dpid_to_str +from ryu.lib.packet import bpdu +from ryu.lib.packet import ethernet +from ryu.lib.packet import llc +from ryu.lib.packet import packet +from ryu.ofproto import ofproto_v1_0 + + +STP_EV_DISPATCHER = "stplib" + + +MAX_PORT_NO = 0xfff + +# Result of compared config BPDU priority. +SUPERIOR = -1 +REPEATED = 0 +INFERIOR = 1 + +# Port role +DESIGNATED_PORT = 0 # The port which sends BPDU. +ROOT_PORT = 1 # The port which receives BPDU from a root bridge. +NON_DESIGNATED_PORT = 2 # The port which blocked. + +""" How to decide the port roles. + + Root bridge: + a bridge has smallest bridge ID is chosen as a root. + it sends original config BPDU. + Non Root bridge: + forwards config BPDU received from the root bridge. + + +-----------------------+ + | Root bridge | + +-----------------------+ + (D) (D) + | | + | | + (R) (R) + +-----------------+ +-----------------+ + | Non Root bridge |(D)---(ND)| Non Root bridge | + +-----------------+ +-----------------+ + + ROOT_PORT(R): + the nearest port to a root bridge of the bridge. + it is determined by the cost of the path, etc. + DESIGNATED_PORT(D): + the port of the side near the root bridge of each link. + it is determined by the cost of the path, etc. + NON_DESIGNATED_PORT(ND): + the port other than a ROOT_PORT and DESIGNATED_PORT. +""" + + +# Port state +# DISABLE: Administratively down or link down by an obstacle. +# BLOCK : Not part of spanning tree. +# LISTEN : Not learning or relaying frames. +# LEARN : Learning but not relaying frames. +# FORWARD: Learning and relaying frames. +PORT_STATE_DISABLE = (ofproto_v1_0.OFPPC_NO_RECV_STP + | ofproto_v1_0.OFPPC_NO_RECV + | ofproto_v1_0.OFPPC_NO_FLOOD + | ofproto_v1_0.OFPPC_NO_FWD) +PORT_STATE_BLOCK = (ofproto_v1_0.OFPPC_NO_RECV + | ofproto_v1_0.OFPPC_NO_FLOOD + | ofproto_v1_0.OFPPC_NO_FWD) +PORT_STATE_LISTEN = (ofproto_v1_0.OFPPC_NO_RECV + | ofproto_v1_0.OFPPC_NO_FLOOD) +PORT_STATE_LEARN = ofproto_v1_0.OFPPC_NO_FLOOD +PORT_STATE_FORWARD = 0 + +""" Port state machine + + +------------------------<--------------------------+ + | |*2 + +--> [BLOCK] -----+--> [LISTEN] ----> [LEARN] ------+----> [FORWARD] + *3 | | 15sec | 15sec *1 | + | |*3 |*3 |*3 + +----<---+------<------+----------<----------+ + + *1 if port role == DESIGNATED_PORT or ROOT_PORT + *2 if port role == NON_DESIGNATED_PORT + *3 re-calculation of Spanning tree occurred. + + When bridge has started, each port state is set to [LISTEN] + except port configuration is disable. + If port configuration is disable or link down occurred, + the port state is set to [DISABLE] +""" + + +# Throw this event when network topology is changed. +# Flush filtering database, when you receive this event. +class EventTopologyChange(event.EventBase): + def __init__(self, dp): + super(EventTopologyChange, self).__init__() + self.dp = dp + + +# Throw this event when port status is changed. +class EventPortStateChange(event.EventBase): + def __init__(self, dp, port): + super(EventPortStateChange, self).__init__() + + of_state = {PORT_STATE_DISABLE: ofproto_v1_0.OFPPS_LINK_DOWN, + PORT_STATE_BLOCK: ofproto_v1_0.OFPPS_STP_BLOCK, + PORT_STATE_LISTEN: ofproto_v1_0.OFPPS_STP_LISTEN, + PORT_STATE_LEARN: ofproto_v1_0.OFPPS_STP_LEARN, + PORT_STATE_FORWARD: ofproto_v1_0.OFPPS_STP_FORWARD} + + self.dp = dp + self.port_no = port.ofport.port_no + self.port_state = of_state[port.state] + + +# Event for receive packet in message except BPDU packet. +class EventPacketIn(event.EventBase): + def __init__(self, msg): + super(EventPacketIn, self).__init__() + self.msg = msg + + +class Stp(app_manager.RyuApp): + """ STP(spanning tree) library. """ + + OFP_VERSIONS = [ofproto_v1_0.OFP_VERSION] + + def __init__(self): + super(Stp, self).__init__() + self.name = 'stplib' + self._set_logger() + self.config = {} + self.bridge_list = {} + + def close(self): + for dpid in self.bridge_list.keys(): + self._unregister_bridge(dpid) + + def _set_logger(self): + self.logger.propagate = False + hdlr = logging.StreamHandler() + fmt_str = '[STP][%(levelname)s] dpid=%(dpid)s: %(message)s' + hdlr.setFormatter(logging.Formatter(fmt_str)) + self.logger.addHandler(hdlr) + + def set_config(self, config): + """ Use this API if you want to set up configuration + of each bridge and ports. + Set configuration with 'config' parameter as follows. + + config = {: {'bridge': {'priority': , + 'sys_ext_id': , + 'max_age': , + 'hello_time': , + 'fwd_delay': } + 'ports': {: {'priority': , + 'path_cost': , + 'enable': }, + : {...},,,}} + : {...}, + : {...},,,} + + NOTE: You may omit each field. + If omitted, a default value is set up. + It becomes effective when a bridge starts. + + Default values: + ------------------------------------------------------ + | bridge | priority | bpdu.DEFAULT_BRIDGE_PRIORITY | + | | sys_ext_id | 0 | + | | max_age | bpdu.DEFAULT_MAX_AGE | + | | hello_time | bpdu.DEFAULT_HELLO_TIME | + | | fwd_delay | bpdu.DEFAULT_FORWARD_DELAY | + |--------|------------|------------------------------| + | port | priority | bpdu.DEFAULT_PORT_PRIORITY | + | | path_cost | (Set up automatically | + | | | according to link speed.) | + | | enable | True | + ------------------------------------------------------ + """ + assert isinstance(config, dict) + self.config = config + + @set_ev_cls(ofp_event.EventOFPStateChange, + [handler.MAIN_DISPATCHER, handler.DEAD_DISPATCHER]) + def dispacher_change(self, ev): + assert ev.datapath is not None + if ev.state == handler.MAIN_DISPATCHER: + self._register_bridge(ev.datapath) + elif ev.state == handler.DEAD_DISPATCHER: + self._unregister_bridge(ev.datapath.id) + + def _register_bridge(self, dp): + self._unregister_bridge(dp.id) + + dpid_str = {'dpid': dpid_to_str(dp.id)} + self.logger.info('Join as stp bridge.', extra=dpid_str) + try: + bridge = Bridge(dp, self.logger, + self.config.get(dp.id, {}), + self.send_event_to_observers) + except OFPUnknownVersion as message: + self.logger.error(str(message), extra=dpid_str) + return + + self.bridge_list[dp.id] = bridge + + def _unregister_bridge(self, dp_id): + if dp_id in self.bridge_list: + self.bridge_list[dp_id].delete() + del self.bridge_list[dp_id] + self.logger.info('Leave stp bridge.', + extra={'dpid': dpid_to_str(dp_id)}) + + @set_ev_cls(ofp_event.EventOFPPacketIn, handler.MAIN_DISPATCHER) + def packet_in_handler(self, ev): + if ev.msg.datapath.id in self.bridge_list: + bridge = self.bridge_list[ev.msg.datapath.id] + bridge.packet_in_handler(ev.msg) + + @set_ev_cls(ofp_event.EventOFPPortStatus, handler.MAIN_DISPATCHER) + def port_status_handler(self, ev): + dp = ev.msg.datapath + dpid_str = {'dpid': dpid_to_str(dp.id)} + port = ev.msg.desc + reason = ev.msg.reason + link_down_flg = port.state & 0b1 + + if dp.id in self.bridge_list: + bridge = self.bridge_list[dp.id] + + if reason is dp.ofproto.OFPPR_ADD: + self.logger.info('[port=%d] Port add.', + port.port_no, extra=dpid_str) + bridge.port_add(port) + elif reason is dp.ofproto.OFPPR_DELETE: + self.logger.info('[port=%d] Port delete.', + port.port_no, extra=dpid_str) + bridge.port_delete(port.port_no) + else: + assert reason is dp.ofproto.OFPPR_MODIFY + if link_down_flg: + self.logger.info('[port=%d] Link down.', + port.port_no, extra=dpid_str) + bridge.link_down(port.port_no) + else: + self.logger.info('[port=%d] Link up.', + port.port_no, extra=dpid_str) + bridge.link_up(port.port_no) + + @staticmethod + def compare_root_path(path_cost1, path_cost2, bridge_id1, bridge_id2, + port_id1, port_id2): + """ Decide the port of the side near a root bridge. + It is compared by the following priorities. + 1. root path cost + 2. designated bridge ID value + 3. designated port ID value """ + result = Stp._cmp_value(path_cost1, path_cost2) + if not result: + result = Stp._cmp_value(bridge_id1, bridge_id2) + if not result: + result = Stp._cmp_value(port_id1, port_id2) + return result + + @staticmethod + def compare_bpdu_info(my_priority, my_times, rcv_priority, rcv_times): + """ Check received BPDU is superior to currently held BPDU + by the following comparison. + - root bridge ID value + - root path cost + - designated bridge ID value + - designated port ID value + - times """ + if my_priority is None: + result = SUPERIOR + else: + result = Stp._cmp_value(rcv_priority.root_id.value, + my_priority.root_id.value) + if not result: + result = Stp.compare_root_path( + rcv_priority.root_path_cost, + my_priority.root_path_cost, + rcv_priority.designated_bridge_id.value, + my_priority.designated_bridge_id.value, + rcv_priority.designated_port_id.value, + my_priority.designated_port_id.value) + if not result: + result1 = Stp._cmp_value( + rcv_priority.designated_bridge_id.value, + my_priority.designated_bridge_id.mac_addr) + result2 = Stp._cmp_value( + rcv_priority.designated_port_id.value, + my_priority.designated_port_id.port_no) + if not result1 and not result2: + result = SUPERIOR + else: + result = Stp._cmp_obj(rcv_times, my_times) + return result + + @staticmethod + def _cmp_value(value1, value2): + result = cmp(value1, value2) + if result < 0: + return SUPERIOR + elif result == 0: + return REPEATED + else: + return INFERIOR + + @staticmethod + def _cmp_obj(obj1, obj2): + for key in obj1.__dict__.keys(): + if (not hasattr(obj2, key) + or getattr(obj1, key) != getattr(obj2, key)): + return SUPERIOR + return REPEATED + + +class Bridge(object): + _DEFAULT_VALUE = {'priority': bpdu.DEFAULT_BRIDGE_PRIORITY, + 'sys_ext_id': 0, + 'max_age': bpdu.DEFAULT_MAX_AGE, + 'hello_time': bpdu.DEFAULT_HELLO_TIME, + 'fwd_delay': bpdu.DEFAULT_FORWARD_DELAY} + + def __init__(self, dp, logger, config, send_ev_func): + super(Bridge, self).__init__() + self.dp = dp + self.logger = logger + self.dpid_str = {'dpid': dpid_to_str(dp.id)} + self.send_event = send_ev_func + + # Bridge data + bridge_conf = config.get('bridge', {}) + values = self._DEFAULT_VALUE + for key, value in bridge_conf.items(): + values[key] = value + system_id = dp.ports.values()[0].hw_addr + + self.bridge_id = BridgeId(values['priority'], + values['sys_ext_id'], + system_id) + self.bridge_times = Times(0, # message_age + values['max_age'], + values['hello_time'], + values['fwd_delay']) + # Root bridge data + self.root_priority = Priority(self.bridge_id, 0, None, None) + self.root_times = self.bridge_times + # Ports + self.ports = {} + self.ports_conf = config.get('ports', {}) + for ofport in dp.ports.values(): + self.port_add(ofport) + + @property + def is_root_bridge(self): + return bool(self.bridge_id.value == self.root_priority.root_id.value) + + def delete(self): + for port in self.ports.values(): + port.delete() + + def port_add(self, ofport): + if ofport.port_no <= MAX_PORT_NO: + port_conf = self.ports_conf.get(ofport.port_no, {}) + self.ports[ofport.port_no] = Port(self.dp, self.logger, + port_conf, self.send_event, + self.recalculate_spanning_tree, + self.topology_change_notify, + self.bridge_id, + self.bridge_times, + ofport) + + def port_delete(self, port_no): + self.link_down(port_no) + self.ports[port_no].delete() + del self.ports[port_no] + + def link_up(self, port_no): + port = self.ports[port_no] + port.up(DESIGNATED_PORT, self.root_priority, self.root_times) + + def link_down(self, port_no): + """ DESIGNATED_PORT/NON_DESIGNATED_PORT: change status to DISABLE. + ROOT_PORT: change status to DISABLE and recalculate STP. """ + port = self.ports[port_no] + init_stp_flg = bool(port.role is ROOT_PORT) + + port.down(PORT_STATE_DISABLE, msg_init=True) + if init_stp_flg: + self.recalculate_spanning_tree() + + def packet_in_handler(self, msg): + if not msg.in_port in self.ports: + return + + pkt = packet.Packet(msg.data) + in_port = self.ports[msg.in_port] + + if bpdu.ConfigurationBPDUs in pkt: + """ Receive Configuration BPDU. + - If receive superior BPDU: + re-caluculation of spanning tree. + - If receive Topology Change BPDU: + throw EventTopologyChange. + forward Topology Change BPDU. """ + (bpdu_pkt, ) = pkt.get_protocols(bpdu.ConfigurationBPDUs) + if bpdu_pkt.message_age > bpdu_pkt.max_age: + log_msg = 'Drop BPDU packet which message_age exceeded.' + self.logger.debug(log_msg, extra=self.dpid_str) + return + + rcv_info, rcv_tc = in_port.rcv_config_bpdu(bpdu_pkt) + + if rcv_info is SUPERIOR: + self.logger.info('[port=%d] Receive superior BPDU.', + msg.in_port, extra=self.dpid_str) + self.recalculate_spanning_tree(init=False) + + elif rcv_tc: + self.send_event(EventTopologyChange(self.dp)) + + if in_port.role is ROOT_PORT: + self._forward_tc_bpdu(rcv_tc) + + elif bpdu.TopologyChangeNotificationBPDUs in pkt: + """ Receive Topology Change Notification BPDU. + send Topology Change Ack BPDU. + throw EventTopologyChange. + - Root bridge: + send Topology Change BPDU from all port. + - Non root bridge: + send Topology Change Notification BPDU to root bridge. """ + in_port.transmit_ack_bpdu() + self.topology_change_notify(None) + + elif bpdu.RstBPDUs in pkt: + """ Receive Rst BPDU. """ + #TODO: RSTP + pass + + else: + """ Receive non BPDU packet. + throw EventPacketIn. """ + self.send_event(EventPacketIn(msg)) + + def recalculate_spanning_tree(self, init=True): + """ Re-calculation of spanning tree. """ + # All port down. + for port in self.ports.values(): + if port.state is not PORT_STATE_DISABLE: + port.down(PORT_STATE_BLOCK, msg_init=init) + + # Send topology change event. + if init: + self.send_event(EventTopologyChange(self.dp)) + + # Update tree roles. + port_roles = {} + self.root_priority = Priority(self.bridge_id, 0, None, None) + self.root_times = self.bridge_times + + if init: + self.logger.info('Root bridge.', extra=self.dpid_str) + for port_no in self.ports.keys(): + port_roles[port_no] = DESIGNATED_PORT + else: + (port_roles, + self.root_priority, + self.root_times) = self._spanning_tree_algorithm() + + # All port up. + for port_no, role in port_roles.items(): + if self.ports[port_no].state is not PORT_STATE_DISABLE: + self.ports[port_no].up(role, self.root_priority, + self.root_times) + + def _spanning_tree_algorithm(self): + """ Update tree roles. + - Root bridge: + all port is DESIGNATED_PORT. + - Non root bridge: + select one ROOT_PORT and some DESIGNATED_PORT, + and the other port is set to NON_DESIGNATED_PORT.""" + port_roles = {} + + root_port = self._select_root_port() + + if root_port is None: + # My bridge is a root bridge. + self.logger.info('Root bridge.', extra=self.dpid_str) + root_priority = self.root_priority + root_times = self.root_times + + for port_no in self.ports.keys(): + if self.ports[port_no].state is not PORT_STATE_DISABLE: + port_roles[port_no] = DESIGNATED_PORT + else: + # Other bridge is a root bridge. + self.logger.info('Non root bridge.', extra=self.dpid_str) + root_priority = root_port.designated_priority + root_times = root_port.designated_times + + port_roles[root_port.ofport.port_no] = ROOT_PORT + + d_ports = self._select_designated_port(root_port) + for port_no in d_ports: + port_roles[port_no] = DESIGNATED_PORT + + for port in self.ports.values(): + if port.state is not PORT_STATE_DISABLE: + port_roles.setdefault(port.ofport.port_no, + NON_DESIGNATED_PORT) + + return port_roles, root_priority, root_times + + def _select_root_port(self): + """ ROOT_PORT is the nearest port to a root bridge. + It is determined by the cost of path, etc. """ + root_port = None + + for port in self.ports.values(): + root_msg = (self.root_priority if root_port is None + else root_port.designated_priority) + port_msg = port.designated_priority + if port.state is PORT_STATE_DISABLE or port_msg is None: + continue + if root_msg.root_id.value > port_msg.root_id.value: + result = SUPERIOR + elif root_msg.root_id.value == port_msg.root_id.value: + if root_msg.designated_bridge_id is None: + result = INFERIOR + else: + result = Stp.compare_root_path( + port_msg.root_path_cost, + root_msg.root_path_cost, + port_msg.designated_bridge_id.value, + root_msg.designated_bridge_id.value, + port_msg.designated_port_id.value, + root_msg.designated_port_id.value) + else: + result = INFERIOR + + if result is SUPERIOR: + root_port = port + + return root_port + + def _select_designated_port(self, root_port): + """ DESIGNATED_PORT is a port of the side near the root bridge + of each link. It is determined by the cost of each path, etc + same as ROOT_PORT. """ + d_ports = [] + root_msg = root_port.designated_priority + + for port in self.ports.values(): + port_msg = port.designated_priority + if (port.state is PORT_STATE_DISABLE + or port.ofport.port_no == root_port.ofport.port_no): + continue + if (port_msg is None or + (port_msg.root_id.value != root_msg.root_id.value)): + d_ports.append(port.ofport.port_no) + else: + result = Stp.compare_root_path( + root_msg.root_path_cost, + port_msg.root_path_cost - port.path_cost, + self.bridge_id.value, + port_msg.designated_bridge_id.value, + port.port_id.value, + port_msg.designated_port_id.value) + if result is SUPERIOR: + d_ports.append(port.ofport.port_no) + + return d_ports + + def topology_change_notify(self, port_state): + notice = False + if port_state is PORT_STATE_FORWARD: + for port in self.ports.values(): + if port.role is DESIGNATED_PORT: + notice = True + break + else: + notice = True + + if notice: + self.send_event(EventTopologyChange(self.dp)) + if self.is_root_bridge: + self._transmit_tc_bpdu() + else: + self._transmit_tcn_bpdu() + + def _transmit_tc_bpdu(self): + for port in self.ports.values(): + port.transmit_tc_bpdu() + + def _transmit_tcn_bpdu(self): + root_port = None + for port in self.ports.values(): + if port.role is ROOT_PORT: + root_port = port + break + if root_port: + root_port.transmit_tcn_bpdu() + + def _forward_tc_bpdu(self, fwd_flg): + for port in self.ports.values(): + port.send_tc_flg = fwd_flg + + +class Port(object): + _DEFAULT_VALUE = {'priority': bpdu.DEFAULT_PORT_PRIORITY, + 'path_cost': bpdu.PORT_PATH_COST_10MB, + 'enable': True} + + _PATH_COST = {ofproto_v1_0.OFPPF_10MB_HD: bpdu.PORT_PATH_COST_10MB, + ofproto_v1_0.OFPPF_10MB_FD: bpdu.PORT_PATH_COST_10MB, + ofproto_v1_0.OFPPF_100MB_HD: bpdu.PORT_PATH_COST_100MB, + ofproto_v1_0.OFPPF_100MB_FD: bpdu.PORT_PATH_COST_100MB, + ofproto_v1_0.OFPPF_1GB_HD: bpdu.PORT_PATH_COST_1GB, + ofproto_v1_0.OFPPF_1GB_FD: bpdu.PORT_PATH_COST_1GB, + ofproto_v1_0.OFPPF_10GB_FD: bpdu.PORT_PATH_COST_10GB} + + def __init__(self, dp, logger, config, send_ev_func, timeout_func, + topology_change_func, bridge_id, bridge_times, ofport): + super(Port, self).__init__() + self.dp = dp + self.logger = logger + self.dpid_str = {'dpid': dpid_to_str(dp.id)} + self.config_enable = config.get('enable', + self._DEFAULT_VALUE['enable']) + self.send_event = send_ev_func + self.wait_bpdu_timeout = timeout_func + self.topology_change_notify = topology_change_func + self.ofctl = OfCtl_v1_0(dp) + + # Bridge data + self.bridge_id = bridge_id + # Root bridge data + self.port_priority = None + self.port_times = None + # ofproto_v1_0_parser.OFPPhyPort data + self.ofport = ofport + # Port data + values = self._DEFAULT_VALUE + for rate in sorted(self._PATH_COST.keys(), reverse=True): + if ofport.curr & rate: + values['path_cost'] = self._PATH_COST[rate] + break + for key, value in values.items(): + values[key] = value + self.port_id = PortId(values['priority'], ofport.port_no) + self.path_cost = values['path_cost'] + self.state = (None if self.config_enable else PORT_STATE_DISABLE) + self.role = None + # Receive BPDU data + self.designated_priority = None + self.designated_times = None + # BPDU handling threads + self.send_bpdu_thread = PortThread(self._transmit_config_bpdu) + self.wait_bpdu_thread = PortThread(self._wait_bpdu_timer) + self.send_tc_thread = PortThread(self._transmit_tc_bpdu) + self.send_tcn_thread = PortThread(self._transmit_tcn_bpdu) + self.send_tc_flg = None + self.send_tcn_flg = None + self.wait_timer_event = None + # State machine thread + self.state_machine = PortThread(self._state_machine) + self.state_event = None + + self.up(DESIGNATED_PORT, + Priority(bridge_id, 0, None, None), + bridge_times) + + self.state_machine.start() + self.logger.debug('[port=%d] Start port state machine.', + self.ofport.port_no, extra=self.dpid_str) + + def delete(self): + self.state_machine.stop() + self.send_bpdu_thread.stop() + self.wait_bpdu_thread.stop() + self.send_tc_thread.stop() + self.send_tcn_thread.stop() + if self.state_event is not None: + self.state_event.set() + self.state_event = None + if self.wait_timer_event is not None: + self.wait_timer_event.set() + self.wait_timer_event = None + self.logger.debug('[port=%d] Stop port threads.', + self.ofport.port_no, extra=self.dpid_str) + + def up(self, role, root_priority, root_times): + """ A port is started in the state of LISTEN. """ + self.port_priority = root_priority + self.port_times = root_times + + state = (PORT_STATE_LISTEN if self.config_enable + else PORT_STATE_DISABLE) + self._change_role(role) + self._change_status(state) + + def down(self, state, msg_init=False): + """ A port will be in the state of DISABLE or BLOCK, + and be stopped. """ + assert (state is PORT_STATE_DISABLE + or state is PORT_STATE_BLOCK) + if not self.config_enable: + return + + if msg_init: + self.designated_priority = None + self.designated_times = None + + self._change_role(DESIGNATED_PORT) + self._change_status(state) + + def _state_machine(self): + """ Port state machine. + Change next status when timer is exceeded + or _change_status() method is called.""" + role_str = {ROOT_PORT: 'ROOT_PORT ', + DESIGNATED_PORT: 'DESIGNATED_PORT ', + NON_DESIGNATED_PORT: 'NON_DESIGNATED_PORT'} + state_str = {PORT_STATE_DISABLE: 'DISABLE', + PORT_STATE_BLOCK: 'BLOCK', + PORT_STATE_LISTEN: 'LISTEN', + PORT_STATE_LEARN: 'LEARN', + PORT_STATE_FORWARD: 'FORWARD'} + + if self.state is PORT_STATE_DISABLE: + self.ofctl.set_port_status(self.ofport, self.state) + + while True: + self.logger.info('[port=%d] %s / %s', self.ofport.port_no, + role_str[self.role], state_str[self.state], + extra=self.dpid_str) + + self.state_event = hub.Event() + timer = self._get_timer() + if timer: + timeout = hub.Timeout(timer) + try: + self.state_event.wait() + except hub.Timeout as t: + if t is not timeout: + err_msg = 'Internal error. Not my timeout.' + raise RyuException(msg=err_msg) + new_state = self._get_next_state() + self._change_status(new_state, thread_switch=False) + finally: + timeout.cancel() + else: + self.state_event.wait() + + self.state_event = None + + def _get_timer(self): + timer = {PORT_STATE_DISABLE: None, + PORT_STATE_BLOCK: None, + PORT_STATE_LISTEN: self.port_times.forward_delay, + PORT_STATE_LEARN: self.port_times.forward_delay, + PORT_STATE_FORWARD: None} + return timer[self.state] + + def _get_next_state(self): + next_state = {PORT_STATE_DISABLE: None, + PORT_STATE_BLOCK: None, + PORT_STATE_LISTEN: PORT_STATE_LEARN, + PORT_STATE_LEARN: (PORT_STATE_FORWARD + if (self.role is ROOT_PORT or + self.role is DESIGNATED_PORT) + else PORT_STATE_BLOCK), + PORT_STATE_FORWARD: None} + return next_state[self.state] + + def _change_status(self, new_state, thread_switch=True): + if new_state is not PORT_STATE_DISABLE: + self.ofctl.set_port_status(self.ofport, new_state) + + if(new_state is PORT_STATE_FORWARD or + (self.state is PORT_STATE_FORWARD and + (new_state is PORT_STATE_DISABLE or + new_state is PORT_STATE_BLOCK))): + self.topology_change_notify(new_state) + + if (new_state is PORT_STATE_DISABLE + or new_state is PORT_STATE_BLOCK): + self.send_tc_flg = False + self.send_tcn_flg = False + self.send_bpdu_thread.stop() + self.send_tc_thread.stop() + self.send_tcn_thread.stop() + elif new_state is PORT_STATE_LISTEN: + self.send_bpdu_thread.start() + + self.state = new_state + self.send_event(EventPortStateChange(self.dp, self)) + + if self.state_event is not None: + self.state_event.set() + self.state_event = None + if thread_switch: + hub.sleep(0) # For thread switching. + + def _change_role(self, new_role): + if self.role is new_role: + return + self.role = new_role + if (new_role is ROOT_PORT + or new_role is NON_DESIGNATED_PORT): + self.wait_bpdu_thread.start() + else: + assert new_role is DESIGNATED_PORT + self.wait_bpdu_thread.stop() + + def rcv_config_bpdu(self, bpdu_pkt): + # Check received BPDU is superior to currently held BPDU. + root_id = BridgeId(bpdu_pkt.root_priority, + bpdu_pkt.root_system_id_extension, + bpdu_pkt.root_mac_address) + root_path_cost = bpdu_pkt.root_path_cost + designated_bridge_id = BridgeId(bpdu_pkt.bridge_priority, + bpdu_pkt.bridge_system_id_extension, + bpdu_pkt.bridge_mac_address) + designated_port_id = PortId(bpdu_pkt.port_priority, + bpdu_pkt.port_number) + + msg_priority = Priority(root_id, root_path_cost, + designated_bridge_id, + designated_port_id) + msg_times = Times(bpdu_pkt.message_age, + bpdu_pkt.max_age, + bpdu_pkt.hello_time, + bpdu_pkt.forward_delay) + + rcv_info = Stp.compare_bpdu_info(self.designated_priority, + self.designated_times, + msg_priority, msg_times) + if rcv_info is SUPERIOR: + self.designated_priority = msg_priority + self.designated_times = msg_times + + chk_flg = False + if ((rcv_info is SUPERIOR or rcv_info is REPEATED) + and (self.role is ROOT_PORT + or self.role is NON_DESIGNATED_PORT)): + self._update_wait_bpdu_timer() + chk_flg = True + elif(rcv_info is INFERIOR and self.role is DESIGNATED_PORT): + chk_flg = True + + # Check TopologyChange flag. + rcv_tc = False + if chk_flg: + tc_flag_mask = 0b00000001 + tcack_flag_mask = 0b10000000 + if bpdu_pkt.flags & tc_flag_mask: + self.logger.debug('[port=%d] receive TopologyChange BPDU.', + self.ofport.port_no, extra=self.dpid_str) + rcv_tc = True + if bpdu_pkt.flags & tcack_flag_mask: + self.logger.debug('[port=%d] receive TopologyChangeAck BPDU.', + self.ofport.port_no, extra=self.dpid_str) + if self.send_tcn_flg: + self.send_tcn_flg = False + + return rcv_info, rcv_tc + + def _update_wait_bpdu_timer(self): + if self.wait_timer_event is not None: + self.wait_timer_event.set() + self.wait_timer_event = None + hub.sleep(0) # For thread switching. + + def _wait_bpdu_timer(self): + time_exceed = False + + while True: + self.wait_timer_event = hub.Event() + message_age = (self.designated_times.message_age + if self.designated_times else 0) + timer = self.port_times.max_age - message_age + timeout = hub.Timeout(timer) + try: + self.wait_timer_event.wait() + except hub.Timeout as t: + if t is not timeout: + err_msg = 'Internal error. Not my timeout.' + raise RyuException(msg=err_msg) + self.logger.info('[port=%d] Wait BPDU timer is exceeded.', + self.ofport.port_no, extra=self.dpid_str) + time_exceed = True + finally: + timeout.cancel() + self.wait_timer_event = None + + if time_exceed: + break + + if time_exceed: # Bridge.recalculate_spanning_tree + hub.spawn(self.wait_bpdu_timeout) + + def _transmit_config_bpdu(self): + """ Send config BPDU packet if port role is DESIGNATED_PORT. """ + while True: + if self.role == DESIGNATED_PORT: + flags = 0b00000000 + log_msg = '[port=%d] Send Config BPDU.' + if self.send_tc_flg: + flags = 0b00000001 + log_msg = '[port=%d] Send TopologyChange BPDU.' + bpdu_data = self._generate_config_bpdu(flags) + self.ofctl.send_packet_out(self.ofport.port_no, bpdu_data) + self.logger.debug(log_msg, self.ofport.port_no, + extra=self.dpid_str) + hub.sleep(self.port_times.hello_time) + + def transmit_tc_bpdu(self): + self.send_tc_thread.start() + + def _transmit_tc_bpdu(self): + """ Set send_tc_flg to send Topology Change BPDU. """ + timer = self.port_times.max_age + self.port_times.forward_delay + + self.send_tc_flg = True + hub.sleep(timer) + self.send_tc_flg = False + + def transmit_ack_bpdu(self): + """ Send Topology Change Ack BPDU. """ + ack_flags = 0b10000001 + bpdu_data = self._generate_config_bpdu(ack_flags) + self.ofctl.send_packet_out(self.ofport.port_no, bpdu_data) + + def transmit_tcn_bpdu(self): + self.send_tcn_thread.start() + + def _transmit_tcn_bpdu(self): + """ Send Topology Change Notification BPDU until receive Ack. """ + self.send_tcn_flg = True + local_hello_time = bpdu.DEFAULT_HELLO_TIME + while self.send_tcn_flg: + bpdu_data = self._generate_tcn_bpdu() + self.ofctl.send_packet_out(self.ofport.port_no, bpdu_data) + self.logger.debug('[port=%d] Send TopologyChangeNotify BPDU.', + self.ofport.port_no, extra=self.dpid_str) + hub.sleep(local_hello_time) + + def _generate_config_bpdu(self, flags): + src_mac = self.ofport.hw_addr + dst_mac = bpdu.BRIDGE_GROUP_ADDRESS + length = (bpdu.bpdu._PACK_LEN + bpdu.ConfigurationBPDUs.PACK_LEN + + llc.llc._PACK_LEN + llc.ControlFormatU._PACK_LEN) + + e = ethernet.ethernet(dst_mac, src_mac, length) + l = llc.llc(llc.SAP_BPDU, llc.SAP_BPDU, llc.ControlFormatU()) + b = bpdu.ConfigurationBPDUs( + flags=flags, + root_priority=self.port_priority.root_id.priority, + root_mac_address=self.port_priority.root_id.mac_addr, + root_path_cost=self.port_priority.root_path_cost+self.path_cost, + bridge_priority=self.bridge_id.priority, + bridge_mac_address=self.bridge_id.mac_addr, + port_priority=self.port_id.priority, + port_number=self.ofport.port_no, + message_age=self.port_times.message_age+1, + max_age=self.port_times.max_age, + hello_time=self.port_times.hello_time, + forward_delay=self.port_times.forward_delay) + + pkt = packet.Packet() + pkt.add_protocol(e) + pkt.add_protocol(l) + pkt.add_protocol(b) + pkt.serialize() + + return pkt.data + + def _generate_tcn_bpdu(self): + src_mac = self.ofport.hw_addr + dst_mac = bpdu.BRIDGE_GROUP_ADDRESS + length = (bpdu.bpdu._PACK_LEN + + bpdu.TopologyChangeNotificationBPDUs.PACK_LEN + + llc.llc._PACK_LEN + llc.ControlFormatU._PACK_LEN) + + e = ethernet.ethernet(dst_mac, src_mac, length) + l = llc.llc(llc.SAP_BPDU, llc.SAP_BPDU, llc.ControlFormatU()) + b = bpdu.TopologyChangeNotificationBPDUs() + + pkt = packet.Packet() + pkt.add_protocol(e) + pkt.add_protocol(l) + pkt.add_protocol(b) + pkt.serialize() + + return pkt.data + + +class PortThread(object): + def __init__(self, function): + super(PortThread, self).__init__() + self.function = function + self.thread = None + + def start(self): + self.stop() + self.thread = hub.spawn(self.function) + + def stop(self): + if self.thread is not None: + hub.kill(self.thread) + hub.joinall([self.thread]) + self.thread = None + + +class BridgeId(object): + def __init__(self, priority, system_id_extension, mac_addr): + super(BridgeId, self).__init__() + self.priority = priority + self.system_id_extension = system_id_extension + self.mac_addr = mac_addr + self.value = bpdu.ConfigurationBPDUs.encode_bridge_id( + priority, system_id_extension, mac_addr) + + +class PortId(object): + def __init__(self, priority, port_no): + super(PortId, self).__init__() + self.priority = priority + self.port_no = port_no + self.value = bpdu.ConfigurationBPDUs.encode_port_id(priority, port_no) + + +class Priority(object): + def __init__(self, root_id, root_path_cost, + designated_bridge_id, designated_port_id): + super(Priority, self).__init__() + self.root_id = root_id + self.root_path_cost = root_path_cost + self.designated_bridge_id = designated_bridge_id + self.designated_port_id = designated_port_id + + +class Times(object): + def __init__(self, message_age, max_age, hello_time, forward_delay): + super(Times, self).__init__() + self.message_age = message_age + self.max_age = max_age + self.hello_time = hello_time + self.forward_delay = forward_delay + + +class OfCtl_v1_0(object): + def __init__(self, dp): + super(OfCtl_v1_0, self).__init__() + self.dp = dp + + def send_packet_out(self, out_port, data): + actions = [self.dp.ofproto_parser.OFPActionOutput(out_port, 0)] + self.dp.send_packet_out(buffer_id=self.dp.ofproto.OFP_NO_BUFFER, + in_port=self.dp.ofproto.OFPP_CONTROLLER, + actions=actions, data=data) + + def set_port_status(self, port, config): + ofproto_parser = self.dp.ofproto_parser + mask = 0b1111111 + msg = ofproto_parser.OFPPortMod(self.dp, port.port_no, port.hw_addr, + config, mask, port.advertised) + self.dp.send_msg(msg)