# Copyright 2011 VMware, Inc. # 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. from oslo.config import cfg from neutron.agent.linux import ip_lib from neutron.agent.linux import utils from neutron.common import exceptions from neutron.common import utils as common_utils from neutron.openstack.common import excutils from neutron.openstack.common import jsonutils from neutron.openstack.common import log as logging from neutron.plugins.common import constants as p_const # TODO(JLH) Should we remove the explicit include of the ovs plugin here from neutron.plugins.openvswitch.common import constants # Default timeout for ovs-vsctl command DEFAULT_OVS_VSCTL_TIMEOUT = 10 OPTS = [ cfg.IntOpt('ovs_vsctl_timeout', default=DEFAULT_OVS_VSCTL_TIMEOUT, help=_('Timeout in seconds for ovs-vsctl commands')), ] cfg.CONF.register_opts(OPTS) LOG = logging.getLogger(__name__) class VifPort: def __init__(self, port_name, ofport, vif_id, vif_mac, switch): self.port_name = port_name self.ofport = ofport self.vif_id = vif_id self.vif_mac = vif_mac self.switch = switch def __str__(self): return ("iface-id=" + self.vif_id + ", vif_mac=" + self.vif_mac + ", port_name=" + self.port_name + ", ofport=" + str(self.ofport) + ", bridge_name=" + self.switch.br_name) class BaseOVS(object): def __init__(self, root_helper): self.root_helper = root_helper self.vsctl_timeout = cfg.CONF.ovs_vsctl_timeout def run_vsctl(self, args, check_error=False): full_args = ["ovs-vsctl", "--timeout=%d" % self.vsctl_timeout] + args try: return utils.execute(full_args, root_helper=self.root_helper) except Exception as e: with excutils.save_and_reraise_exception() as ctxt: LOG.error(_("Unable to execute %(cmd)s. " "Exception: %(exception)s"), {'cmd': full_args, 'exception': e}) if not check_error: ctxt.reraise = False def add_bridge(self, bridge_name): self.run_vsctl(["--", "--may-exist", "add-br", bridge_name]) return OVSBridge(bridge_name, self.root_helper) def delete_bridge(self, bridge_name): self.run_vsctl(["--", "--if-exists", "del-br", bridge_name]) def bridge_exists(self, bridge_name): try: self.run_vsctl(['br-exists', bridge_name], check_error=True) except RuntimeError as e: with excutils.save_and_reraise_exception() as ctxt: if 'Exit code: 2\n' in str(e): ctxt.reraise = False return False return True def get_bridge_name_for_port_name(self, port_name): try: return self.run_vsctl(['port-to-br', port_name], check_error=True) except RuntimeError as e: with excutils.save_and_reraise_exception() as ctxt: if 'Exit code: 1\n' in str(e): ctxt.reraise = False def port_exists(self, port_name): return bool(self.get_bridge_name_for_port_name(port_name)) class OVSBridge(BaseOVS): def __init__(self, br_name, root_helper): super(OVSBridge, self).__init__(root_helper) self.br_name = br_name self.defer_apply_flows = False self.deferred_flows = {'add': '', 'mod': '', 'del': ''} def set_controller(self, controller_names): vsctl_command = ['--', 'set-controller', self.br_name] vsctl_command.extend(controller_names) self.run_vsctl(vsctl_command, check_error=True) def del_controller(self): self.run_vsctl(['--', 'del-controller', self.br_name], check_error=True) def get_controller(self): res = self.run_vsctl(['--', 'get-controller', self.br_name], check_error=True) if res: return res.strip().split('\n') return res def set_secure_mode(self): self.run_vsctl(['--', 'set-fail-mode', self.br_name, 'secure'], check_error=True) def set_protocols(self, protocols): self.run_vsctl(['--', 'set', 'bridge', self.br_name, "protocols=%s" % protocols], check_error=True) def create(self): self.add_bridge(self.br_name) def destroy(self): self.delete_bridge(self.br_name) def reset_bridge(self): self.destroy() self.create() def add_port(self, port_name): self.run_vsctl(["--", "--may-exist", "add-port", self.br_name, port_name]) return self.get_port_ofport(port_name) def delete_port(self, port_name): self.run_vsctl(["--", "--if-exists", "del-port", self.br_name, port_name]) def set_db_attribute(self, table_name, record, column, value): args = ["set", table_name, record, "%s=%s" % (column, value)] self.run_vsctl(args) def clear_db_attribute(self, table_name, record, column): args = ["clear", table_name, record, column] self.run_vsctl(args) def run_ofctl(self, cmd, args, process_input=None): full_args = ["ovs-ofctl", cmd, self.br_name] + args try: return utils.execute(full_args, root_helper=self.root_helper, process_input=process_input) except Exception as e: LOG.error(_("Unable to execute %(cmd)s. Exception: %(exception)s"), {'cmd': full_args, 'exception': e}) def count_flows(self): flow_list = self.run_ofctl("dump-flows", []).split("\n")[1:] return len(flow_list) - 1 def remove_all_flows(self): self.run_ofctl("del-flows", []) def get_port_ofport(self, port_name): ofport = self.db_get_val("Interface", port_name, "ofport") # This can return a non-integer string, like '[]' so ensure a # common failure case try: int(ofport) return ofport except (ValueError, TypeError): return constants.INVALID_OFPORT def get_datapath_id(self): return self.db_get_val('Bridge', self.br_name, 'datapath_id').strip('"') def add_flow(self, **kwargs): flow_str = _build_flow_expr_str(kwargs, 'add') if self.defer_apply_flows: self.deferred_flows['add'] += flow_str + '\n' else: self.run_ofctl("add-flow", [flow_str]) def mod_flow(self, **kwargs): flow_str = _build_flow_expr_str(kwargs, 'mod') if self.defer_apply_flows: self.deferred_flows['mod'] += flow_str + '\n' else: self.run_ofctl("mod-flows", [flow_str]) def delete_flows(self, **kwargs): flow_expr_str = _build_flow_expr_str(kwargs, 'del') if self.defer_apply_flows: self.deferred_flows['del'] += flow_expr_str + '\n' else: self.run_ofctl("del-flows", [flow_expr_str]) def dump_flows_for_table(self, table): retval = None flow_str = "table=%s" % table flows = self.run_ofctl("dump-flows", [flow_str]) if flows: retval = '\n'.join(item for item in flows.splitlines() if 'NXST' not in item) return retval def defer_apply_on(self): # TODO(vivek): when defer_apply_on is used, DVR # flows are only getting partially configured when # run concurrently with l2-pop ON. # Will need make ovs_lib flow API context sensitive # and then use the same across this file, which will # address the race issue here. LOG.debug(_('defer_apply_on')) self.defer_apply_flows = True def defer_apply_off(self): # TODO(vivek): when defer_apply_off is used, DVR # flows are only getting partially configured when # run concurrently with l2-pop ON. # Will need make ovs_lib flow API context sensitive # and then use the same across this file, which will # address the race issue here. LOG.debug(_('defer_apply_off')) # Note(ethuleau): stash flows and disable deferred mode. Then apply # flows from the stashed reference to be sure to not purge flows that # were added between two ofctl commands. stashed_deferred_flows, self.deferred_flows = ( self.deferred_flows, {'add': '', 'mod': '', 'del': ''} ) self.defer_apply_flows = False for action, flows in stashed_deferred_flows.items(): if flows: LOG.debug(_('Applying following deferred flows ' 'to bridge %s'), self.br_name) for line in flows.splitlines(): LOG.debug(_('%(action)s: %(flow)s'), {'action': action, 'flow': line}) self.run_ofctl('%s-flows' % action, ['-'], flows) def add_tunnel_port(self, port_name, remote_ip, local_ip, tunnel_type=p_const.TYPE_GRE, vxlan_udp_port=constants.VXLAN_UDP_PORT, dont_fragment=True): vsctl_command = ["--", "--may-exist", "add-port", self.br_name, port_name] vsctl_command.extend(["--", "set", "Interface", port_name, "type=%s" % tunnel_type]) if tunnel_type == p_const.TYPE_VXLAN: # Only set the VXLAN UDP port if it's not the default if vxlan_udp_port != constants.VXLAN_UDP_PORT: vsctl_command.append("options:dst_port=%s" % vxlan_udp_port) vsctl_command.append(("options:df_default=%s" % bool(dont_fragment)).lower()) vsctl_command.extend(["options:remote_ip=%s" % remote_ip, "options:local_ip=%s" % local_ip, "options:in_key=flow", "options:out_key=flow"]) self.run_vsctl(vsctl_command) ofport = self.get_port_ofport(port_name) if (tunnel_type == p_const.TYPE_VXLAN and ofport == constants.INVALID_OFPORT): LOG.error(_('Unable to create VXLAN tunnel port. Please ensure ' 'that an openvswitch version that supports VXLAN is ' 'installed.')) return ofport def add_patch_port(self, local_name, remote_name): self.run_vsctl(["add-port", self.br_name, local_name, "--", "set", "Interface", local_name, "type=patch", "options:peer=%s" % remote_name]) return self.get_port_ofport(local_name) def db_get_map(self, table, record, column, check_error=False): output = self.run_vsctl(["get", table, record, column], check_error) if output: output_str = output.rstrip("\n\r") return self.db_str_to_map(output_str) return {} def db_get_val(self, table, record, column, check_error=False): output = self.run_vsctl(["get", table, record, column], check_error) if output: return output.rstrip("\n\r") def db_str_to_map(self, full_str): list = full_str.strip("{}").split(", ") ret = {} for e in list: if e.find("=") == -1: continue arr = e.split("=") ret[arr[0]] = arr[1].strip("\"") return ret def get_port_name_list(self): res = self.run_vsctl(["list-ports", self.br_name], check_error=True) if res: return res.strip().split("\n") return [] def get_port_stats(self, port_name): return self.db_get_map("Interface", port_name, "statistics") def get_xapi_iface_id(self, xs_vif_uuid): args = ["xe", "vif-param-get", "param-name=other-config", "param-key=nicira-iface-id", "uuid=%s" % xs_vif_uuid] try: return utils.execute(args, root_helper=self.root_helper).strip() except Exception as e: with excutils.save_and_reraise_exception(): LOG.error(_("Unable to execute %(cmd)s. " "Exception: %(exception)s"), {'cmd': args, 'exception': e}) # returns a VIF object for each VIF port def get_vif_ports(self): edge_ports = [] port_names = self.get_port_name_list() for name in port_names: external_ids = self.db_get_map("Interface", name, "external_ids", check_error=True) ofport = self.db_get_val("Interface", name, "ofport", check_error=True) if "iface-id" in external_ids and "attached-mac" in external_ids: p = VifPort(name, ofport, external_ids["iface-id"], external_ids["attached-mac"], self) edge_ports.append(p) elif ("xs-vif-uuid" in external_ids and "attached-mac" in external_ids): # if this is a xenserver and iface-id is not automatically # synced to OVS from XAPI, we grab it from XAPI directly iface_id = self.get_xapi_iface_id(external_ids["xs-vif-uuid"]) p = VifPort(name, ofport, iface_id, external_ids["attached-mac"], self) edge_ports.append(p) return edge_ports def get_vif_port_set(self): port_names = self.get_port_name_list() edge_ports = set() args = ['--format=json', '--', '--columns=name,external_ids,ofport', 'list', 'Interface'] result = self.run_vsctl(args, check_error=True) if not result: return edge_ports for row in jsonutils.loads(result)['data']: name = row[0] if name not in port_names: continue external_ids = dict(row[1][1]) # Do not consider VIFs which aren't yet ready # This can happen when ofport values are either [] or ["set", []] # We will therefore consider only integer values for ofport ofport = row[2] try: int_ofport = int(ofport) except (ValueError, TypeError): LOG.warn(_("Found not yet ready openvswitch port: %s"), row) else: if int_ofport > 0: if ("iface-id" in external_ids and "attached-mac" in external_ids): edge_ports.add(external_ids['iface-id']) elif ("xs-vif-uuid" in external_ids and "attached-mac" in external_ids): # if this is a xenserver and iface-id is not # automatically synced to OVS from XAPI, we grab it # from XAPI directly iface_id = self.get_xapi_iface_id( external_ids["xs-vif-uuid"]) edge_ports.add(iface_id) else: LOG.warn(_("Found failed openvswitch port: %s"), row) return edge_ports def get_port_tag_dict(self): """Get a dict of port names and associated vlan tags. e.g. the returned dict is of the following form:: {u'int-br-eth2': [], u'patch-tun': [], u'qr-76d9e6b6-21': 1, u'tapce5318ff-78': 1, u'tape1400310-e6': 1} The TAG ID is only available in the "Port" table and is not available in the "Interface" table queried by the get_vif_port_set() method. """ port_names = self.get_port_name_list() args = ['--format=json', '--', '--columns=name,tag', 'list', 'Port'] result = self.run_vsctl(args, check_error=True) port_tag_dict = {} if not result: return port_tag_dict for name, tag in jsonutils.loads(result)['data']: if name not in port_names: continue # 'tag' can be [u'set', []] or an integer if isinstance(tag, list): tag = tag[1] port_tag_dict[name] = tag return port_tag_dict def get_vif_port_by_id(self, port_id): args = ['--format=json', '--', '--columns=external_ids,name,ofport', 'find', 'Interface', 'external_ids:iface-id="%s"' % port_id] result = self.run_vsctl(args) if not result: return json_result = jsonutils.loads(result) try: # Retrieve the indexes of the columns we're looking for headings = json_result['headings'] ext_ids_idx = headings.index('external_ids') name_idx = headings.index('name') ofport_idx = headings.index('ofport') # If data attribute is missing or empty the line below will raise # an exeception which will be captured in this block. # We won't deal with the possibility of ovs-vsctl return multiple # rows since the interface identifier is unique data = json_result['data'][0] port_name = data[name_idx] switch = get_bridge_for_iface(self.root_helper, port_name) if switch != self.br_name: LOG.info(_("Port: %(port_name)s is on %(switch)s," " not on %(br_name)s"), {'port_name': port_name, 'switch': switch, 'br_name': self.br_name}) return ofport = data[ofport_idx] # ofport must be integer otherwise return None if not isinstance(ofport, int) or ofport == -1: LOG.warn(_("ofport: %(ofport)s for VIF: %(vif)s is not a " "positive integer"), {'ofport': ofport, 'vif': port_id}) return # Find VIF's mac address in external ids ext_id_dict = dict((item[0], item[1]) for item in data[ext_ids_idx][1]) vif_mac = ext_id_dict['attached-mac'] return VifPort(port_name, ofport, port_id, vif_mac, self) except Exception as e: LOG.warn(_("Unable to parse interface details. Exception: %s"), e) return def delete_ports(self, all_ports=False): if all_ports: port_names = self.get_port_name_list() else: port_names = (port.port_name for port in self.get_vif_ports()) for port_name in port_names: self.delete_port(port_name) def get_local_port_mac(self): """Retrieve the mac of the bridge's local port.""" address = ip_lib.IPDevice(self.br_name, self.root_helper).link.address if address: return address else: msg = _('Unable to determine mac address for %s') % self.br_name raise Exception(msg) def __enter__(self): self.create() return self def __exit__(self, exc_type, exc_value, exc_tb): self.destroy() def get_bridge_for_iface(root_helper, iface): args = ["ovs-vsctl", "--timeout=%d" % cfg.CONF.ovs_vsctl_timeout, "iface-to-br", iface] try: return utils.execute(args, root_helper=root_helper).strip() except Exception: LOG.exception(_("Interface %s not found."), iface) return None def get_bridges(root_helper): args = ["ovs-vsctl", "--timeout=%d" % cfg.CONF.ovs_vsctl_timeout, "list-br"] try: return utils.execute(args, root_helper=root_helper).strip().split("\n") except Exception as e: with excutils.save_and_reraise_exception(): LOG.exception(_("Unable to retrieve bridges. Exception: %s"), e) def get_bridge_external_bridge_id(root_helper, bridge): args = ["ovs-vsctl", "--timeout=2", "br-get-external-id", bridge, "bridge-id"] try: return utils.execute(args, root_helper=root_helper).strip() except Exception: LOG.exception(_("Bridge %s not found."), bridge) return None def _build_flow_expr_str(flow_dict, cmd): flow_expr_arr = [] actions = None if cmd == 'add': flow_expr_arr.append("hard_timeout=%s" % flow_dict.pop('hard_timeout', '0')) flow_expr_arr.append("idle_timeout=%s" % flow_dict.pop('idle_timeout', '0')) flow_expr_arr.append("priority=%s" % flow_dict.pop('priority', '1')) elif 'priority' in flow_dict: msg = _("Cannot match priority on flow deletion or modification") raise exceptions.InvalidInput(error_message=msg) if cmd != 'del': if "actions" not in flow_dict: msg = _("Must specify one or more actions on flow addition" " or modification") raise exceptions.InvalidInput(error_message=msg) actions = "actions=%s" % flow_dict.pop('actions') for key, value in flow_dict.iteritems(): if key == 'proto': flow_expr_arr.append(value) else: flow_expr_arr.append("%s=%s" % (key, str(value))) if actions: flow_expr_arr.append(actions) return ','.join(flow_expr_arr) def ofctl_arg_supported(root_helper, cmd, args): '''Verify if ovs-ofctl binary supports command with specific args. :param root_helper: utility to use when running shell cmds. :param cmd: ovs-vsctl command to use for test. :param args: arguments to test with command. :returns: a boolean if the args supported. ''' supported = True br_name = 'br-test-%s' % common_utils.get_random_string(6) test_br = OVSBridge(br_name, root_helper) test_br.reset_bridge() full_args = ["ovs-ofctl", cmd, test_br.br_name] + args try: utils.execute(full_args, root_helper=root_helper) except Exception: supported = False test_br.destroy() return supported