# Copyright (C) 2011, 2012 Nippon Telegraph and Telephone Corporation. # Copyright (C) 2011, 2012 Isaku Yamahata # # 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. """ Basic OpenFlow handling including negotiation. """ import itertools import logging import warnings import os_ken.base.app_manager from os_ken.lib import hub from os_ken import utils from os_ken.controller import ofp_event from os_ken.controller.controller import OpenFlowController from os_ken.controller.handler import set_ev_handler from os_ken.controller.handler import HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER,\ MAIN_DISPATCHER from os_ken.ofproto import ofproto_parser # The state transition: HANDSHAKE -> CONFIG -> MAIN # # HANDSHAKE: if it receives HELLO message with the valid OFP version, # sends Features Request message, and moves to CONFIG. # # CONFIG: it receives Features Reply message and moves to MAIN # # MAIN: it does nothing. Applications are expected to register their # own handlers. # # Note that at any state, when we receive Echo Request message, send # back Echo Reply message. class OFPHandler(os_ken.base.app_manager.OSKenApp): def __init__(self, *args, **kwargs): super(OFPHandler, self).__init__(*args, **kwargs) self.name = ofp_event.NAME self.controller = None def start(self): super(OFPHandler, self).start() self.controller = OpenFlowController() return hub.spawn(self.controller) def _hello_failed(self, datapath, error_desc): self.logger.error('%s on datapath %s', error_desc, datapath.address) error_msg = datapath.ofproto_parser.OFPErrorMsg( datapath=datapath, type_=datapath.ofproto.OFPET_HELLO_FAILED, code=datapath.ofproto.OFPHFC_INCOMPATIBLE, data=error_desc) datapath.send_msg(error_msg, close_socket=True) @set_ev_handler(ofp_event.EventOFPHello, HANDSHAKE_DISPATCHER) def hello_handler(self, ev): self.logger.debug('hello ev %s', ev) msg = ev.msg datapath = msg.datapath # check if received version is supported. # pre 1.0 is not supported elements = getattr(msg, 'elements', None) if elements: switch_versions = set() for version in itertools.chain.from_iterable( element.versions for element in elements): switch_versions.add(version) usable_versions = switch_versions & set( datapath.supported_ofp_version) # We didn't send our supported versions for interoperability as # most switches would not understand elements at the moment. # So the switch would think that the negotiated version would # be max(negotiated_versions), but actual usable version is # max(usable_versions). negotiated_versions = set( version for version in switch_versions if version <= max(datapath.supported_ofp_version)) if negotiated_versions and not usable_versions: # e.g. # versions of OF 1.0 and 1.1 from switch # max of OF 1.2 from OSKen and supported_ofp_version = (1.2, ) # negotiated version = 1.1 # usable version = None error_desc = ( 'no compatible version found: ' 'switch versions %s controller version 0x%x, ' 'the negotiated version is 0x%x, ' 'but no usable version found. ' 'If possible, set the switch to use one of OF version %s' % (switch_versions, max(datapath.supported_ofp_version), max(negotiated_versions), sorted(datapath.supported_ofp_version))) self._hello_failed(datapath, error_desc) return if (negotiated_versions and usable_versions and max(negotiated_versions) != max(usable_versions)): # e.g. # versions of OF 1.0 and 1.1 from switch # max of OF 1.2 from OSKen and supported_ofp_version = (1.0, 1.2) # negotiated version = 1.1 # usable version = 1.0 # # TODO: In order to get the version 1.0, OSKen need to send # supported verions. error_desc = ( 'no compatible version found: ' 'switch versions 0x%x controller version 0x%x, ' 'the negotiated version is %s but found usable %s. ' 'If possible, ' 'set the switch to use one of OF version %s' % ( max(switch_versions), max(datapath.supported_ofp_version), sorted(negotiated_versions), sorted(usable_versions), sorted(usable_versions))) self._hello_failed(datapath, error_desc) return else: usable_versions = set(version for version in datapath.supported_ofp_version if version <= msg.version) if (usable_versions and max(usable_versions) != min(msg.version, datapath.ofproto.OFP_VERSION)): # The version of min(msg.version, datapath.ofproto.OFP_VERSION) # should be used according to the spec. But we can't. # So log it and use max(usable_versions) with the hope that # the switch is able to understand lower version. # e.g. # OF 1.1 from switch # OF 1.2 from OSKen and supported_ofp_version = (1.0, 1.2) # In this case, 1.1 should be used according to the spec, # but 1.1 can't be used. # # OF1.3.1 6.3.1 # Upon receipt of this message, the recipient must # calculate the OpenFlow protocol version to be used. If # both the Hello message sent and the Hello message # received contained a OFPHET_VERSIONBITMAP hello element, # and if those bitmaps have some common bits set, the # negotiated version must be the highest version set in # both bitmaps. Otherwise, the negotiated version must be # the smaller of the version number that was sent and the # one that was received in the version fields. If the # negotiated version is supported by the recipient, then # the connection proceeds. Otherwise, the recipient must # reply with an OFPT_ERROR message with a type field of # OFPET_HELLO_FAILED, a code field of OFPHFC_INCOMPATIBLE, # and optionally an ASCII string explaining the situation # in data, and then terminate the connection. version = max(usable_versions) error_desc = ( 'no compatible version found: ' 'switch 0x%x controller 0x%x, but found usable 0x%x. ' 'If possible, set the switch to use OF version 0x%x' % ( msg.version, datapath.ofproto.OFP_VERSION, version, version)) self._hello_failed(datapath, error_desc) return if not usable_versions: error_desc = ( 'unsupported version 0x%x. ' 'If possible, set the switch to use one of the versions %s' % ( msg.version, sorted(datapath.supported_ofp_version))) self._hello_failed(datapath, error_desc) return datapath.set_version(max(usable_versions)) # Move on to config state self.logger.debug('move onto config mode') datapath.set_state(CONFIG_DISPATCHER) # Finally, send feature request features_request = datapath.ofproto_parser.OFPFeaturesRequest(datapath) datapath.send_msg(features_request) @set_ev_handler(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) def switch_features_handler(self, ev): msg = ev.msg datapath = msg.datapath self.logger.debug('switch features ev %s', msg) datapath.id = msg.datapath_id # hacky workaround, will be removed. OF1.3 doesn't have # ports. An application should not depend on them. But there # might be such bad applications so keep this workaround for # while. if datapath.ofproto.OFP_VERSION < 0x04: datapath.ports = msg.ports else: datapath.ports = {} if datapath.ofproto.OFP_VERSION < 0x04: self.logger.debug('move onto main mode') ev.msg.datapath.set_state(MAIN_DISPATCHER) else: port_desc = datapath.ofproto_parser.OFPPortDescStatsRequest( datapath, 0) datapath.send_msg(port_desc) @set_ev_handler(ofp_event.EventOFPPortDescStatsReply, CONFIG_DISPATCHER) def multipart_reply_handler(self, ev): msg = ev.msg datapath = msg.datapath with warnings.catch_warnings(): warnings.simplefilter('ignore') for port in msg.body: datapath.ports[port.port_no] = port if msg.flags & datapath.ofproto.OFPMPF_REPLY_MORE: return self.logger.debug('move onto main mode') ev.msg.datapath.set_state(MAIN_DISPATCHER) @set_ev_handler(ofp_event.EventOFPEchoRequest, [HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER, MAIN_DISPATCHER]) def echo_request_handler(self, ev): msg = ev.msg datapath = msg.datapath echo_reply = datapath.ofproto_parser.OFPEchoReply(datapath) echo_reply.xid = msg.xid echo_reply.data = msg.data datapath.send_msg(echo_reply) @set_ev_handler(ofp_event.EventOFPEchoReply, [HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER, MAIN_DISPATCHER]) def echo_reply_handler(self, ev): msg = ev.msg datapath = msg.datapath datapath.acknowledge_echo_reply(msg.xid) @set_ev_handler(ofp_event.EventOFPPortStatus, MAIN_DISPATCHER) def port_status_handler(self, ev): msg = ev.msg datapath = msg.datapath ofproto = datapath.ofproto if msg.reason in [ofproto.OFPPR_ADD, ofproto.OFPPR_MODIFY]: datapath.ports[msg.desc.port_no] = msg.desc elif msg.reason == ofproto.OFPPR_DELETE: datapath.ports.pop(msg.desc.port_no, None) else: return self.send_event_to_observers( ofp_event.EventOFPPortStateChange( datapath, msg.reason, msg.desc.port_no), datapath.state) @set_ev_handler(ofp_event.EventOFPErrorMsg, [HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER, MAIN_DISPATCHER]) def error_msg_handler(self, ev): msg = ev.msg ofp = msg.datapath.ofproto self.logger.debug( "EventOFPErrorMsg received.\n" "version=%s, msg_type=%s, msg_len=%s, xid=%s\n" " `-- msg_type: %s", hex(msg.version), hex(msg.msg_type), hex(msg.msg_len), hex(msg.xid), ofp.ofp_msg_type_to_str(msg.msg_type)) if msg.type == ofp.OFPET_EXPERIMENTER: self.logger.debug( "OFPErrorExperimenterMsg(type=%s, exp_type=%s," " experimenter=%s, data=b'%s')", hex(msg.type), hex(msg.exp_type), hex(msg.experimenter), utils.binary_str(msg.data)) else: self.logger.debug( "OFPErrorMsg(type=%s, code=%s, data=b'%s')\n" " |-- type: %s\n" " |-- code: %s", hex(msg.type), hex(msg.code), utils.binary_str(msg.data), ofp.ofp_error_type_to_str(msg.type), ofp.ofp_error_code_to_str(msg.type, msg.code)) if msg.type == ofp.OFPET_HELLO_FAILED: self.logger.debug( " `-- data: %s", msg.data.decode('ascii')) elif len(msg.data) >= ofp.OFP_HEADER_SIZE: (version, msg_type, msg_len, xid) = ofproto_parser.header(msg.data) self.logger.debug( " `-- data: version=%s, msg_type=%s, msg_len=%s, xid=%s\n" " `-- msg_type: %s", hex(version), hex(msg_type), hex(msg_len), hex(xid), ofp.ofp_msg_type_to_str(msg_type)) else: self.logger.warning( "The data field sent from the switch is too short: " "len(msg.data) < OFP_HEADER_SIZE\n" "The OpenFlow Spec says that the data field should contain " "at least 64 bytes of the failed request.\n" "Please check the settings or implementation of your switch.")