From 6c6006528a473fff882151806251aa36c5fbf5b8 Mon Sep 17 00:00:00 2001 From: mirabal Date: Thu, 16 Mar 2017 17:22:57 +0100 Subject: [PATCH] Openflow controller abstract connector - Add openflow_conn abstract impletation for all openflow connectors - Refactor all existing conenctor to Inherit la clase abstracta - Now all of connector raise an exeption in case of faliure - As OF_connector raise an expection, all code that make use of this class now capture the execption. - Add to ofc DB table last_error and status column - Check for each operation if an error exist an update DB ofc status and last error column info Change-Id: Ia3d3bf63fee79dd18d61aeeb08a983dfcb88b729 Signed-off-by: mirabal --- ODL.py | 311 +++++++++++-------- database_utils/migrate_vim_db.sh | 17 ++ floodlight.py | 504 ++++++++++++++++--------------- onos.py | 225 +++++++------- openflow | 150 +++++---- openflow_conn.py | 223 ++++++++++++++ openflow_thread.py | 254 ++++++++-------- openvim | 2 +- ovim.py | 6 +- 9 files changed, 1031 insertions(+), 661 deletions(-) create mode 100644 openflow_conn.py diff --git a/ODL.py b/ODL.py index 1c8fe44..588409e 100644 --- a/ODL.py +++ b/ODL.py @@ -22,11 +22,11 @@ # contact with: nfvlabs@tid.es ## -''' +""" Implement the plugging for OpendayLight openflow controller It creates the class OF_conn to create dataplane connections with static rules based on packet destination MAC address -''' +""" __author__="Pablo Montes, Alfonso Tierno" __date__ ="$28-oct-2014 12:07:15$" @@ -36,11 +36,14 @@ import json import requests import base64 import logging +import openflow_conn + + +class OF_conn(openflow_conn.OpenflowConn): + """OpenDayLight connector. No MAC learning is used""" -class OF_conn(): - '''OpenDayLight connector. No MAC learning is used''' def __init__(self, params): - ''' Constructor. + """ Constructor. Params: dictionary with the following keys: of_dpid: DPID to use for this controller of_ip: controller IP address @@ -50,15 +53,16 @@ class OF_conn(): of_debug: debug level for logging. Default to ERROR other keys are ignored Raise an exception if same parameter is missing or wrong - ''' - #check params + """ + + # check params if "of_ip" not in params or params["of_ip"]==None or "of_port" not in params or params["of_port"]==None: raise ValueError("IP address and port must be provided") - #internal variables + + openflow_conn.OpenflowConn.__init__(self, params) + # internal variables self.name = "OpenDayLight" - self.headers = {'content-type':'application/json', - 'Accept':'application/json' - } + self.headers = {'content-type': 'application/json', 'Accept': 'application/json'} self.auth=None self.pp2ofi={} # From Physical Port to OpenFlow Index self.ofi2pp={} # From OpenFlow Index to Physical Port @@ -73,101 +77,118 @@ class OF_conn(): of_password=str(params["of_password"]) self.auth = base64.b64encode(str(params["of_user"])+":"+of_password) self.headers['Authorization'] = 'Basic '+self.auth - self.logger = logging.getLogger('vim.OF.ODL') self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR")) ) def get_of_switches(self): - ''' Obtain a a list of switches or DPID detected by this controller - Return - >=0, list: list length, and a list where each element a tuple pair (DPID, IP address) - <0, text_error: if fails - ''' + """ + Obtain a a list of switches or DPID detected by this controller + :return: list length, and a list where each element a tuple pair (DPID, IP address) + Raise an OpenflowconnConnectionException exception if fails with text_error + """ try: of_response = requests.get(self.url+"/restconf/operational/opendaylight-inventory:nodes", headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("get_of_switches " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse("Error get_of_switches " + error_text) + self.logger.debug("get_of_switches " + error_text) info = of_response.json() - + if type(info) != dict: self.logger.error("get_of_switches. Unexpected response, not a dict: %s", str(info)) - return -1, "Unexpected response, not a dict. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response, not a dict. Wrong version?") nodes = info.get('nodes') if type(nodes) is not dict: self.logger.error("get_of_switches. Unexpected response at 'nodes', not found or not a dict: %s", str(type(info))) - return -1, "Unexpected response at 'nodes', not found or not a dict. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'nodes', not found or " + "not a dict. Wrong version?") node_list = nodes.get('node') if type(node_list) is not list: - self.logger.error("get_of_switches. Unexpected response, at 'nodes':'node', not found or not a list: %s", str(type(node_list))) - return -1, "Unexpected response, at 'nodes':'node', not found or not a list. Wrong version?" + self.logger.error("get_of_switches. Unexpected response, at 'nodes':'node', " + "not found or not a list: %s", str(type(node_list))) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response, at 'nodes':'node', not found " + "or not a list. Wrong version?") switch_list=[] for node in node_list: node_id = node.get('id') if node_id is None: self.logger.error("get_of_switches. Unexpected response at 'nodes':'node'[]:'id', not found: %s", str(node)) - return -1, "Unexpected response at 'nodes':'node'[]:'id', not found . Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:'id', " + "not found . Wrong version?") if node_id == 'controller-config': continue node_ip_address = node.get('flow-node-inventory:ip-address') if node_ip_address is None: - self.logger.error("get_of_switches. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:ip-address', not found: %s", str(node)) - return -1, "Unexpected response at 'nodes':'node'[]:'flow-node-inventory:ip-address', not found. Wrong version?" + self.logger.error("get_of_switches. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:" + "ip-address', not found: %s", str(node)) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:" + "'flow-node-inventory:ip-address', " + "not found. Wrong version?") node_id_hex=hex(int(node_id.split(':')[1])).split('x')[1].zfill(16) switch_list.append( (':'.join(a+b for a,b in zip(node_id_hex[::2], node_id_hex[1::2])), node_ip_address)) return len(switch_list), switch_list - except (requests.exceptions.RequestException, ValueError) as e: - #ValueError in the case that JSON can not be decoded + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_switches " + error_text) + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded error_text = type(e).__name__ + ": " + str(e) self.logger.error("get_of_switches " + error_text) - return -1, error_text - + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) + def obtain_port_correspondence(self): - '''Obtain the correspondence between physical and openflow port names - return: - 0, dictionary: with physical name as key, openflow name as value - -1, error_text: if fails - ''' + """ + Obtain the correspondence between physical and openflow port names + :return: dictionary: with physical name as key, openflow name as value, + Raise a OpenflowconnConnectionException expection in case of failure + """ try: of_response = requests.get(self.url+"/restconf/operational/opendaylight-inventory:nodes", headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("obtain_port_correspondence " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("obtain_port_correspondence " + error_text) info = of_response.json() - + if type(info) != dict: self.logger.error("obtain_port_correspondence. Unexpected response not a dict: %s", str(info)) - return -1, "Unexpected openflow response, not a dict. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected openflow response, not a dict. " + "Wrong version?") nodes = info.get('nodes') if type(nodes) is not dict: - self.logger.error("obtain_port_correspondence. Unexpected response at 'nodes', not found or not a dict: %s", str(type(nodes))) - return -1, "Unexpected response at 'nodes',not found or not a dict. Wrong version?" + self.logger.error("obtain_port_correspondence. Unexpected response at 'nodes', " + "not found or not a dict: %s", str(type(nodes))) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'nodes',not found or not a dict. Wrong version?") node_list = nodes.get('node') if type(node_list) is not list: - self.logger.error("obtain_port_correspondence. Unexpected response, at 'nodes':'node', not found or not a list: %s", str(type(node_list))) - return -1, "Unexpected response, at 'nodes':'node', not found or not a list. Wrong version?" + self.logger.error("obtain_port_correspondence. Unexpected response, at 'nodes':'node', " + "not found or not a list: %s", str(type(node_list))) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response, at 'nodes':'node', " + "not found or not a list. Wrong version?") for node in node_list: node_id = node.get('id') if node_id is None: - self.logger.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'id', not found: %s", str(node)) - return -1, "Unexpected response at 'nodes':'node'[]:'id', not found . Wrong version?" + self.logger.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'id', " + "not found: %s", str(node)) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:'id', " + "not found . Wrong version?") if node_id == 'controller-config': continue @@ -180,37 +201,44 @@ class OF_conn(): node_connector_list = node.get('node-connector') if type(node_connector_list) is not list: - self.logger.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'node-connector', not found or not a list: %s", str(node)) - return -1, "Unexpected response at 'nodes':'node'[]:'node-connector', not found or not a list. Wrong version?" + self.logger.error("obtain_port_correspondence. Unexpected response at " + "'nodes':'node'[]:'node-connector', not found or not a list: %s", str(node)) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:" + "'node-connector', not found or not a list. " + "Wrong version?") for node_connector in node_connector_list: self.pp2ofi[ str(node_connector['flow-node-inventory:name']) ] = str(node_connector['id'] ) self.ofi2pp[ node_connector['id'] ] = str(node_connector['flow-node-inventory:name']) - node_ip_address = node.get('flow-node-inventory:ip-address') if node_ip_address is None: - self.logger.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:ip-address', not found: %s", str(node)) - return -1, "Unexpected response at 'nodes':'node'[]:'flow-node-inventory:ip-address', not found. Wrong version?" + self.logger.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:" + "'flow-node-inventory:ip-address', not found: %s", str(node)) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:" + "'flow-node-inventory:ip-address', not found. Wrong version?") self.ip_address = node_ip_address - #If we found the appropriate dpid no need to continue in the for loop + # If we found the appropriate dpid no need to continue in the for loop break - #print self.name, ": obtain_port_correspondence ports:", self.pp2ofi - return 0, self.pp2ofi - except (requests.exceptions.RequestException, ValueError) as e: - #ValueError in the case that JSON can not be decoded + # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi + return self.pp2ofi + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("obtain_port_correspondence " + error_text) + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded error_text = type(e).__name__ + ": " + str(e) self.logger.error("obtain_port_correspondence " + error_text) - return -1, error_text - + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) + def get_of_rules(self, translate_of_ports=True): - ''' Obtain the rules inserted at openflow controller - Params: - translate_of_ports: if True it translates ports from openflow index to physical switch name - Return: - 0, dict if ok: with the rule name as key and value is another dictionary with the following content: + """ + Obtain the rules inserted at openflow controller + :param translate_of_ports: + :return: dict if ok: with the rule name as key and value is another dictionary with the following content: priority: rule priority name: rule name (present also as the master dict key) ingress_port: match input port of the rule @@ -218,69 +246,77 @@ class OF_conn(): vlan_id: match vlan tag of the rule, can be missing or None if not apply actions: list of actions, composed by a pair tuples: (vlan, None/int): for stripping/setting a vlan tag - (out, port): send to this port - switch: DPID, all - -1, text_error if fails - ''' - - if len(self.ofi2pp) == 0: - r,c = self.obtain_port_correspondence() - if r<0: - return r,c - #get rules + (out, port): send to this port + switch: DPID, all + Raise a OpenflowconnConnectionException expection in case of failure + + """ + try: + # get rules + if len(self.ofi2pp) == 0: + self.obtain_port_correspondence() + of_response = requests.get(self.url+"/restconf/config/opendaylight-inventory:nodes/node/" + self.id + "/table/0", headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) # The configured page does not exist if there are no rules installed. In that case we return an empty dict if of_response.status_code == 404: - return 0, {} + return {} elif of_response.status_code != 200: self.logger.warning("get_of_rules " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) + self.logger.debug("get_of_rules " + error_text) - + info = of_response.json() if type(info) != dict: self.logger.error("get_of_rules. Unexpected response not a dict: %s", str(info)) - return -1, "Unexpected openflow response, not a dict. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected openflow response, not a dict. " + "Wrong version?") table = info.get('flow-node-inventory:table') if type(table) is not list: - self.logger.error("get_of_rules. Unexpected response at 'flow-node-inventory:table', not a list: %s", str(type(table))) - return -1, "Unexpected response at 'flow-node-inventory:table', not a list. Wrong version?" + self.logger.error("get_of_rules. Unexpected response at 'flow-node-inventory:table', " + "not a list: %s", str(type(table))) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'flow-node-inventory:table'," + " not a list. Wrong version?") flow_list = table[0].get('flow') if flow_list is None: - return 0, {} + return {} if type(flow_list) is not list: self.logger.error("get_of_rules. Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a list: %s", str(type(flow_list))) - return -1, "Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a list. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'flow-node-inventory:" + "table'[0]:'flow', not a list. Wrong version?") - #TODO translate ports according to translate_of_ports parameter + # TODO translate ports according to translate_of_ports parameter rules = dict() for flow in flow_list: - if not ('id' in flow and 'match' in flow and 'instructions' in flow and \ - 'instruction' in flow['instructions'] and 'apply-actions' in flow['instructions']['instruction'][0] and \ - 'action' in flow['instructions']['instruction'][0]['apply-actions']): - return -1, "unexpected openflow response, one or more elements are missing. Wrong version?" + if not ('id' in flow and 'match' in flow and 'instructions' in flow and + 'instruction' in flow['instructions'] and + 'apply-actions' in flow['instructions']['instruction'][0] and + 'action' in flow['instructions']['instruction'][0]['apply-actions']): + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow response, one or more " + "elements are missing. Wrong version?") flow['instructions']['instruction'][0]['apply-actions']['action'] rule = dict() rule['switch'] = self.dpid rule['priority'] = flow.get('priority') - #rule['name'] = flow['id'] - #rule['cookie'] = flow['cookie'] + # rule['name'] = flow['id'] + # rule['cookie'] = flow['cookie'] if 'in-port' in flow['match']: in_port = flow['match']['in-port'] if not in_port in self.ofi2pp: - return -1, "Error: Ingress port "+in_port+" is not in switch port list" + raise openflow_conn.OpenflowconnUnexpectedResponse("Error: Ingress port " + in_port + + " is not in switch port list") if translate_of_ports: in_port = self.ofi2pp[in_port] @@ -308,11 +344,14 @@ class OF_conn(): for instruction in instructions: if 'output-action' in instruction: if not 'output-node-connector' in instruction['output-action']: - return -1, "unexpected openflow response, one or more elementa are missing. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow response, one or " + "more elementa are missing. " + "Wrong version?") out_port = instruction['output-action']['output-node-connector'] if not out_port in self.ofi2pp: - return -1, "Error: Output port "+out_port+" is not in switch port list" + raise openflow_conn.OpenflowconnUnexpectedResponse("Error: Output port " + out_port + + " is not in switch port list") if translate_of_ports: out_port = self.ofi2pp[out_port] @@ -324,7 +363,9 @@ class OF_conn(): elif 'set-field' in instruction: if not ('vlan-match' in instruction['set-field'] and 'vlan-id' in instruction['set-field']['vlan-match'] and 'vlan-id' in instruction['set-field']['vlan-match']['vlan-id']): - return -1, "unexpected openflow response, one or more elements are missing. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow response, one or " + "more elements are missing. " + "Wrong version?") actions[instruction['order']] = ('vlan', instruction['set-field']['vlan-match']['vlan-id']['vlan-id']) @@ -341,7 +382,7 @@ class OF_conn(): # match -> in-port # -> vlan-match -> vlan-id -> vlan-id #flow['match']['vlan-match']['vlan-id']['vlan-id-present'] - #TODO se asume que no se usan reglas con vlan-id-present:false + #TODO we asume that is not using rules with vlan-id-present:false #instructions -> instruction -> apply-actions -> action #instructions=flow['instructions']['instruction'][0]['apply-actions']['action'] #Es una lista. Posibles elementos: @@ -361,39 +402,43 @@ class OF_conn(): #actions = [x for x in actions if x != None] # -> output-action -> output-node-connector # -> pop-vlan-action - - return 0, rules - except (requests.exceptions.RequestException, ValueError) as e: - #ValueError in the case that JSON can not be decoded + return rules + except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("get_of_rules " + error_text) - return -1, error_text - + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_rules " + error_text) + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) + def del_flow(self, flow_name): - ''' Delete an existing rule - Params: flow_name, this is the rule name - Return - 0, None if ok - -1, text_error if fails - ''' + """ + Delete an existing rule + :param flow_name: flow_name, this is the rule name + :return: Raise a OpenflowconnConnectionException expection in case of failure + """ + try: of_response = requests.delete(self.url+"/restconf/config/opendaylight-inventory:nodes/node/" + self.id + "/table/0/flow/"+flow_name, headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("del_flow " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("del_flow OK " + error_text) - return 0, None - + return None except requests.exceptions.RequestException as e: + # raise an exception in case of contection error error_text = type(e).__name__ + ": " + str(e) self.logger.error("del_flow " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) def new_flow(self, data): - ''' Insert a new static rule - Params: data: dictionary with the following content: + """ + Insert a new static rule + :param data: dictionary with the following content: priority: rule priority name: rule name ingress_port: match input port of the rule @@ -402,16 +447,15 @@ class OF_conn(): actions: list of actions, composed by a pair tuples with these posibilities: ('vlan', None/int): for stripping/setting a vlan tag ('out', port): send to this port - Return - 0, None if ok - -1, text_error if fails - ''' - if len(self.pp2ofi) == 0: - r,c = self.obtain_port_correspondence() - if r<0: - return r,c + :return: Raise a OpenflowconnConnectionException expection in case of failure + """ + try: - #We have to build the data for the opendaylight call from the generic data + + if len(self.pp2ofi) == 0: + self.obtain_port_correspondence() + + # We have to build the data for the opendaylight call from the generic data sdata = dict() sdata['flow-node-inventory:flow'] = list() sdata['flow-node-inventory:flow'].append(dict()) @@ -426,7 +470,7 @@ class OF_conn(): if not data['ingress_port'] in self.pp2ofi: error_text = 'Error. Port '+data['ingress_port']+' is not present in the switch' self.logger.warning("new_flow " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) flow['match']['in-port'] = self.pp2ofi[data['ingress_port']] if 'dst_mac' in data: flow['match']['ethernet-match'] = dict() @@ -450,7 +494,7 @@ class OF_conn(): new_action = { 'order': order } if action[0] == "vlan": if action[1] == None: - #strip vlan + # strip vlan new_action['strip-vlan-action'] = dict() else: new_action['set-field'] = dict() @@ -462,49 +506,48 @@ class OF_conn(): new_action['output-action'] = dict() if not action[1] in self.pp2ofi: error_msj = 'Port '+action[1]+' is not present in the switch' - return -1, error_msj + raise openflow_conn.OpenflowconnUnexpectedResponse(error_msj) new_action['output-action']['output-node-connector'] = self.pp2ofi[ action[1] ] else: error_msj = "Unknown item '%s' in action list" % action[0] self.logger.error("new_flow " + error_msj) - return -1, error_msj + raise openflow_conn.OpenflowconnUnexpectedResponse(error_msj) actions.append(new_action) order += 1 - #print json.dumps(sdata) + # print json.dumps(sdata) of_response = requests.put(self.url+"/restconf/config/opendaylight-inventory:nodes/node/" + self.id + "/table/0/flow/" + data['name'], headers=self.headers, data=json.dumps(sdata) ) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("new_flow " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("new_flow OK " + error_text) - return 0, None + return None except requests.exceptions.RequestException as e: + # raise an exception in case of contection error error_text = type(e).__name__ + ": " + str(e) self.logger.error("new_flow " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) def clear_all_flows(self): - ''' Delete all existing rules - Return: - 0, None if ok - -1, text_error if fails - ''' + """ + Delete all existing rules + :return: Raise a OpenflowconnConnectionException expection in case of failure + """ try: of_response = requests.delete(self.url+"/restconf/config/opendaylight-inventory:nodes/node/" + self.id + "/table/0", headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200 and of_response.status_code != 404: #HTTP_Not_Found self.logger.warning("clear_all_flows " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("clear_all_flows OK " + error_text) - return 0, None except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("clear_all_flows " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) diff --git a/database_utils/migrate_vim_db.sh b/database_utils/migrate_vim_db.sh index a3d96e8..1d24867 100755 --- a/database_utils/migrate_vim_db.sh +++ b/database_utils/migrate_vim_db.sh @@ -183,6 +183,7 @@ DATABASE_TARGET_VER_NUM=0 [ $OPENVIM_VER_NUM -ge 5006 ] && DATABASE_TARGET_VER_NUM=13 #0.5.6 => 13 [ $OPENVIM_VER_NUM -ge 5007 ] && DATABASE_TARGET_VER_NUM=14 #0.5.7 => 14 [ $OPENVIM_VER_NUM -ge 5008 ] && DATABASE_TARGET_VER_NUM=15 #0.5.8 => 15 +[ $OPENVIM_VER_NUM -ge 5009 ] && DATABASE_TARGET_VER_NUM=16 #0.5.9 => 16 #TODO ... put next versions here function upgrade_to_1(){ @@ -603,6 +604,22 @@ function downgrade_from_15(){ echo "DELETE FROM schema_version WHERE version_int = '15';" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 } + +function upgrade_to_16(){ + echo " upgrade database from version 0.15 to version 0.16" + echo " Add last_error and status colum to 'ofcs'" + echo "ALTER TABLE ofcs + ADD COLUMN last_error VARCHAR(255) NULL DEFAULT NULL AFTER password, + ADD COLUMN status ENUM('ACTIVE','INACTIVE','ERROR') NULL DEFAULT 'ACTIVE' AFTER last_error;"| $DBCMD || ! echo "ERROR. Aborted!" || exit -1 + echo "INSERT INTO schema_version (version_int, version, openvim_ver, comments, date) VALUES (16, '0.16', '0.5.9', 'Add last_error and status colum to ofcs', '2017-03-17');"| $DBCMD || ! echo "ERROR. Aborted!" || exit -1 +} + +function downgrade_from_16(){ + echo " downgrade database from version 0.16 to version 0.15" + echo " Delete last_error and status colum to 'ofcs'" + echo "ALTER TABLE ofcs DROP COLUMN last_error, DROP COLUMN status; " | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 + echo "DELETE FROM schema_version WHERE version_int = '16';" | $DBCMD || ! echo "ERROR. Aborted!" || exit -1 +} #TODO ... put funtions here echo "db version = "${DATABASE_VER_NUM} diff --git a/floodlight.py b/floodlight.py index fe7d616..826e300 100644 --- a/floodlight.py +++ b/floodlight.py @@ -22,132 +22,149 @@ # contact with: nfvlabs@tid.es ## -''' +""" Implement the plugging for floodligth openflow controller It creates the class OF_conn to create dataplane connections with static rules based on packet destination MAC address -''' - -__author__="Pablo Montes, Alfonso Tierno" -__date__ ="$28-oct-2014 12:07:15$" +""" +__author__ = "Pablo Montes, Alfonso Tierno" +__date__ = "$28-oct-2014 12:07:15$" import json import requests import logging +import openflow_conn + + +class OF_conn(openflow_conn.OpenflowConn): + """ + Openflow Connector for Floodlight. + No MAC learning is used + version 0.9 or 1.X is autodetected + version 1.X is in progress, not finished!!! + """ -class OF_conn(): - ''' Openflow Connector for Floodlight. - No MAC learning is used - version 0.9 or 1.X is autodetected - version 1.X is in progress, not finished!!! - ''' def __init__(self, params): - ''' Constructor. - params is a dictionay with the following keys: - of_dpid: DPID to use for this controller - of_ip: controller IP address - of_port: controller TCP port - of_version: version, can be "0.9" or "1.X". By default it is autodetected - of_debug: debug level for logging. Default to ERROR - other keys are ignored - Raise an exception if same parameter is missing or wrong - ''' - #check params - if "of_ip" not in params or params["of_ip"]==None or "of_port" not in params or params["of_port"]==None: + """ + Constructor + :param self: + :param params: dictionay with the following keys: + of_dpid: DPID to use for this controller + of_ip: controller IP address + of_port: controller TCP port + of_version: version, can be "0.9" or "1.X". By default it is autodetected + of_debug: debug level for logging. Default to ERROR + other keys are ignored + :return: Raise an ValueError exception if same parameter is missing or wrong + """ + # check params + if "of_ip" not in params or params["of_ip"] == None or "of_port" not in params or params["of_port"] == None: raise ValueError("IP address and port must be provided") + openflow_conn.OpenflowConn.__init__(self, params) + self.name = "Floodlight" self.dpid = str(params["of_dpid"]) - self.url = "http://%s:%s" %( str(params["of_ip"]), str(params["of_port"]) ) + self.url = "http://%s:%s" % (str(params["of_ip"]), str(params["of_port"])) - self.pp2ofi={} # From Physical Port to OpenFlow Index - self.ofi2pp={} # From OpenFlow Index to Physical Port - self.headers = {'content-type':'application/json', 'Accept':'application/json'} - self.version= None + self.pp2ofi = {} # From Physical Port to OpenFlow Index + self.ofi2pp = {} # From OpenFlow Index to Physical Port + self.headers = {'content-type': 'application/json', 'Accept': 'application/json'} + self.version = None self.logger = logging.getLogger('vim.OF.FL') - self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR") ) ) - self._set_version(params.get("of_version") ) - + self.logger.setLevel(getattr(logging, params.get("of_debug", "ERROR"))) + self._set_version(params.get("of_version")) + def _set_version(self, version): - '''set up a version of the controller. + """ + set up a version of the controller. Depending on the version it fills the self.ver_names with the naming used in this version - ''' - #static version names - if version==None: - self.version= None - elif version=="0.9": - self.version= version + :param version: Openflow controller version + :return: Raise an ValueError exception if same parameter is missing or wrong + """ + # static version names + if version == None: + self.version = None + elif version == "0.9": + self.version = version self.name = "Floodlightv0.9" - self.ver_names={ - "dpid": "dpid", - "URLmodifier": "staticflowentrypusher", - "destmac": "dst-mac", - "vlanid": "vlan-id", - "inport": "ingress-port", - "setvlan": "set-vlan-id", - "stripvlan": "strip-vlan", + self.ver_names = { + "dpid": "dpid", + "URLmodifier": "staticflowentrypusher", + "destmac": "dst-mac", + "vlanid": "vlan-id", + "inport": "ingress-port", + "setvlan": "set-vlan-id", + "stripvlan": "strip-vlan", } - elif version[0]=="1" : #version 1.X - self.version= version + elif version[0] == "1": # version 1.X + self.version = version self.name = "Floodlightv1.X" - self.ver_names={ - "dpid": "switchDPID", - "URLmodifier": "staticflowpusher", - "destmac": "eth_dst", - "vlanid": "eth_vlan_vid", - "inport": "in_port", - "setvlan": "set_vlan_vid", - "stripvlan": "strip_vlan", + self.ver_names = { + "dpid": "switchDPID", + "URLmodifier": "staticflowpusher", + "destmac": "eth_dst", + "vlanid": "eth_vlan_vid", + "inport": "in_port", + "setvlan": "set_vlan_vid", + "stripvlan": "strip_vlan", } else: raise ValueError("Invalid version for floodlight controller") - + def get_of_switches(self): - ''' Obtain a a list of switches or DPID detected by this controller - Return - >=0, list: list length, and a list where each element a tuple pair (DPID, IP address) - <0, text_error: if fails - ''' + """ + Obtain a a list of switches or DPID detected by this controller + :return: list where each element a tuple pair (DPID, IP address) + Raise an OpenflowconnConnectionException or OpenflowconnConnectionException exception if same + parameter is missing or wrong + """ try: - of_response = requests.get(self.url+"/wm/core/controller/switches/json", headers=self.headers) + of_response = requests.get(self.url + "/wm/core/controller/switches/json", headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("get_of_switches " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("get_of_switches " + error_text) info = of_response.json() if type(info) != list and type(info) != tuple: self.logger.error("get_of_switches. Unexpected response not a list %s", str(type(info))) - return -1, "Unexpected response, not a list. Wrong version?" - if len(info)==0: - return 0, info - #autodiscover version + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response, not a list. Wrong version?") + if len(info) == 0: + return info + # autodiscover version if self.version == None: if 'dpid' in info[0] and 'inetAddress' in info[0]: self._set_version("0.9") elif 'switchDPID' in info[0] and 'inetAddress' in info[0]: self._set_version("1.X") else: - self.logger.error("get_of_switches. Unexpected response, not found 'dpid' or 'switchDPID' field: %s", str(info[0])) - return -1, "Unexpected response, not found 'dpid' or 'switchDPID' field. Wrong version?" - - switch_list=[] + self.logger.error( + "get_of_switches. Unexpected response, not found 'dpid' or 'switchDPID' field: %s", + str(info[0])) + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response, not found 'dpid' or " + "'switchDPID' field. Wrong version?") + + switch_list = [] for switch in info: - switch_list.append( (switch[ self.ver_names["dpid"] ], switch['inetAddress']) ) - return len(switch_list), switch_list - except (requests.exceptions.RequestException, ValueError) as e: - #ValueError in the case that JSON can not be decoded + switch_list.append((switch[self.ver_names["dpid"]], switch['inetAddress'])) + return switch_list + except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("get_of_switches " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_switches " + error_text) + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) def get_of_rules(self, translate_of_ports=True): - ''' Obtain the rules inserted at openflow controller - Params: - translate_of_ports: if True it translates ports from openflow index to physical switch name - Return: - 0, dict if ok: with the rule name as key and value is another dictionary with the following content: + """ + Obtain the rules inserted at openflow controller + :param translate_of_ports: if True it translates ports from openflow index to physical switch name + :return: dict if ok: with the rule name as key and value is another dictionary with the following content: priority: rule priority name: rule name (present also as the master dict key) ingress_port: match input port of the rule @@ -155,43 +172,41 @@ class OF_conn(): vlan_id: match vlan tag of the rule, can be missing or None if not apply actions: list of actions, composed by a pair tuples: (vlan, None/int): for stripping/setting a vlan tag - (out, port): send to this port - switch: DPID, all - -1, text_error if fails - ''' - - #get translation, autodiscover version - if len(self.ofi2pp) == 0: - r,c = self.obtain_port_correspondence() - if r<0: - return r,c - #get rules + (out, port): send to this port + switch: DPID, all + Raise an openflowconnUnexpectedResponse exception if fails with text_error + """ + try: - of_response = requests.get(self.url+"/wm/%s/list/%s/json" %(self.ver_names["URLmodifier"], self.dpid), - headers=self.headers) + # get translation, autodiscover version + if len(self.ofi2pp) == 0: + self.obtain_port_correspondence() + + of_response = requests.get(self.url + "/wm/%s/list/%s/json" % (self.ver_names["URLmodifier"], self.dpid), + headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("get_of_rules " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("get_of_rules " + error_text) info = of_response.json() if type(info) != dict: self.logger.error("get_of_rules. Unexpected response not a dict %s", str(type(info))) - return -1, "Unexpected response, not a dict. Wrong version?" - rule_dict={} - for switch,switch_info in info.iteritems(): + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response, not a dict. Wrong version?") + rule_dict = {} + for switch, switch_info in info.iteritems(): if switch_info == None: continue if str(switch) != self.dpid: continue - for name,details in switch_info.iteritems(): + for name, details in switch_info.iteritems(): rule = {} rule["switch"] = str(switch) - #rule["active"] = "true" + # rule["active"] = "true" rule["priority"] = int(details["priority"]) - if self.version[0]=="0": + if self.version[0] == "0": if translate_of_ports: - rule["ingress_port"] = self.ofi2pp[ details["match"]["inputPort"] ] + rule["ingress_port"] = self.ofi2pp[details["match"]["inputPort"]] else: rule["ingress_port"] = str(details["match"]["inputPort"]) dst_mac = details["match"]["dataLayerDestination"] @@ -200,25 +215,26 @@ class OF_conn(): vlan = details["match"]["dataLayerVirtualLan"] if vlan != -1: rule["vlan_id"] = vlan - actionlist=[] + actionlist = [] for action in details["actions"]: - if action["type"]=="OUTPUT": + if action["type"] == "OUTPUT": if translate_of_ports: - port = self.ofi2pp[ action["port"] ] + port = self.ofi2pp[action["port"]] else: port = action["port"] - actionlist.append( ("out", port) ) - elif action["type"]=="STRIP_VLAN": - actionlist.append( ("vlan",None) ) - elif action["type"]=="SET_VLAN_ID": - actionlist.append( ("vlan", action["virtualLanIdentifier"]) ) + actionlist.append(("out", port)) + elif action["type"] == "STRIP_VLAN": + actionlist.append(("vlan", None)) + elif action["type"] == "SET_VLAN_ID": + actionlist.append(("vlan", action["virtualLanIdentifier"])) else: - actionlist.append( (action["type"], str(action) )) - self.logger.warning("get_of_rules() Unknown action in rule %s: %s", rule["name"], str(action)) + actionlist.append((action["type"], str(action))) + self.logger.warning("get_of_rules() Unknown action in rule %s: %s", rule["name"], + str(action)) rule["actions"] = actionlist - elif self.version[0]=="1": + elif self.version[0] == "1": if translate_of_ports: - rule["ingress_port"] = self.ofi2pp[ details["match"]["in_port"] ] + rule["ingress_port"] = self.ofi2pp[details["match"]["in_port"]] else: rule["ingress_port"] = details["match"]["in_port"] if "eth_dst" in details["match"]: @@ -226,216 +242,232 @@ class OF_conn(): if dst_mac != "00:00:00:00:00:00": rule["dst_mac"] = dst_mac if "eth_vlan_vid" in details["match"]: - vlan = int(details["match"]["eth_vlan_vid"],16) & 0xFFF + vlan = int(details["match"]["eth_vlan_vid"], 16) & 0xFFF rule["vlan_id"] = str(vlan) - actionlist=[] + actionlist = [] for action in details["instructions"]["instruction_apply_actions"]: - if action=="output": + if action == "output": if translate_of_ports: - port = self.ofi2pp[ details["instructions"]["instruction_apply_actions"]["output"] ] + port = self.ofi2pp[details["instructions"]["instruction_apply_actions"]["output"]] else: port = details["instructions"]["instruction_apply_actions"]["output"] - actionlist.append( ("out",port) ) - elif action=="strip_vlan": - actionlist.append( ("vlan",None) ) - elif action=="set_vlan_vid": - actionlist.append( ("vlan", details["instructions"]["instruction_apply_actions"]["set_vlan_vid"]) ) + actionlist.append(("out", port)) + elif action == "strip_vlan": + actionlist.append(("vlan", None)) + elif action == "set_vlan_vid": + actionlist.append( + ("vlan", details["instructions"]["instruction_apply_actions"]["set_vlan_vid"])) else: - self.logger.error("get_of_rules Unknown action in rule %s: %s", rule["name"], str(action)) - #actionlist.append( (action, str(details["instructions"]["instruction_apply_actions"]) )) + self.logger.error("get_of_rules Unknown action in rule %s: %s", rule["name"], + str(action)) + # actionlist.append( (action, str(details["instructions"]["instruction_apply_actions"]) )) rule_dict[str(name)] = rule - return 0, rule_dict - except (requests.exceptions.RequestException, ValueError) as e: - #ValueError in the case that JSON can not be decoded + return rule_dict + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_rules " + error_text) + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded error_text = type(e).__name__ + ": " + str(e) self.logger.error("get_of_rules " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) def obtain_port_correspondence(self): - '''Obtain the correspondence between physical and openflow port names - return: - 0, dictionary: with physical name as key, openflow name as value - -1, error_text: if fails - ''' + """ + Obtain the correspondence between physical and openflow port names + :return: dictionary: with physical name as key, openflow name as value + Raise an openflowconnUnexpectedResponse exception if fails with text_error + """ try: - of_response = requests.get(self.url+"/wm/core/controller/switches/json", headers=self.headers) - #print vim_response.status_code + of_response = requests.get(self.url + "/wm/core/controller/switches/json", headers=self.headers) + # print vim_response.status_code error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("obtain_port_correspondence " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("obtain_port_correspondence " + error_text) info = of_response.json() - + if type(info) != list and type(info) != tuple: - return -1, "unexpected openflow response, not a list. Wrong version?" - + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow response, not a list. " + "Wrong version?") + index = -1 - if len(info)>0: - #autodiscover version + if len(info) > 0: + # autodiscover version if self.version == None: if 'dpid' in info[0] and 'ports' in info[0]: self._set_version("0.9") elif 'switchDPID' in info[0]: self._set_version("1.X") else: - return -1, "unexpected openflow response, Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow response, " + "Wrong version?") - for i in range(0,len(info)): - if info[i][ self.ver_names["dpid"] ] == self.dpid: + for i in range(0, len(info)): + if info[i][self.ver_names["dpid"]] == self.dpid: index = i break if index == -1: - text = "DPID '"+self.dpid+"' not present in controller "+self.url - #print self.name, ": get_of_controller_info ERROR", text - return -1, text + text = "DPID '" + self.dpid + "' not present in controller " + self.url + # print self.name, ": get_of_controller_info ERROR", text + raise openflow_conn.OpenflowconnUnexpectedResponse(text) else: - if self.version[0]=="0": + if self.version[0] == "0": ports = info[index]["ports"] - else: #version 1.X - of_response = requests.get(self.url+"/wm/core/switch/%s/port-desc/json" %self.dpid, headers=self.headers) - #print vim_response.status_code + else: # version 1.X + of_response = requests.get(self.url + "/wm/core/switch/%s/port-desc/json" % self.dpid, + headers=self.headers) + # print vim_response.status_code error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("obtain_port_correspondence " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("obtain_port_correspondence " + error_text) info = of_response.json() if type(info) != dict: - return -1, "unexpected openflow port-desc response, not a dict. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow port-desc response, " + "not a dict. Wrong version?") if "portDesc" not in info: - return -1, "unexpected openflow port-desc response, 'portDesc' not found. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow port-desc response, " + "'portDesc' not found. Wrong version?") if type(info["portDesc"]) != list and type(info["portDesc"]) != tuple: - return -1, "unexpected openflow port-desc response at 'portDesc', not a list. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow port-desc response at " + "'portDesc', not a list. Wrong version?") ports = info["portDesc"] for port in ports: - self.pp2ofi[ str(port["name"]) ] = str(port["portNumber"] ) - self.ofi2pp[ port["portNumber"]] = str(port["name"]) - #print self.name, ": get_of_controller_info ports:", self.pp2ofi - return 0, self.pp2ofi - except (requests.exceptions.RequestException, ValueError) as e: - #ValueError in the case that JSON can not be decoded + self.pp2ofi[str(port["name"])] = str(port["portNumber"]) + self.ofi2pp[port["portNumber"]] = str(port["name"]) + # print self.name, ": get_of_controller_info ports:", self.pp2ofi + return self.pp2ofi + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("obtain_port_correspondence " + error_text) + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded error_text = type(e).__name__ + ": " + str(e) self.logger.error("obtain_port_correspondence " + error_text) - return -1, error_text - + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) + def del_flow(self, flow_name): - ''' Delete an existing rule - Params: flow_name, this is the rule name - Return - 0, None if ok - -1, text_error if fails - ''' - #autodiscover version - if self.version == None: - r,c = self.get_of_switches() - if r<0: - return r,c - elif r==0: - return -1, "No dpid found " + """ + Delete an existing rule + :param flow_name: this is the rule name + :return: None if ok + Raise an openflowconnUnexpectedResponse exception if fails with text_error + """ try: - of_response = requests.delete(self.url+"/wm/%s/json" % self.ver_names["URLmodifier"], - headers=self.headers, data='{"switch":"%s","name":"%s"}' %(self.dpid, flow_name) - ) + + # Raise an openflowconnUnexpectedResponse exception if fails with text_error + # autodiscover version + + if self.version == None: + self.get_of_switches() + + of_response = requests.delete(self.url + "/wm/%s/json" % self.ver_names["URLmodifier"], + headers=self.headers, + data='{"switch":"%s","name":"%s"}' % (self.dpid, flow_name) + ) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("del_flow " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("del_flow OK " + error_text) - return 0, None + return None except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("del_flow " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) def new_flow(self, data): - ''' Insert a new static rule - Params: data: dictionary with the following content: - priority: rule priority - name: rule name - ingress_port: match input port of the rule - dst_mac: match destination mac address of the rule, missing or None if not apply - vlan_id: match vlan tag of the rule, missing or None if not apply - actions: list of actions, composed by a pair tuples with these posibilities: - ('vlan', None/int): for stripping/setting a vlan tag - ('out', port): send to this port - Return - 0, None if ok - -1, text_error if fails - ''' - #get translation, autodiscover version + """ + Insert a new static rule + :param data: dictionary with the following content: + priority: rule priority + name: rule name + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, missing or None if not apply + vlan_id: match vlan tag of the rule, missing or None if not apply + actions: list of actions, composed by a pair tuples with these posibilities: + ('vlan', None/int): for stripping/setting a vlan tag + ('out', port): send to this port + :return: None if ok + Raise an openflowconnUnexpectedResponse exception if fails with text_error + """ + # get translation, autodiscover version if len(self.pp2ofi) == 0: - r,c = self.obtain_port_correspondence() - if r<0: - return r,c + self.obtain_port_correspondence() + try: - #We have to build the data for the floodlight call from the generic data - sdata = {'active': "true", "name":data["name"]} + # We have to build the data for the floodlight call from the generic data + sdata = {'active': "true", "name": data["name"]} if data.get("priority"): sdata["priority"] = str(data["priority"]) if data.get("vlan_id"): - sdata[ self.ver_names["vlanid"] ] = data["vlan_id"] + sdata[self.ver_names["vlanid"]] = data["vlan_id"] if data.get("dst_mac"): - sdata[ self.ver_names["destmac"] ] = data["dst_mac"] + sdata[self.ver_names["destmac"]] = data["dst_mac"] sdata['switch'] = self.dpid if not data['ingress_port'] in self.pp2ofi: - error_text = 'Error. Port '+data['ingress_port']+' is not present in the switch' + error_text = 'Error. Port ' + data['ingress_port'] + ' is not present in the switch' self.logger.warning("new_flow " + error_text) - return -1, error_text - - sdata[ self.ver_names["inport"] ] = self.pp2ofi[data['ingress_port']] + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) + + sdata[self.ver_names["inport"]] = self.pp2ofi[data['ingress_port']] sdata['actions'] = "" for action in data['actions']: if len(sdata['actions']) > 0: - sdata['actions'] += ',' + sdata['actions'] += ',' if action[0] == "vlan": - if action[1]==None: + if action[1] == None: sdata['actions'] += self.ver_names["stripvlan"] else: sdata['actions'] += self.ver_names["setvlan"] + "=" + str(action[1]) elif action[0] == 'out': - sdata['actions'] += "output=" + self.pp2ofi[ action[1] ] - + sdata['actions'] += "output=" + self.pp2ofi[action[1]] - of_response = requests.post(self.url+"/wm/%s/json" % self.ver_names["URLmodifier"], - headers=self.headers, data=json.dumps(sdata) ) + of_response = requests.post(self.url + "/wm/%s/json" % self.ver_names["URLmodifier"], + headers=self.headers, data=json.dumps(sdata)) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("new_flow " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("new_flow OK" + error_text) - return 0, None + return None except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("new_flow " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) def clear_all_flows(self): - ''' Delete all existing rules - Return: - 0, None if ok - -1, text_error if fails - ''' - #autodiscover version - if self.version == None: - r,c = self.get_of_switches() - if r<0: - return r,c - elif r==0: #empty - return 0, None + """ + Delete all existing rules + :return: None if ok + Raise an openflowconnUnexpectedResponse exception if fails with text_error + """ + try: - url = self.url+"/wm/%s/clear/%s/json" % (self.ver_names["URLmodifier"], self.dpid) - of_response = requests.get(url ) + # autodiscover version + if self.version == None: + sw_list = self.get_of_switches() + if len(sw_list) == 0: # empty + return None + + url = self.url + "/wm/%s/clear/%s/json" % (self.ver_names["URLmodifier"], self.dpid) + of_response = requests.get(url) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code < 200 or of_response.status_code >= 300: self.logger.warning("clear_all_flows " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("clear_all_flows OK " + error_text) - return 0, None + return None except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("clear_all_flows " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) + diff --git a/onos.py b/onos.py index 86fbac7..338412f 100644 --- a/onos.py +++ b/onos.py @@ -36,11 +36,15 @@ import json import requests import base64 import logging +import openflow_conn -class OF_conn(): - '''ONOS connector. No MAC learning is used''' + +class OF_conn(openflow_conn.OpenflowConn): + """ + ONOS connector. No MAC learning is used + """ def __init__(self, params): - ''' Constructor. + """ Constructor. Params: dictionary with the following keys: of_dpid: DPID to use for this controller ?? Does a controller have a dpid? of_ip: controller IP address @@ -50,16 +54,16 @@ class OF_conn(): of_debug: debug level for logging. Default to ERROR other keys are ignored Raise an exception if same parameter is missing or wrong - ''' - #check params + """ + + openflow_conn.OpenflowConn.__init__(self, params) + # check params if "of_ip" not in params or params["of_ip"]==None or "of_port" not in params or params["of_port"]==None: raise ValueError("IP address and port must be provided") #internal variables self.name = "onos" - self.headers = {'content-type':'application/json', - 'accept':'application/json', - } + self.headers = {'content-type':'application/json','accept':'application/json',} self.auth="None" self.pp2ofi={} # From Physical Port to OpenFlow Index @@ -78,30 +82,30 @@ class OF_conn(): self.auth = base64.b64encode(str(params["of_user"])+":"+of_password) self.headers['authorization'] = 'Basic ' + self.auth - self.logger = logging.getLogger('vim.OF.onos') self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR")) ) + self.ip_address = None def get_of_switches(self): - ''' Obtain a a list of switches or DPID detected by this controller - Return - >=0, list: list length, and a list where each element a tuple pair (DPID, IP address) - <0, text_error: if fails - ''' + """ + Obtain a a list of switches or DPID detected by this controller + :return: list where each element a tuple pair (DPID, IP address) + Raise a openflowconnUnexpectedResponse expection in case of failure + """ try: self.headers['content-type'] = 'text/plain' of_response = requests.get(self.url + "devices", headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("get_of_switches " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("get_of_switches " + error_text) info = of_response.json() if type(info) != dict: self.logger.error("get_of_switches. Unexpected response, not a dict: %s", str(info)) - return -1, "Unexpected response, not a dict. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response, not a dict. Wrong version?") node_list = info.get('devices') @@ -109,7 +113,8 @@ class OF_conn(): self.logger.error( "get_of_switches. Unexpected response, at 'devices', not found or not a list: %s", str(type(node_list))) - return -1, "Unexpected response, at 'devices', not found or not a list. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response, at 'devices', not found " + "or not a list. Wrong version?") switch_list = [] for node in node_list: @@ -117,43 +122,46 @@ class OF_conn(): if node_id is None: self.logger.error("get_of_switches. Unexpected response at 'device':'id', not found: %s", str(node)) - return -1, "Unexpected response at 'device':'id', not found . Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'device':'id', " + "not found . Wrong version?") node_ip_address = node.get('annotations').get('managementAddress') if node_ip_address is None: self.logger.error( "get_of_switches. Unexpected response at 'device':'managementAddress', not found: %s", str(node)) - return -1, "Unexpected response at 'device':'managementAddress', not found. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse( + "Unexpected response at 'device':'managementAddress', not found. Wrong version?") node_id_hex = hex(int(node_id.split(':')[1])).split('x')[1].zfill(16) switch_list.append( (':'.join(a + b for a, b in zip(node_id_hex[::2], node_id_hex[1::2])), node_ip_address)) + raise switch_list - return len(switch_list), switch_list - - except (requests.exceptions.RequestException, ValueError) as e: + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_switches " + error_text) + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: # ValueError in the case that JSON can not be decoded error_text = type(e).__name__ + ": " + str(e) self.logger.error("get_of_switches " + error_text) - return -1, error_text - + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) - def obtain_port_correspondence(self): - '''Obtain the correspondence between physical and openflow port names - return: - 0, dictionary: with physical name as key, openflow name as value - -1, error_text: if fails - ''' + """ + Obtain the correspondence between physical and openflow port names + :return: dictionary with physical name as key, openflow name as value + Raise a openflowconnUnexpectedResponse expection in case of failure + """ try: self.headers['content-type'] = 'text/plain' of_response = requests.get(self.url + "devices/" + self.id + "/ports", headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 200: self.logger.warning("obtain_port_correspondence " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("obtain_port_correspondence " + error_text) info = of_response.json() @@ -163,10 +171,11 @@ class OF_conn(): self.logger.error( "obtain_port_correspondence. Unexpected response at 'ports', not found or not a list: %s", str(node_connector_list)) - return -1, "Unexpected response at 'ports', not found or not a list. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'ports', not found or not " + "a list. Wrong version?") for node_connector in node_connector_list: - if (node_connector['port'] != "local"): + if node_connector['port'] != "local": self.pp2ofi[str(node_connector['annotations']['portName'])] = str(node_connector['port']) self.ofi2pp[str(node_connector['port'])] = str(node_connector['annotations']['portName']) @@ -175,24 +184,27 @@ class OF_conn(): self.logger.error( "obtain_port_correspondence. Unexpected response at 'managementAddress', not found: %s", str(self.id)) - return -1, "Unexpected response at 'managementAddress', not found. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'managementAddress', " + "not found. Wrong version?") self.ip_address = node_ip_address # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi - return 0, self.pp2ofi - - except (requests.exceptions.RequestException, ValueError) as e: + return self.pp2ofi + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("obtain_port_correspondence " + error_text) + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: # ValueError in the case that JSON can not be decoded error_text = type(e).__name__ + ": " + str(e) self.logger.error("obtain_port_correspondence " + error_text) - return -1, error_text - + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) + def get_of_rules(self, translate_of_ports=True): - ''' Obtain the rules inserted at openflow controller - Params: - translate_of_ports: if True it translates ports from openflow index to physical switch name - Return: - 0, dict if ok: with the rule name as key and value is another dictionary with the following content: + """ + Obtain the rules inserted at openflow controller + :param translate_of_ports: if True it translates ports from openflow index to physical switch name + :return: dict if ok: with the rule name as key and value is another dictionary with the following content: priority: rule priority name: rule name (present also as the master dict key) ingress_port: match input port of the rule @@ -200,47 +212,48 @@ class OF_conn(): vlan_id: match vlan tag of the rule, can be missing or None if not apply actions: list of actions, composed by a pair tuples: (vlan, None/int): for stripping/setting a vlan tag - (out, port): send to this port - switch: DPID, all - -1, text_error if fails - ''' - + (out, port): send to this port + switch: DPID, all + Raise a openflowconnUnexpectedResponse expection in case of failure + """ - if len(self.ofi2pp) == 0: - r, c = self.obtain_port_correspondence() - if r < 0: - return r, c - # get rules try: - self.headers['content-type'] = 'text/plain' + + if len(self.ofi2pp) == 0: + self.obtain_port_correspondence() + + # get rules + self.headers['content-type'] = 'text/plain' of_response = requests.get(self.url + "flows/" + self.id, headers=self.headers) error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) # The configured page does not exist if there are no rules installed. In that case we return an empty dict if of_response.status_code == 404: - return 0, {} + return {} elif of_response.status_code != 200: self.logger.warning("get_of_rules " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) self.logger.debug("get_of_rules " + error_text) info = of_response.json() if type(info) != dict: self.logger.error("get_of_rules. Unexpected response, not a dict: %s", str(info)) - return -1, "Unexpected openflow response, not a dict. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected openflow response, not a dict. " + "Wrong version?") flow_list = info.get('flows') if flow_list is None: - return 0, {} + return {} if type(flow_list) is not list: self.logger.error( "get_of_rules. Unexpected response at 'flows', not a list: %s", str(type(flow_list))) - return -1, "Unexpected response at 'flows', not a list. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("Unexpected response at 'flows', not a list. " + "Wrong version?") rules = dict() # Response dictionary @@ -248,7 +261,8 @@ class OF_conn(): if not ('id' in flow and 'selector' in flow and 'treatment' in flow and \ 'instructions' in flow['treatment'] and 'criteria' in \ flow['selector']): - return -1, "unexpected openflow response, one or more elements are missing. Wrong version?" + raise openflow_conn.OpenflowconnUnexpectedResponse("unexpected openflow response, one or more " + "elements are missing. Wrong version?") rule = dict() rule['switch'] = self.dpid @@ -260,7 +274,8 @@ class OF_conn(): in_port = str(criteria['port']) if in_port != "CONTROLLER": if not in_port in self.ofi2pp: - return -1, "Error: Ingress port " + in_port + " is not in switch port list" + raise openflow_conn.OpenflowconnUnexpectedResponse("Error: Ingress port {} is not " + "in switch port list".format(in_port)) if translate_of_ports: in_port = self.ofi2pp[in_port] rule['ingress_port'] = in_port @@ -277,7 +292,8 @@ class OF_conn(): out_port = str(instruction['port']) if out_port != "CONTROLLER": if not out_port in self.ofi2pp: - return -1, "Error: Output port " + out_port + " is not in switch port list" + raise openflow_conn.OpenflowconnUnexpectedResponse("Error: Output port {} is not in " + "switch port list".format(out_port)) if translate_of_ports: out_port = self.ofi2pp[out_port] @@ -291,22 +307,25 @@ class OF_conn(): rule['actions'] = actions rules[flow['id']] = dict(rule) + return rules - return 0, rules - - except (requests.exceptions.RequestException, ValueError) as e: + except requests.exceptions.RequestException as e: + # ValueError in the case that JSON can not be decoded + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_rules " + error_text) + raise openflow_conn.OpenflowconnConnectionException(error_text) + except ValueError as e: # ValueError in the case that JSON can not be decoded error_text = type(e).__name__ + ": " + str(e) self.logger.error("get_of_rules " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) def del_flow(self, flow_name): - ''' Delete an existing rule - Params: flow_name, this is the rule name - Return - 0, None if ok - -1, text_error if fails - ''' + """ + Delete an existing rule + :param flow_name: + :return: Raise a openflowconnUnexpectedResponse expection in case of failure + """ try: self.headers['content-type'] = None @@ -315,18 +334,20 @@ class OF_conn(): if of_response.status_code != 204: self.logger.warning("del_flow " + error_text) - return -1 , error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) + self.logger.debug("del_flow OK " + error_text) - return 0, None + return None except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("del_flow " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) def new_flow(self, data): - ''' Insert a new static rule - Params: data: dictionary with the following content: + """ + Insert a new static rule + :param data: dictionary with the following content: priority: rule priority name: rule name ingress_port: match input port of the rule @@ -335,16 +356,13 @@ class OF_conn(): actions: list of actions, composed by a pair tuples with these posibilities: ('vlan', None/int): for stripping/setting a vlan tag ('out', port): send to this port - Return - 0, None if ok - -1, text_error if fails - ''' - - if len(self.pp2ofi) == 0: - r,c = self.obtain_port_correspondence() - if r<0: - return r,c + :return: Raise a openflowconnUnexpectedResponse expection in case of failure + """ try: + + if len(self.pp2ofi) == 0: + self.obtain_port_correspondence() + # Build the dictionary with the flow rule information for ONOS flow = dict() #flow['id'] = data['name'] @@ -360,7 +378,7 @@ class OF_conn(): if not data['ingress_port'] in self.pp2ofi: error_text = 'Error. Port ' + data['ingress_port'] + ' is not present in the switch' self.logger.warning("new_flow " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) ingress_port_criteria = dict() ingress_port_criteria['type'] = "IN_PORT" @@ -397,12 +415,12 @@ class OF_conn(): new_action['type'] = "OUTPUT" if not action[1] in self.pp2ofi: error_msj = 'Port '+ action[1] + ' is not present in the switch' - return -1, error_msj + raise openflow_conn.OpenflowconnUnexpectedResponse(error_msj) new_action['port'] = self.pp2ofi[action[1]] else: error_msj = "Unknown item '%s' in action list" % action[0] self.logger.error("new_flow " + error_msj) - return -1, error_msj + raise openflow_conn.OpenflowconnUnexpectedResponse(error_msj) flow['treatment']['instructions'].append(new_action) @@ -413,41 +431,40 @@ class OF_conn(): error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) if of_response.status_code != 201: self.logger.warning("new_flow " + error_text) - return -1 , error_text - + raise openflow_conn.OpenflowconnUnexpectedResponse(error_text) flowId = of_response.headers['location'][path.__len__() + 1:] data['name'] = flowId self.logger.debug("new_flow OK " + error_text) - return 0, None + return None except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("new_flow " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) def clear_all_flows(self): - ''' Delete all existing rules - Return: - 0, None if ok - -1, text_error if fails - ''' + """ + Delete all existing rules + :return: Raise a openflowconnUnexpectedResponse expection in case of failure + """ try: - c, rules = self.get_of_rules(True) - if c < 0: - return -1, "Error retrieving the flows" + rules = self.get_of_rules(True) for rule in rules: self.del_flow(rule) self.logger.debug("clear_all_flows OK ") - return 0, None + return None except requests.exceptions.RequestException as e: error_text = type(e).__name__ + ": " + str(e) self.logger.error("clear_all_flows " + error_text) - return -1, error_text + raise openflow_conn.OpenflowconnConnectionException(error_text) + + + diff --git a/openflow b/openflow index 80cf624..4df9338 100755 --- a/openflow +++ b/openflow @@ -40,64 +40,76 @@ import imp import yaml import requests import logging +import openflow_conn from openflow_thread import change_db2of, FlowBadFormat + def of_switches(args): - r,c = ofconnector.get_of_switches() - if r<0: - print c - return r - else: + try: + c = ofconnector.get_of_switches() + for s in c: print " %s %s" % (s[0], s[1]) - return 0 + return 0 + except openflow_conn.OpenflowconnException as e: + print ("OF get switch error {}".format(str(e))) + return -1 + def of_list(args): - r,c = ofconnector.get_of_rules(not args.no_translate) - if r<0: - print c - return r - if args.verbose > 0: - print yaml.safe_dump(c, indent=4, default_flow_style=False) + try: + c = ofconnector.get_of_rules(not args.no_translate) + + if args.verbose > 0: + print yaml.safe_dump(c, indent=4, default_flow_style=False) + return 0 + + print " switch priority name ingress_port " \ + "dst_mac vlan_id actions" + for name, rule in c.iteritems(): + action_list = [] + for action in rule["actions"]: + action_list.append(action[0] + "=" + str(action[1])) + if "vlan_id" in rule: + vlan = str(rule["vlan_id"]) + else: + vlan = "any" + print "%s %s %s %s %s %s %s" % \ + (rule["switch"], str(rule["priority"]).ljust(6), name.ljust(40), rule["ingress_port"].ljust(8), + rule.get("dst_mac", "any").ljust(18), vlan.ljust(4), ",".join(action_list)) return 0 - print " switch priority name ingress_port dst_mac vlan_id actions" - for name,rule in c.iteritems(): - action_list=[] - for action in rule["actions"]: - action_list.append(action[0]+"="+str(action[1])) - if "vlan_id" in rule: - vlan=str(rule["vlan_id"]) - else: - vlan="any" - print "%s %s %s %s %s %s %s" % \ - (rule["switch"], str(rule["priority"]).ljust(6), name.ljust(40), rule["ingress_port"].ljust(8), \ - rule.get("dst_mac","any").ljust(18), vlan.ljust(4), ",".join(action_list) ) - return 0 + except openflow_conn.OpenflowconnException as e: + print("OF get list error {}".format(str(e))) + return -1 + def of_clear(args): - if not args.force: - r = raw_input("Clear all Openflow rules (y/N)? ") - if not (len(r)>0 and r[0].lower()=="y"): - return 0 - r,c = ofconnector.clear_all_flows() - if r<0: - print c - return r - return 0 + try: + if not args.force: + r = raw_input("Clear all Openflow rules (y/N)? ") + if not (len(r) > 0 and r[0].lower() == "y"): + return 0 + c = ofconnector.clear_all_flows() + return 0 + except openflow_conn.OpenflowconnException as e: + print ("OF error {}".format(str(e))) + return -1 + def of_port_list(args): - r,c = ofconnector.obtain_port_correspondence() - if r<0: - print c - return r - yaml.safe_dump({"ports": c}, sys.stdout, indent=2, default_flow_style=False) - -#def of_dump(args): -# args.verbose = 3 -# args.no_translate=False -# of_list(args) - return 0 + try: + c = ofconnector.obtain_port_correspondence() + yaml.safe_dump({"ports": c}, sys.stdout, indent=2, default_flow_style=False) + # def of_dump(args): + # args.verbose = 3 + # args.no_translate=False + # of_list(args) + return len(c) + except openflow_conn.OpenflowconnException as e: + print("OF error {}".format(str(e))) + return -1 + def of_reinstall(args): try: @@ -110,6 +122,7 @@ def of_reinstall(args): print " Exception GET at '"+URLrequest+"' " + str(e) return -1 + def of_install(args): line_number=1 try: @@ -138,14 +151,17 @@ def of_install(args): except FlowBadFormat as e: print "Format error at line %d: %s" % (line_number, str(e)) continue - r,c = ofconnector.new_flow(rule) - if r<0: - error="ERROR: "+c - else: - error="OK" - print "%s %s %s input=%s dst_mac=%s vlan_id=%s %s" % \ - (rule["switch"], str(rule.get("priority")).ljust(6), rule["name"].ljust(20), rule["ingress_port"].ljust(3), \ - rule.get("dst_mac","any").ljust(18), rule.get("vlan_id","any").ljust(4), error ) + try: + ofconnector.new_flow(rule) + error = "OK" + except openflow_conn.OpenflowconnException as e: + error = "ERROR: " + str(e) + print "%s %s %s input=%s dst_mac=%s vlan_id=%s %s" % (rule["switch"], + str(rule.get("priority")).ljust(6), + rule["name"].ljust(20), + rule["ingress_port"].ljust(3), + rule.get("dst_mac", "any").ljust(18), + rule.get("vlan_id", "any").ljust(4), error) return 0 except IOError as e: print " Error opening file '" + args.file + "': " + e.args[1] @@ -158,6 +174,7 @@ def of_install(args): print " Error yaml/json format error at " + error_pos return -1 + def of_add(args): if args.act==None and args.actions==None: print "openflow add: error: one of the arguments --actions or [--setvlan,--stripvlan],--out is required" @@ -201,24 +218,29 @@ def of_add(args): #print rule #return - r,c = ofconnector.new_flow(rule) - if r<0: - print c + try: + c = ofconnector.new_flow(rule) + if args.print_id: + print rule["name"] + return 0 + + except openflow_conn.OpenflowconnException as e: + print("OF error {}".format(str(e))) return -1 - if args.print_id: - print rule["name"] - return 0 + def of_delete(args): if not args.force: r = raw_input("Clear rule %s (y/N)? " %(args.name)) - if not (len(r)>0 and r[0].lower()=="y"): + if not (len(r) >0 and r[0].lower() == "y"): return 0 - r,c = ofconnector.del_flow(args.name) - if r<0: - print c + try: + ofconnector.del_flow(args.name) + return 0 + except openflow_conn.OpenflowconnException as e: + print("OF error {}".format(str(e))) return -1 - return 0 + def config(args): print "OPENVIM_HOST: %s" %(vim_host) diff --git a/openflow_conn.py b/openflow_conn.py new file mode 100644 index 0000000..f42f4dc --- /dev/null +++ b/openflow_conn.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- + +## +# Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U. +# This file is part of openmano +# All Rights Reserved. +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact with: nfvlabs@tid.es +## +import logging +import base64 + +""" +vimconn implement an Abstract class for the vim connector plugins + with the definition of the method to be implemented. +""" +__author__ = "Alfonso Tierno, Leonardo Mirabal" +__date__ = "$16-oct-2015 11:09:29$" + + + +# Error variables +HTTP_Bad_Request = 400 +HTTP_Unauthorized = 401 +HTTP_Not_Found = 404 +HTTP_Method_Not_Allowed = 405 +HTTP_Request_Timeout = 408 +HTTP_Conflict = 409 +HTTP_Not_Implemented = 501 +HTTP_Service_Unavailable = 503 +HTTP_Internal_Server_Error = 500 + + +class OpenflowconnException(Exception): + """Common and base class Exception for all vimconnector exceptions""" + def __init__(self, message, http_code=HTTP_Bad_Request): + Exception.__init__(self, message) + self.http_code = http_code + + +class OpenflowconnConnectionException(OpenflowconnException): + """Connectivity error with the VIM""" + def __init__(self, message, http_code=HTTP_Service_Unavailable): + OpenflowconnException.__init__(self, message, http_code) + + +class OpenflowconnUnexpectedResponse(OpenflowconnException): + """Get an wrong response from VIM""" + def __init__(self, message, http_code=HTTP_Internal_Server_Error): + OpenflowconnException.__init__(self, message, http_code) + + +class OpenflowconnAuthException(OpenflowconnException): + """Invalid credentials or authorization to perform this action over the VIM""" + def __init__(self, message, http_code=HTTP_Unauthorized): + OpenflowconnException.__init__(self, message, http_code) + + +class OpenflowconnNotFoundException(OpenflowconnException): + """The item is not found at VIM""" + def __init__(self, message, http_code=HTTP_Not_Found): + OpenflowconnException.__init__(self, message, http_code) + + +class OpenflowconnConflictException(OpenflowconnException): + """There is a conflict, e.g. more item found than one""" + def __init__(self, message, http_code=HTTP_Conflict): + OpenflowconnException.__init__(self, message, http_code) + + +class OpenflowconnNotSupportedException(OpenflowconnException): + """The request is not supported by connector""" + def __init__(self, message, http_code=HTTP_Service_Unavailable): + OpenflowconnException.__init__(self, message, http_code) + + +class OpenflowconnNotImplemented(OpenflowconnException): + """The method is not implemented by the connected""" + def __init__(self, message, http_code=HTTP_Not_Implemented): + OpenflowconnException.__init__(self, message, http_code) + + +class OpenflowConn: + """ + Openflow controller connector abstract implementeation. + """ + def __init__(self, params): + self.name = "openflow_conector" + self.headers = {'content-type': 'application/json', 'Accept': 'application/json'} + self.auth = None + self.pp2ofi = {} # From Physical Port to OpenFlow Index + self.ofi2pp = {} # From OpenFlow Index to Physical Port + self.dpid = '00:01:02:03:04:05:06:07' + self.id = 'openflow:00:01:02:03:04:05:06:07' + self.rules = {} + self.url = "http://%s:%s" % ('localhost', str(8081)) + self.auth = base64.b64encode('of_user:of_password') + self.headers['Authorization'] = 'Basic ' + self.auth + self.logger = logging.getLogger('openflow_conn') + self.logger.setLevel(getattr(logging, params.get("of_debug", "ERROR"))) + self.ip_address = None + + def get_of_switches(self): + """" + Obtain a a list of switches or DPID detected by this controller + :return: list length, and a list where each element a tuple pair (DPID, IP address), text_error: if fails + """ + raise OpenflowconnNotImplemented("Should have implemented this") + + def obtain_port_correspondence(self): + """ + Obtain the correspondence between physical and openflow port names + :return: dictionary: with physical name as key, openflow name as value, error_text: if fails + """ + raise OpenflowconnNotImplemented("Should have implemented this") + + def get_of_rules(self, translate_of_ports=True): + """ + Obtain the rules inserted at openflow controller + :param translate_of_ports: if True it translates ports from openflow index to physical switch name + :return: dict if ok: with the rule name as key and value is another dictionary with the following content: + priority: rule priority + name: rule name (present also as the master dict key) + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, can be missing or None if not apply + vlan_id: match vlan tag of the rule, can be missing or None if not apply + actions: list of actions, composed by a pair tuples: + (vlan, None/int): for stripping/setting a vlan tag + (out, port): send to this port + switch: DPID, all + text_error if fails + """ + raise OpenflowconnNotImplemented("Should have implemented this") + + def del_flow(self, flow_name): + """ + Delete all existing rules + :param flow_name: flow_name, this is the rule name + :return: None if ok, text_error if fails + """ + raise OpenflowconnNotImplemented("Should have implemented this") + + def new_flow(self, data): + """ + Insert a new static rule + :param data: dictionary with the following content: + priority: rule priority + name: rule name + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, missing or None if not apply + vlan_id: match vlan tag of the rule, missing or None if not apply + actions: list of actions, composed by a pair tuples with these posibilities: + ('vlan', None/int): for stripping/setting a vlan tag + ('out', port): send to this port + :return: None if ok, text_error if fails + """ + raise OpenflowconnNotImplemented("Should have implemented this") + + def clear_all_flows(self): + """" + Delete all existing rules + :return: None if ok, text_error if fails + """ + raise OpenflowconnNotImplemented("Should have implemented this") + + +class OfTestConnector(OpenflowConn): + """ + This is a fake openflow connector for testing. + It does nothing and it is used for running openvim without an openflow controller + """ + + def __init__(self, params): + OpenflowConn.__init__(self, params) + + name = params.get("name", "test-ofc") + self.name = name + self.dpid = params.get("dpid") + self.rules = {} + self.logger = logging.getLogger('vim.OF.TEST') + self.logger.setLevel(getattr(logging, params.get("of_debug", "ERROR"))) + self.pp2ofi = {} + + def get_of_switches(self): + return () + + def obtain_port_correspondence(self): + return () + + def del_flow(self, flow_name): + if flow_name in self.rules: + self.logger.debug("del_flow OK") + del self.rules[flow_name] + return None + else: + self.logger.warning("del_flow not found") + raise OpenflowconnUnexpectedResponse("flow {} not found".format(flow_name)) + + def new_flow(self, data): + self.rules[data["name"]] = data + self.logger.debug("new_flow OK") + return None + + def get_of_rules(self, translate_of_ports=True): + return self.rules + + def clear_all_flows(self): + self.logger.debug("clear_all_flows OK") + self.rules = {} + return None diff --git a/openflow_thread.py b/openflow_thread.py index b797f2d..6b25dab 100644 --- a/openflow_thread.py +++ b/openflow_thread.py @@ -23,7 +23,7 @@ ## ''' -This thread interacts with a openflow floodligth controller to create dataplane connections +This thread interacts with a openflow controller to create dataplane connections ''' __author__="Pablo Montes, Alfonso Tierno" @@ -36,6 +36,11 @@ import time import Queue import requests import logging +import openflow_conn + +OFC_STATUS_ACTIVE = 'ACTIVE' +OFC_STATUS_INACTIVE = 'INACTIVE' +OFC_STATUS_ERROR = 'ERROR' class FlowBadFormat(Exception): '''raise when a bad format of flow is found''' @@ -84,51 +89,11 @@ def change_db2of(flow): flow['actions'] = actions -class of_test_connector(): - '''This is a fake openflow connector for testing. - It does nothing and it is used for running openvim without an openflow controller - ''' - def __init__(self, params): - name = params.get("name", "test-ofc") - self.name = name - self.dpid = params.get("dpid") - self.rules= {} - self.logger = logging.getLogger('vim.OF.TEST') - self.logger.setLevel(getattr(logging, params.get("of_debug", "ERROR"))) - self.pp2ofi = {} - - def get_of_switches(self): - return 0, () - - def obtain_port_correspondence(self): - return 0, () - - def del_flow(self, flow_name): - if flow_name in self.rules: - self.logger.debug("del_flow OK") - del self.rules[flow_name] - return 0, None - else: - self.logger.warning("del_flow not found") - return -1, "flow %s not found" - - def new_flow(self, data): - self.rules[ data["name"] ] = data - self.logger.debug("new_flow OK") - return 0, None - - def get_of_rules(self, translate_of_ports=True): - return 0, self.rules - - def clear_all_flows(self): - self.logger.debug("clear_all_flows OK") - self.rules={} - return 0, None - - - class openflow_thread(threading.Thread): - def __init__(self, of_uuid, OF_connector, db, db_lock, of_test, pmp_with_same_vlan=False, debug='ERROR'): + """ + This thread interacts with a openflow controller to create dataplane connections + """ + def __init__(self, of_uuid, of_connector, db, db_lock, of_test, pmp_with_same_vlan=False, debug='ERROR'): threading.Thread.__init__(self) self.of_uuid = of_uuid self.db = db @@ -136,10 +101,10 @@ class openflow_thread(threading.Thread): self.name = "openflow" self.test = of_test self.db_lock = db_lock - self.OF_connector = OF_connector + self.OF_connector = of_connector self.logger = logging.getLogger('vim.OF-' + of_uuid) - self.logger.setLevel( getattr(logging, debug) ) - self.logger.name = OF_connector.name + " " + self.OF_connector.dpid + self.logger.setLevel(getattr(logging, debug)) + self.logger.name = of_connector.name + " " + self.OF_connector.dpid self.queueLock = threading.Lock() self.taskQueue = Queue.Queue(2000) @@ -154,47 +119,57 @@ class openflow_thread(threading.Thread): def run(self): self.logger.debug("Start openflow thread") + self.set_openflow_controller_status(OFC_STATUS_ACTIVE) + while True: - self.queueLock.acquire() - if not self.taskQueue.empty(): - task = self.taskQueue.get() - else: - task = None - self.queueLock.release() + try: + self.queueLock.acquire() + if not self.taskQueue.empty(): + task = self.taskQueue.get() + else: + task = None + self.queueLock.release() - if task is None: - time.sleep(1) - continue + if task is None: + time.sleep(1) + continue - if task[0] == 'update-net': - r,c = self.update_of_flows(task[1]) - #update database status - self.db_lock.acquire() - if r<0: - UPDATE={'status':'ERROR', 'last_error': str(c)} - self.logger.error("processing task 'update-net' %s: %s", str(task[1]), c) + if task[0] == 'update-net': + r,c = self.update_of_flows(task[1]) + # update database status + if r<0: + UPDATE={'status':'ERROR', 'last_error': str(c)} + self.logger.error("processing task 'update-net' %s: %s", str(task[1]), c) + self.set_openflow_controller_status(OFC_STATUS_ERROR, "Error updating net {}".format(task[1])) + else: + UPDATE={'status':'ACTIVE', 'last_error': None} + self.logger.debug("processing task 'update-net' %s: OK", str(task[1])) + self.set_openflow_controller_status(OFC_STATUS_ACTIVE) + self.db_lock.acquire() + self.db.update_rows('nets', UPDATE, WHERE={'uuid': task[1]}) + self.db_lock.release() + + elif task[0] == 'clear-all': + r,c = self.clear_all_flows() + if r<0: + self.logger.error("processing task 'clear-all': %s", c) + self.set_openflow_controller_status(OFC_STATUS_ERROR, "Error deleting all flows") + else: + self.set_openflow_controller_status(OFC_STATUS_ACTIVE) + self.logger.debug("processing task 'clear-all': OK") + elif task[0] == 'exit': + self.logger.debug("exit from openflow_thread") + self.terminate() + self.set_openflow_controller_status(OFC_STATUS_INACTIVE, "Ofc with thread killed") + return 0 else: - UPDATE={'status':'ACTIVE', 'last_error': None} - self.logger.debug("processing task 'update-net' %s: OK", str(task[1])) - self.db.update_rows('nets', UPDATE, WHERE={'uuid':task[1]}) - self.db_lock.release() + self.logger.error("unknown task %s", str(task)) + except openflow_conn.OpenflowconnException as e: + self.set_openflow_controller_status(OFC_STATUS_ERROR, str(e)) - elif task[0] == 'clear-all': - r,c = self.clear_all_flows() - if r<0: - self.logger.error("processing task 'clear-all': %s", c) - else: - self.logger.debug("processing task 'clear-all': OK") - elif task[0] == 'exit': - self.logger.debug("exit from openflow_thread") - self.terminate() - return 0 - else: - self.logger.error("unknown task %s", str(task)) - def terminate(self): pass - #print self.name, ": exit from openflow_thread" + # print self.name, ": exit from openflow_thread" def update_of_flows(self, net_id): ports=() @@ -230,6 +205,7 @@ class openflow_thread(threading.Thread): WHERE={'net_id':net_id, 'admin_state_up':'true', 'status':'ACTIVE'} ) self.db_lock.release() if nb_ports < 0: + #print self.name, ": update_of_flows() ERROR getting ports", ports return -1, "DB error getting ports from net '%s': %s" % (net_id, net_ports) @@ -253,23 +229,27 @@ class openflow_thread(threading.Thread): result, database_net_flows = self.db.get_table(FROM='of_flows', WHERE={'net_id':net_id}) self.db_lock.release() if result < 0: - #print self.name, ": update_of_flows() ERROR getting flows from database", database_flows - return -1, "DB error getting flows from net '%s': %s" %(net_id, database_net_flows) + error_msg = "DB error getting flows from net '{}': {}".format(net_id, database_net_flows) + # print self.name, ": update_of_flows() ERROR getting flows from database", database_flows + return -1, error_msg database_flows += database_net_flows # Get the name of flows where net_id==NULL that means net deleted (At DB foreign key: On delete set null) self.db_lock.acquire() result, database_net_flows = self.db.get_table(FROM='of_flows', WHERE={'net_id':None}) self.db_lock.release() if result < 0: - #print self.name, ": update_of_flows() ERROR getting flows from database", database_flows - return -1, "DB error getting flows from net 'null': %s" %(database_net_flows) + error_msg = "DB error getting flows from net 'null': {}".format(database_net_flows) + # print self.name, ": update_of_flows() ERROR getting flows from database", database_flows + return -1, error_msg database_flows += database_net_flows - #Get the existing flows at openflow controller - result, of_flows = self.OF_connector.get_of_rules() - if result < 0: - #print self.name, ": update_of_flows() ERROR getting flows from controller", of_flows - return -1, "OF error getting flows: " + of_flows + # Get the existing flows at openflow controller + try: + of_flows = self.OF_connector.get_of_rules() + # print self.name, ": update_of_flows() ERROR getting flows from controller", of_flows + except openflow_conn.OpenflowconnException as e: + # self.set_openflow_controller_status(OFC_STATUS_ERROR, "OF error {} getting flows".format(str(e))) + return -1, "OF error {} getting flows".format(str(e)) if ifaces_nb < 2: pass @@ -334,37 +314,39 @@ class openflow_thread(threading.Thread): continue used_names.append(flow['name']) name_index=0 - #insert at database the new flows, change actions to human text + # insert at database the new flows, change actions to human text for flow in new_flows: - #1 check if an equal flow is already present + # 1 check if an equal flow is already present index = self._check_flow_already_present(flow, database_flows) if index>=0: database_flows[index]["not delete"]=True self.logger.debug("Skipping already present flow %s", str(flow)) continue - #2 look for a non used name + # 2 look for a non used name flow_name=flow["net_id"]+"."+str(name_index) while flow_name in used_names or flow_name in of_flows: name_index += 1 flow_name=flow["net_id"]+"."+str(name_index) used_names.append(flow_name) flow['name'] = flow_name - #3 insert at openflow - result, content = self.OF_connector.new_flow(flow) - if result < 0: - #print self.name, ": Error '%s' at flow insertion" % c, flow - return -1, content - #4 insert at database + # 3 insert at openflow + + try: + self.OF_connector.new_flow(flow) + except openflow_conn.OpenflowconnException as e: + return -1, "Error creating new flow {}".format(str(e)) + + # 4 insert at database try: change_of2db(flow) except FlowBadFormat as e: - #print self.name, ": Error Exception FlowBadFormat '%s'" % str(e), flow + # print self.name, ": Error Exception FlowBadFormat '%s'" % str(e), flow return -1, str(e) self.db_lock.acquire() result, content = self.db.new_row('of_flows', flow) self.db_lock.release() if result < 0: - #print self.name, ": Error '%s' at database insertion" % content, flow + # print self.name, ": Error '%s' at database insertion" % content, flow return -1, content #delete not needed old flows from openflow and from DDBB, @@ -372,19 +354,23 @@ class openflow_thread(threading.Thread): for flow in database_flows: if "not delete" in flow: if flow["name"] not in of_flows: - #not in controller, insert it - result, content = self.OF_connector.new_flow(flow) - if result < 0: - #print self.name, ": Error '%s' at flow insertion" % c, flow - return -1, content + # not in controller, insert it + try: + self.OF_connector.new_flow(flow) + except openflow_conn.OpenflowconnException as e: + return -1, "Error creating new flow {}".format(str(e)) + continue - #Delete flow + # Delete flow if flow["name"] in of_flows: - result, content = self.OF_connector.del_flow(flow['name']) - if result<0: - self.logger.error("cannot delete flow '%s' from OF: %s", flow['name'], content ) - continue #skip deletion from database - #delete from database + try: + self.OF_connector.del_flow(flow['name']) + except openflow_conn.OpenflowconnException as e: + self.logger.error("cannot delete flow '%s' from OF: %s", flow['name'], str(e)) + # skip deletion from database + continue + + # delete from database self.db_lock.acquire() result, content = self.db.delete_row_by_key('of_flows', 'id', flow['id']) self.db_lock.release() @@ -397,16 +383,17 @@ class openflow_thread(threading.Thread): try: if not self.test: self.OF_connector.clear_all_flows() - #remove from database + + # remove from database self.db_lock.acquire() self.db.delete_row_by_key('of_flows', None, None) #this will delete all lines self.db_lock.release() return 0, None - except requests.exceptions.RequestException as e: - #print self.name, ": clear_all_flows Exception:", str(e) - return -1, str(e) + except openflow_conn.OpenflowconnException as e: + return -1, self.logger.error("Error deleting all flows {}", str(e)) + + flow_fields = ('priority', 'vlan', 'ingress_port', 'actions', 'dst_mac', 'src_mac', 'net_id') - flow_fields=('priority', 'vlan', 'ingress_port', 'actions', 'dst_mac', 'src_mac', 'net_id') def _check_flow_already_present(self, new_flow, flow_list): '''check if the same flow is already present in the flow list The flow is repeated if all the fields, apart from name, are equal @@ -435,7 +422,7 @@ class openflow_thread(threading.Thread): nb_ports += 1 if not self.test and str(port['switch_port']) not in self.OF_connector.pp2ofi: error_text= "switch port name '%s' is not valid for the openflow controller" % str(port['switch_port']) - #print self.name, ": ERROR " + error_text + # print self.name, ": ERROR " + error_text return -1, error_text for net_src in nets: @@ -581,4 +568,31 @@ class openflow_thread(threading.Thread): else: # add all the rules new_flows2 += flow_list return 0, new_flows2 - + + def set_openflow_controller_status(self, status, error_text=None): + """ + Set openflow controller last operation status in DB + :param status: ofc status ('ACTIVE','INACTIVE','ERROR') + :param error_text: error text + :return: + """ + if self.of_uuid == "Default": + return True + + ofc = {} + ofc['status'] = status + ofc['last_error'] = error_text + self.db_lock.acquire() + result, content = self.db.update_rows('ofcs', ofc, WHERE={'uuid': self.of_uuid}, log=False) + self.db_lock.release() + if result >= 0: + return True + else: + return False + + + + + + + diff --git a/openvim b/openvim index cb7321d..58d6ff0 100755 --- a/openvim +++ b/openvim @@ -896,7 +896,7 @@ def openflow_action(args): if "ADMIN_PORT" not in vim_config: print "OPENVIM_ADMIN_PORT variable not defined" return 401 # HTTP_Unauthorized - url = "http://%s:%s/openvim/networks/openflow/clear" %(vim_config["HOST"], vim_config["ADMIN_PORT"]) + url = "http://%s:%s/openvim/networks/clear/openflow" %(vim_config["HOST"], vim_config["ADMIN_PORT"]) r,c = vim_delete(url) else: return 400 #HTTP_Bad_Request diff --git a/ovim.py b/ovim.py index cc223a6..06ee50a 100644 --- a/ovim.py +++ b/ovim.py @@ -42,6 +42,7 @@ import dhcp_thread as dt import openflow_thread as oft from netaddr import IPNetwork from jsonschema import validate as js_v, exceptions as js_e +import openflow_conn HTTP_Bad_Request = 400 HTTP_Unauthorized = 401 @@ -342,8 +343,9 @@ class ovim(): try: if self.of_test_mode: - return oft.of_test_connector({"name": db_config['type'], "dpid": db_config['dpid'], - "of_debug": self.config['log_level_of']}) + return openflow_conn.OfTestConnector({"name": db_config['type'], + "dpid": db_config['dpid'], + "of_debug": self.config['log_level_of']}) temp_dict = {} if db_config: -- 2.25.1