From fdb3f050553455ec199b32f43ec0a551ba3d1d1c Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Thu, 28 Nov 2019 15:36:58 +0000 Subject: [PATCH] [OVN] Import ovsdb related code (part 2) This patch imports ovsdb related code from networking_ovn. Previous paths in networking-ovn tree: ./networking_ovn/ovsdb/ovsdb_monitor.py -> ./neutron/ovsdb/ovn/ovsdb_monitor.py ./networking_ovn/ovsdb/impl_idl_ovn.py -> ./neutron/ovsdb/ovn/impl_idl_ovn.py Change-Id: I5fe40a3b3e62e8c2adeb6660d1673dee49fe4965 Related-Blueprint: neutron-ovn-merge Signed-off-by: Lucas Alvares Gomes Co-Authored-By: Rodolfo Alonso Hernandez --- .../ovn/mech_driver/ovsdb/impl_idl_ovn.py | 840 ++++++++++++++++++ .../ovn/mech_driver/ovsdb/ovsdb_monitor.py | 465 ++++++++++ .../ovsdb/schemas/ovn-nb.ovsschema | 449 ++++++++++ .../ovsdb/schemas/ovn-sb.ovsschema | 404 +++++++++ .../mech_driver/ovsdb/test_impl_idl_ovn.py | 773 ++++++++++++++++ .../mech_driver/ovsdb/test_ovsdb_monitor.py | 294 ++++++ 6 files changed, 3225 insertions(+) create mode 100644 neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py create mode 100644 neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/schemas/ovn-nb.ovsschema create mode 100644 neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/schemas/ovn-sb.ovsschema create mode 100644 neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py new file mode 100644 index 00000000000..7fff3a2c355 --- /dev/null +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py @@ -0,0 +1,840 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import contextlib +import uuid + +from neutron_lib import exceptions as n_exc +from neutron_lib.utils import helpers +from oslo_log import log +from oslo_utils import uuidutils +from ovsdbapp.backend import ovs_idl +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp.backend.ovs_idl import transaction as idl_trans +from ovsdbapp.backend.ovs_idl import vlog +from ovsdbapp.schema.ovn_northbound import impl_idl as nb_impl_idl +from ovsdbapp.schema.ovn_southbound import impl_idl as sb_impl_idl +import tenacity + +from neutron._i18n import _ +from neutron.common.ovn import constants as ovn_const +from neutron.common.ovn import exceptions as ovn_exc +from neutron.common.ovn import utils +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as cfg +from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import commands as cmd +from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor + + +LOG = log.getLogger(__name__) + + +class OvnNbTransaction(idl_trans.Transaction): + + def __init__(self, *args, **kwargs): + # NOTE(lucasagomes): The bump_nb_cfg parameter is only used by + # the agents health status check + self.bump_nb_cfg = kwargs.pop('bump_nb_cfg', False) + super(OvnNbTransaction, self).__init__(*args, **kwargs) + + def pre_commit(self, txn): + if not self.bump_nb_cfg: + return + self.api.nb_global.increment('nb_cfg') + + +# This version of Backend doesn't use a class variable for ovsdb_connection +# and therefor allows networking-ovn to manage connection scope on its own +class Backend(ovs_idl.Backend): + lookup_table = {} + + def __init__(self, connection): + self.ovsdb_connection = connection + super(Backend, self).__init__(connection) + + def start_connection(self, connection): + try: + self.ovsdb_connection.start() + except Exception as e: + connection_exception = OvsdbConnectionUnavailable( + db_schema=self.schema, error=e) + LOG.exception(connection_exception) + raise connection_exception + + @property + def idl(self): + return self.ovsdb_connection.idl + + @property + def tables(self): + return self.idl.tables + + _tables = tables + + def is_table_present(self, table_name): + return table_name in self._tables + + def is_col_present(self, table_name, col_name): + return self.is_table_present(table_name) and ( + col_name in self._tables[table_name].columns) + + def create_transaction(self, check_error=False, log_errors=True): + return idl_trans.Transaction( + self, self.ovsdb_connection, self.ovsdb_connection.timeout, + check_error, log_errors) + + # Check for a column match in the table. If not found do a retry with + # a stop delay of 10 secs. This function would be useful if the caller + # wants to verify for the presence of a particular row in the table + # with the column match before doing any transaction. + # Eg. We can check if Logical_Switch row is present before adding a + # logical switch port to it. + @tenacity.retry(retry=tenacity.retry_if_exception_type(RuntimeError), + wait=tenacity.wait_exponential(), + stop=tenacity.stop_after_delay(10), + reraise=True) + def check_for_row_by_value_and_retry(self, table, column, match): + try: + idlutils.row_by_value(self.idl, table, column, match) + except idlutils.RowNotFound: + msg = (_("%(match)s does not exist in %(column)s of %(table)s") + % {'match': match, 'column': column, 'table': table}) + raise RuntimeError(msg) + + +class OvsdbConnectionUnavailable(n_exc.ServiceUnavailable): + message = _("OVS database connection to %(db_schema)s failed with error: " + "'%(error)s'. Verify that the OVS and OVN services are " + "available and that the 'ovn_nb_connection' and " + "'ovn_sb_connection' configuration options are correct.") + + +# Retry forever to get the OVN NB and SB IDLs. Wait 2^x * 1 seconds between +# each retry, up to 'max_interval' seconds, then interval will be fixed +# to 'max_interval' seconds afterwards. The default 'max_interval' is 180. +def get_ovn_idls(driver, trigger, binding_events=False): + @tenacity.retry( + wait=tenacity.wait_exponential( + max=cfg.get_ovn_ovsdb_retry_max_interval()), + reraise=True) + def get_ovn_idl_retry(cls): + trigger_class = utils.get_method_class(trigger) + LOG.info('Getting %(cls)s for %(trigger)s with retry', + {'cls': cls.__name__, 'trigger': trigger_class.__name__}) + return cls(get_connection(cls, trigger, driver, binding_events)) + + vlog.use_python_logger(max_level=cfg.get_ovn_ovsdb_log_level()) + return tuple(get_ovn_idl_retry(c) for c in (OvsdbNbOvnIdl, OvsdbSbOvnIdl)) + + +def get_connection(db_class, trigger=None, driver=None, binding_events=False): + if db_class == OvsdbNbOvnIdl: + args = (cfg.get_ovn_nb_connection(), 'OVN_Northbound') + elif db_class == OvsdbSbOvnIdl: + args = (cfg.get_ovn_sb_connection(), 'OVN_Southbound') + + if binding_events: + if db_class == OvsdbNbOvnIdl: + idl_ = ovsdb_monitor.OvnNbIdl.from_server(*args, driver=driver) + else: + idl_ = ovsdb_monitor.OvnSbIdl.from_server(*args, driver=driver) + else: + if db_class == OvsdbNbOvnIdl: + idl_ = ovsdb_monitor.BaseOvnIdl.from_server(*args) + else: + idl_ = ovsdb_monitor.BaseOvnSbIdl.from_server(*args) + + return connection.Connection(idl_, timeout=cfg.get_ovn_ovsdb_timeout()) + + +class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend): + def __init__(self, connection): + super(OvsdbNbOvnIdl, self).__init__(connection) + self.idl._session.reconnect.set_probe_interval( + cfg.get_ovn_ovsdb_probe_interval()) + + @property + def nb_global(self): + return next(iter(self.tables['NB_Global'].rows.values())) + + def create_transaction(self, check_error=False, log_errors=True, + bump_nb_cfg=False): + return OvnNbTransaction( + self, self.ovsdb_connection, self.ovsdb_connection.timeout, + check_error, log_errors, bump_nb_cfg=bump_nb_cfg) + + @contextlib.contextmanager + def transaction(self, *args, **kwargs): + """A wrapper on the ovsdbapp transaction to work with revisions. + + This method is just a wrapper around the ovsdbapp transaction + to handle revision conflicts correctly. + """ + try: + with super(OvsdbNbOvnIdl, self).transaction(*args, **kwargs) as t: + yield t + except ovn_exc.RevisionConflict as e: + LOG.info('Transaction aborted. Reason: %s', e) + + def set_lswitch_ext_ids(self, lswitch_id, ext_ids, if_exists=True): + return cmd.LSwitchSetExternalIdsCommand(self, lswitch_id, ext_ids, + if_exists) + + def create_lswitch_port(self, lport_name, lswitch_name, may_exist=True, + **columns): + return cmd.AddLSwitchPortCommand(self, lport_name, lswitch_name, + may_exist, **columns) + + def set_lswitch_port(self, lport_name, if_exists=True, **columns): + return cmd.SetLSwitchPortCommand(self, lport_name, + if_exists, **columns) + + def delete_lswitch_port(self, lport_name=None, lswitch_name=None, + ext_id=None, if_exists=True): + if lport_name is not None: + return cmd.DelLSwitchPortCommand(self, lport_name, + lswitch_name, if_exists) + else: + raise RuntimeError(_("Currently only supports " + "delete by lport-name")) + + def get_all_logical_switches_with_ports(self): + result = [] + for lswitch in self._tables['Logical_Switch'].rows.values(): + if ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY not in ( + lswitch.external_ids): + continue + ports = [] + provnet_port = None + for lport in getattr(lswitch, 'ports', []): + if ovn_const.OVN_PORT_NAME_EXT_ID_KEY in lport.external_ids: + ports.append(lport.name) + # Handle provider network port + elif lport.name.startswith( + ovn_const.OVN_PROVNET_PORT_NAME_PREFIX): + provnet_port = lport.name + result.append({'name': lswitch.name, + 'ports': ports, + 'provnet_port': provnet_port}) + return result + + def get_all_logical_routers_with_rports(self): + """Get logical Router ports associated with all logical Routers + + @return: list of dict, each dict has key-value: + - 'name': string router_id in neutron. + - 'static_routes': list of static routes dict. + - 'ports': dict of port_id in neutron (key) and networks on + port (value). + - 'snats': list of snats dict + - 'dnat_and_snats': list of dnat_and_snats dict + """ + result = [] + for lrouter in self._tables['Logical_Router'].rows.values(): + if ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY not in ( + lrouter.external_ids): + continue + lrports = {lrport.name.replace('lrp-', ''): lrport.networks + for lrport in getattr(lrouter, 'ports', [])} + sroutes = [{'destination': sroute.ip_prefix, + 'nexthop': sroute.nexthop} + for sroute in getattr(lrouter, 'static_routes', [])] + + dnat_and_snats = [] + snat = [] + for nat in getattr(lrouter, 'nat', []): + columns = {'logical_ip': nat.logical_ip, + 'external_ip': nat.external_ip, + 'type': nat.type} + if nat.type == 'dnat_and_snat': + if nat.external_mac: + columns['external_mac'] = nat.external_mac[0] + if nat.logical_port: + columns['logical_port'] = nat.logical_port[0] + dnat_and_snats.append(columns) + elif nat.type == 'snat': + snat.append(columns) + + result.append({'name': lrouter.name.replace('neutron-', ''), + 'static_routes': sroutes, + 'ports': lrports, + 'snats': snat, + 'dnat_and_snats': dnat_and_snats}) + return result + + def get_acl_by_id(self, acl_id): + try: + return self.lookup('ACL', uuid.UUID(acl_id)) + except idlutils.RowNotFound: + return + + def get_acls_for_lswitches(self, lswitch_names): + """Get the existing set of acls that belong to the logical switches + + @param lswitch_names: List of logical switch names + @type lswitch_names: [] + @var acl_values_dict: A dictionary indexed by port_id containing the + list of acl values in string format that belong + to that port + @var acl_obj_dict: A dictionary indexed by acl value containing the + corresponding acl idl object. + @var lswitch_ovsdb_dict: A dictionary mapping from logical switch + name to lswitch idl object + @return: (acl_values_dict, acl_obj_dict, lswitch_ovsdb_dict) + """ + acl_values_dict = {} + acl_obj_dict = {} + lswitch_ovsdb_dict = {} + for lswitch_name in lswitch_names: + try: + lswitch = idlutils.row_by_value(self.idl, + 'Logical_Switch', + 'name', + utils.ovn_name(lswitch_name)) + except idlutils.RowNotFound: + # It is possible for the logical switch to be deleted + # while we are searching for it by name in idl. + continue + lswitch_ovsdb_dict[lswitch_name] = lswitch + acls = getattr(lswitch, 'acls', []) + + # Iterate over each acl in a lswitch and store the acl in + # a key:value representation for e.g. acl_string. This + # key:value representation can invoke the code - + # self._ovn.add_acl(**acl_string) + for acl in acls: + ext_ids = getattr(acl, 'external_ids', {}) + port_id = ext_ids.get('neutron:lport') + acl_list = acl_values_dict.setdefault(port_id, []) + acl_string = {'lport': port_id, + 'lswitch': utils.ovn_name(lswitch_name)} + for acl_key in getattr(acl, "_data", {}): + try: + acl_string[acl_key] = getattr(acl, acl_key) + except AttributeError: + pass + acl_obj_dict[str(acl_string)] = acl + acl_list.append(acl_string) + return acl_values_dict, acl_obj_dict, lswitch_ovsdb_dict + + def create_lrouter(self, name, may_exist=True, **columns): + return cmd.AddLRouterCommand(self, name, + may_exist, **columns) + + def update_lrouter(self, name, if_exists=True, **columns): + return cmd.UpdateLRouterCommand(self, name, + if_exists, **columns) + + def delete_lrouter(self, name, if_exists=True): + return cmd.DelLRouterCommand(self, name, if_exists) + + def add_lrouter_port(self, name, lrouter, may_exist=False, **columns): + return cmd.AddLRouterPortCommand(self, name, lrouter, + may_exist, **columns) + + def update_lrouter_port(self, name, if_exists=True, **columns): + return cmd.UpdateLRouterPortCommand(self, name, if_exists, **columns) + + def delete_lrouter_port(self, name, lrouter, if_exists=True): + return cmd.DelLRouterPortCommand(self, name, lrouter, + if_exists) + + def set_lrouter_port_in_lswitch_port( + self, lswitch_port, lrouter_port, is_gw_port=False, if_exists=True, + lsp_address=ovn_const.DEFAULT_ADDR_FOR_LSP_WITH_PEER): + return cmd.SetLRouterPortInLSwitchPortCommand(self, lswitch_port, + lrouter_port, is_gw_port, + if_exists, + lsp_address) + + def add_acl(self, lswitch, lport, **columns): + return cmd.AddACLCommand(self, lswitch, lport, **columns) + + def delete_acl(self, lswitch, lport, if_exists=True): + return cmd.DelACLCommand(self, lswitch, lport, if_exists) + + def update_acls(self, lswitch_names, port_list, acl_new_values_dict, + need_compare=True, is_add_acl=True): + return cmd.UpdateACLsCommand(self, lswitch_names, + port_list, acl_new_values_dict, + need_compare=need_compare, + is_add_acl=is_add_acl) + + def add_static_route(self, lrouter, **columns): + return cmd.AddStaticRouteCommand(self, lrouter, **columns) + + def delete_static_route(self, lrouter, ip_prefix, nexthop, if_exists=True): + return cmd.DelStaticRouteCommand(self, lrouter, ip_prefix, nexthop, + if_exists) + + def create_address_set(self, name, may_exist=True, **columns): + return cmd.AddAddrSetCommand(self, name, may_exist, **columns) + + def delete_address_set(self, name, if_exists=True, **columns): + return cmd.DelAddrSetCommand(self, name, if_exists) + + def update_address_set(self, name, addrs_add, addrs_remove, + if_exists=True): + return cmd.UpdateAddrSetCommand(self, name, addrs_add, addrs_remove, + if_exists) + + def update_address_set_ext_ids(self, name, external_ids, if_exists=True): + return cmd.UpdateAddrSetExtIdsCommand(self, name, external_ids, + if_exists) + + def _get_logical_router_port_gateway_chassis(self, lrp): + """Get the list of chassis hosting this gateway port. + + @param lrp: logical router port + @type lrp: Logical_Router_Port row + @return: List of tuples (chassis_name, priority) sorted by priority + """ + # Try retrieving gateway_chassis with new schema. If new schema is not + # supported or user is using old schema, then use old schema for + # getting gateway_chassis + chassis = [] + if self._tables.get('Gateway_Chassis'): + for gwc in lrp.gateway_chassis: + chassis.append((gwc.chassis_name, gwc.priority)) + else: + rc = lrp.options.get(ovn_const.OVN_GATEWAY_CHASSIS_KEY) + if rc: + chassis.append((rc, 0)) + # make sure that chassis are sorted by priority + return sorted(chassis, reverse=True, key=lambda x: x[1]) + + def get_all_chassis_gateway_bindings(self, + chassis_candidate_list=None): + chassis_bindings = {} + for chassis_name in chassis_candidate_list or []: + chassis_bindings.setdefault(chassis_name, []) + for lrp in self._tables['Logical_Router_Port'].rows.values(): + if not lrp.name.startswith('lrp-'): + continue + chassis = self._get_logical_router_port_gateway_chassis(lrp) + for chassis_name, prio in chassis: + if (not chassis_candidate_list or + chassis_name in chassis_candidate_list): + routers_hosted = chassis_bindings.setdefault(chassis_name, + []) + routers_hosted.append((lrp.name, prio)) + return chassis_bindings + + def get_gateway_chassis_binding(self, gateway_name): + try: + lrp = idlutils.row_by_value( + self.idl, 'Logical_Router_Port', 'name', gateway_name) + chassis_list = self._get_logical_router_port_gateway_chassis(lrp) + return [chassis for chassis, prio in chassis_list] + except idlutils.RowNotFound: + return [] + + def get_unhosted_gateways(self, port_physnet_dict, chassis_physnets, + gw_chassis): + unhosted_gateways = [] + for lrp in self._tables['Logical_Router_Port'].rows.values(): + if not lrp.name.startswith('lrp-'): + continue + physnet = port_physnet_dict.get(lrp.name[len('lrp-'):]) + chassis_list = self._get_logical_router_port_gateway_chassis(lrp) + is_max_gw_reached = len(chassis_list) < ovn_const.MAX_GW_CHASSIS + for chassis_name, prio in chassis_list: + # TODO(azbiswas): Handle the case when a chassis is no + # longer valid. This may involve moving conntrack states, + # so it needs to discussed in the OVN community first. + if is_max_gw_reached or utils.is_gateway_chassis_invalid( + chassis_name, gw_chassis, physnet, chassis_physnets): + unhosted_gateways.append(lrp.name) + return unhosted_gateways + + def add_dhcp_options(self, subnet_id, port_id=None, may_exist=True, + **columns): + return cmd.AddDHCPOptionsCommand(self, subnet_id, port_id=port_id, + may_exist=may_exist, **columns) + + def delete_dhcp_options(self, row_uuid, if_exists=True): + return cmd.DelDHCPOptionsCommand(self, row_uuid, if_exists=if_exists) + + def _format_dhcp_row(self, row): + ext_ids = dict(getattr(row, 'external_ids', {})) + return {'cidr': row.cidr, 'options': dict(row.options), + 'external_ids': ext_ids, 'uuid': row.uuid} + + def get_subnet_dhcp_options(self, subnet_id, with_ports=False): + subnet = None + ports = [] + for row in self._tables['DHCP_Options'].rows.values(): + external_ids = getattr(row, 'external_ids', {}) + if subnet_id == external_ids.get('subnet_id'): + port_id = external_ids.get('port_id') + if with_ports and port_id: + ports.append(self._format_dhcp_row(row)) + elif not port_id: + subnet = self._format_dhcp_row(row) + if not with_ports: + break + return {'subnet': subnet, 'ports': ports} + + def get_subnets_dhcp_options(self, subnet_ids): + ret_opts = [] + for row in self._tables['DHCP_Options'].rows.values(): + external_ids = getattr(row, 'external_ids', {}) + if (external_ids.get('subnet_id') in subnet_ids and not + external_ids.get('port_id')): + ret_opts.append(self._format_dhcp_row(row)) + if len(ret_opts) == len(subnet_ids): + break + return ret_opts + + def get_all_dhcp_options(self): + dhcp_options = {'subnets': {}, 'ports_v4': {}, 'ports_v6': {}} + + for row in self._tables['DHCP_Options'].rows.values(): + external_ids = getattr(row, 'external_ids', {}) + if not external_ids.get('subnet_id'): + # This row is not created by OVN ML2 driver. Ignore it. + continue + + if not external_ids.get('port_id'): + dhcp_options['subnets'][external_ids['subnet_id']] = ( + self._format_dhcp_row(row)) + else: + port_dict = 'ports_v6' if ':' in row.cidr else 'ports_v4' + dhcp_options[port_dict][external_ids['port_id']] = ( + self._format_dhcp_row(row)) + + return dhcp_options + + def get_address_sets(self): + address_sets = {} + for row in self._tables['Address_Set'].rows.values(): + # TODO(lucasagomes): Remove OVN_SG_NAME_EXT_ID_KEY in the + # Rocky release + if not (ovn_const.OVN_SG_EXT_ID_KEY in row.external_ids or + ovn_const.OVN_SG_NAME_EXT_ID_KEY in row.external_ids): + continue + name = getattr(row, 'name') + data = {} + for row_key in getattr(row, "_data", {}): + data[row_key] = getattr(row, row_key) + address_sets[name] = data + return address_sets + + def get_router_port_options(self, lsp_name): + try: + lsp = idlutils.row_by_value(self.idl, 'Logical_Switch_Port', + 'name', lsp_name) + options = getattr(lsp, 'options') + for key in list(options.keys()): + if key not in ovn_const.OVN_ROUTER_PORT_OPTION_KEYS: + del(options[key]) + return options + except idlutils.RowNotFound: + return {} + + def add_nat_rule_in_lrouter(self, lrouter, **columns): + return cmd.AddNATRuleInLRouterCommand(self, lrouter, **columns) + + def delete_nat_rule_in_lrouter(self, lrouter, type, logical_ip, + external_ip, if_exists=True): + return cmd.DeleteNATRuleInLRouterCommand(self, lrouter, type, + logical_ip, external_ip, + if_exists) + + def get_lrouter_nat_rules(self, lrouter_name): + try: + lrouter = idlutils.row_by_value(self.idl, 'Logical_Router', + 'name', lrouter_name) + except idlutils.RowNotFound: + msg = _("Logical Router %s does not exist") % lrouter_name + raise RuntimeError(msg) + + nat_rules = [] + for nat_rule in getattr(lrouter, 'nat', []): + ext_ids = {} + # TODO(dalvarez): remove this check once the minimum OVS required + # version contains the column (when OVS 2.8.2 is released). + if self.is_col_present('NAT', 'external_ids'): + ext_ids = dict(getattr(nat_rule, 'external_ids', {})) + + nat_rules.append({'external_ip': nat_rule.external_ip, + 'logical_ip': nat_rule.logical_ip, + 'type': nat_rule.type, + 'uuid': nat_rule.uuid, + 'external_ids': ext_ids}) + return nat_rules + + def set_nat_rule_in_lrouter(self, lrouter, nat_rule_uuid, **columns): + return cmd.SetNATRuleInLRouterCommand(self, lrouter, nat_rule_uuid, + **columns) + + def get_lswitch_port(self, lsp_name): + try: + return self.lookup('Logical_Switch_Port', lsp_name) + except idlutils.RowNotFound: + return None + + def get_parent_port(self, lsp_name): + lsp = self.get_lswitch_port(lsp_name) + if not lsp: + return '' + return lsp.parent_name + + def get_lswitch(self, lswitch_name): + # FIXME(lucasagomes): We should refactor those get_*() + # methods. Some of 'em require the name, others IDs etc... It can + # be confusing. + if uuidutils.is_uuid_like(lswitch_name): + lswitch_name = utils.ovn_name(lswitch_name) + + try: + return self.lookup('Logical_Switch', lswitch_name) + except idlutils.RowNotFound: + return None + + def get_ls_and_dns_record(self, lswitch_name): + ls = self.get_lswitch(lswitch_name) + if not ls: + return (None, None) + + if not hasattr(ls, 'dns_records'): + return (ls, None) + + for dns_row in ls.dns_records: + if dns_row.external_ids.get('ls_name') == lswitch_name: + return (ls, dns_row) + + return (ls, None) + + def get_floatingip(self, fip_id): + # TODO(dalvarez): remove this check once the minimum OVS required + # version contains the column (when OVS 2.8.2 is released). + if not self.is_col_present('NAT', 'external_ids'): + return + + fip = self.db_find('NAT', ('external_ids', '=', + {ovn_const.OVN_FIP_EXT_ID_KEY: fip_id})) + result = fip.execute(check_error=True) + return result[0] if result else None + + def get_floatingip_by_ips(self, router_id, logical_ip, external_ip): + if not all([router_id, logical_ip, external_ip]): + return + + for nat in self.get_lrouter_nat_rules(utils.ovn_name(router_id)): + if (nat['type'] == 'dnat_and_snat' and + nat['logical_ip'] == logical_ip and + nat['external_ip'] == external_ip): + return nat + + def get_address_set(self, addrset_id, ip_version='ip4'): + addr_name = utils.ovn_addrset_name(addrset_id, ip_version) + try: + return idlutils.row_by_value(self.idl, 'Address_Set', + 'name', addr_name) + except idlutils.RowNotFound: + return None + + def check_revision_number(self, name, resource, resource_type, + if_exists=True): + return cmd.CheckRevisionNumberCommand( + self, name, resource, resource_type, if_exists) + + def get_lrouter(self, lrouter_name): + if uuidutils.is_uuid_like(lrouter_name): + lrouter_name = utils.ovn_name(lrouter_name) + + # TODO(lucasagomes): Use lr_get() once we start refactoring this + # API to use methods from ovsdbapp. + lr = self.db_find_rows('Logical_Router', ('name', '=', lrouter_name)) + result = lr.execute(check_error=True) + return result[0] if result else None + + def get_lrouter_port(self, lrp_name): + # TODO(mangelajo): Implement lrp_get() ovsdbapp and use from here + if uuidutils.is_uuid_like(lrp_name): + lrp_name = utils.ovn_lrouter_port_name(lrp_name) + lrp = self.db_find_rows('Logical_Router_Port', ('name', '=', lrp_name)) + result = lrp.execute(check_error=True) + return result[0] if result else None + + def delete_lrouter_ext_gw(self, lrouter_name, if_exists=True): + return cmd.DeleteLRouterExtGwCommand(self, lrouter_name, if_exists) + + def is_port_groups_supported(self): + return self.is_table_present('Port_Group') + + def get_port_group(self, pg_name): + if uuidutils.is_uuid_like(pg_name): + pg_name = utils.ovn_port_group_name(pg_name) + try: + for pg in self._tables['Port_Group'].rows.values(): + if pg.name == pg_name: + return pg + except KeyError: + # TODO(dalvarez): This except block is added for backwards compat + # with old OVN schemas (<=2.9) where Port Groups are not present. + # This (and other conditional code around this feature) shall be + # removed at some point. + return + + def get_port_groups(self): + port_groups = {} + try: + for row in self._tables['Port_Group'].rows.values(): + name = getattr(row, 'name') + if not (ovn_const.OVN_SG_EXT_ID_KEY in row.external_ids or + name == ovn_const.OVN_DROP_PORT_GROUP_NAME): + continue + data = {} + for row_key in getattr(row, "_data", {}): + data[row_key] = getattr(row, row_key) + port_groups[name] = data + except KeyError: + # TODO(dalvarez): This except block is added for backwards compat + # with old OVN schemas (<=2.9) where Port Groups are not present. + # This (and other conditional code around this feature) shall be + # removed at some point. + pass + return port_groups + + def check_liveness(self): + return cmd.CheckLivenessCommand(self) + + def set_lswitch_port_to_virtual_type(self, lport_name, vip, + virtual_parent, if_exists=True): + return cmd.SetLSwitchPortToVirtualTypeCommand( + self, lport_name, vip, virtual_parent, if_exists) + + def unset_lswitch_port_to_virtual_type(self, lport_name, + virtual_parent, if_exists=True): + return cmd.UnsetLSwitchPortToVirtualTypeCommand( + self, lport_name, virtual_parent, if_exists) + + +class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): + def __init__(self, connection): + super(OvsdbSbOvnIdl, self).__init__(connection) + # TODO(twilson) This direct access of the idl should be removed in + # favor of a backend-agnostic method + self.idl._session.reconnect.set_probe_interval( + cfg.get_ovn_ovsdb_probe_interval()) + + def _get_chassis_physnets(self, chassis): + bridge_mappings = chassis.external_ids.get('ovn-bridge-mappings', '') + mapping_dict = helpers.parse_mappings(bridge_mappings.split(','), + unique_values=False) + return list(mapping_dict.keys()) + + def chassis_exists(self, hostname): + cmd = self.db_find('Chassis', ('hostname', '=', hostname)) + return bool(cmd.execute(check_error=True)) + + def get_chassis_hostname_and_physnets(self): + chassis_info_dict = {} + for ch in self.chassis_list().execute(check_error=True): + chassis_info_dict[ch.hostname] = self._get_chassis_physnets(ch) + return chassis_info_dict + + def get_gateway_chassis_from_cms_options(self): + gw_chassis = [] + for ch in self.chassis_list().execute(check_error=True): + cms_options = ch.external_ids.get('ovn-cms-options', '') + if 'enable-chassis-as-gw' in cms_options.split(','): + gw_chassis.append(ch.name) + return gw_chassis + + def get_chassis_and_physnets(self): + chassis_info_dict = {} + for ch in self.chassis_list().execute(check_error=True): + chassis_info_dict[ch.name] = self._get_chassis_physnets(ch) + return chassis_info_dict + + def get_all_chassis(self, chassis_type=None): + # TODO(azbiswas): Use chassis_type as input once the compute type + # preference patch (as part of external ids) merges. + return [c.name for c in self.chassis_list().execute(check_error=True)] + + def get_chassis_data_for_ml2_bind_port(self, hostname): + try: + cmd = self.db_find_rows('Chassis', ('hostname', '=', hostname)) + chassis = next(c for c in cmd.execute(check_error=True)) + except StopIteration: + msg = _('Chassis with hostname %s does not exist') % hostname + raise RuntimeError(msg) + return (chassis.external_ids.get('datapath-type', ''), + chassis.external_ids.get('iface-types', ''), + self._get_chassis_physnets(chassis)) + + def get_metadata_port_network(self, network): + # TODO(twilson) This function should really just take a Row/RowView + try: + dp = self.lookup('Datapath_Binding', uuid.UUID(network)) + except idlutils.RowNotFound: + return None + cmd = self.db_find_rows('Port_Binding', ('datapath', '=', dp), + ('type', '=', 'localport')) + return next(iter(cmd.execute(check_error=True)), None) + + def get_chassis_metadata_networks(self, chassis_name): + """Return a list with the metadata networks the chassis is hosting.""" + chassis = self.lookup('Chassis', chassis_name) + proxy_networks = chassis.external_ids.get( + 'neutron-metadata-proxy-networks', None) + return proxy_networks.split(',') if proxy_networks else [] + + def set_chassis_metadata_networks(self, chassis, networks): + nets = ','.join(networks) if networks else '' + # TODO(twilson) This could just use DbSetCommand + return cmd.UpdateChassisExtIdsCommand( + self, chassis, {'neutron-metadata-proxy-networks': nets}, + if_exists=True) + + def set_chassis_neutron_description(self, chassis, description, + agent_type): + desc_key = (ovn_const.OVN_AGENT_METADATA_DESC_KEY + if agent_type == ovn_const.OVN_METADATA_AGENT else + ovn_const.OVN_AGENT_DESC_KEY) + return cmd.UpdateChassisExtIdsCommand( + self, chassis, {desc_key: description}, if_exists=False) + + def get_network_port_bindings_by_ip(self, network, ip_address): + rows = self.db_list_rows('Port_Binding').execute(check_error=True) + # TODO(twilson) It would be useful to have a db_find that takes a + # comparison function + return [r for r in rows + if (r.mac and str(r.datapath.uuid) == network) and + ip_address in r.mac[0].split(' ')] + + def update_metadata_health_status(self, chassis, nb_cfg): + return cmd.UpdateChassisExtIdsCommand( + self, chassis, + {ovn_const.OVN_AGENT_METADATA_SB_CFG_KEY: str(nb_cfg)}, + if_exists=True) + + def set_port_cidrs(self, name, cidrs): + # TODO(twilson) add if_exists to db commands + return self.db_set('Port_Binding', name, 'external_ids', + {'neutron-port-cidrs': cidrs}) + + def get_ports_on_chassis(self, chassis): + # TODO(twilson) Some day it would be nice to stop passing names around + # and just start using chassis objects so db_find_rows could be used + rows = self.db_list_rows('Port_Binding').execute(check_error=True) + return [r for r in rows if r.chassis and r.chassis[0].name == chassis] + + def get_logical_port_chassis_and_datapath(self, name): + for port in self._tables['Port_Binding'].rows.values(): + if port.logical_port == name: + datapath = str(port.datapath.uuid) + chassis = port.chassis[0].name if port.chassis else None + return chassis, datapath diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py new file mode 100644 index 00000000000..1d4dfcee997 --- /dev/null +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py @@ -0,0 +1,465 @@ +# Copyright 2016 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import datetime + +from neutron_lib import context as neutron_context +from neutron_lib.plugins import constants +from neutron_lib.plugins import directory +from neutron_lib.utils import helpers +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +from ovs.stream import Stream +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp.backend.ovs_idl import event as row_event +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp import event + +from neutron.common.ovn import constants as ovn_const +from neutron.common.ovn import exceptions +from neutron.common.ovn import hash_ring_manager +from neutron.common.ovn import utils +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from neutron.db import ovn_hash_ring_db + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class BaseEvent(row_event.RowEvent): + table = None + events = tuple() + + def __init__(self): + self.event_name = self.__class__.__name__ + super(BaseEvent, self).__init__(self.events, self.table, None) + + @abc.abstractmethod + def match_fn(self, event, row, old=None): + """Define match criteria other than table/event""" + + def matches(self, event, row, old=None): + if row._table.name != self.table or event not in self.events: + return False + if not self.match_fn(event, row, old): + return False + LOG.debug("%s : Matched %s, %s, %s %s", self.event_name, self.table, + event, self.conditions, self.old_conditions) + return True + + +class ChassisEvent(row_event.RowEvent): + """Chassis create update delete event.""" + + def __init__(self, driver): + self.driver = driver + self.l3_plugin = directory.get_plugin(constants.L3) + table = 'Chassis' + events = (self.ROW_CREATE, self.ROW_UPDATE, self.ROW_DELETE) + super(ChassisEvent, self).__init__(events, table, None) + self.event_name = 'ChassisEvent' + + def run(self, event, row, old): + host = row.hostname + phy_nets = [] + if event != self.ROW_DELETE: + bridge_mappings = row.external_ids.get('ovn-bridge-mappings', '') + mapping_dict = helpers.parse_mappings(bridge_mappings.split(','), + unique_values=False) + phy_nets = list(mapping_dict) + + self.driver.update_segment_host_mapping(host, phy_nets) + if utils.is_ovn_l3(self.l3_plugin): + self.l3_plugin.schedule_unhosted_gateways() + + +class PortBindingChassisUpdateEvent(row_event.RowEvent): + """Event for matching a port moving chassis + + If the LSP is up and the Port_Binding chassis has just changed, + there is a good chance the host died without cleaning up the chassis + column on the Port_Binding. The port never goes down, so we won't + see update the driver with the LogicalSwitchPortUpdateUpEvent which + only monitors for transitions from DOWN to UP. + """ + + def __init__(self, driver): + self.driver = driver + table = 'Port_Binding' + events = (self.ROW_UPDATE,) + super(PortBindingChassisUpdateEvent, self).__init__( + events, table, None) + self.event_name = self.__class__.__name__ + + def match_fn(self, event, row, old=None): + # NOTE(twilson) ROW_UPDATE events always pass old, but chassis will + # only be set if chassis has changed + old_chassis = getattr(old, 'chassis', None) + if not (row.chassis and old_chassis) or row.chassis == old_chassis: + return False + if row.type == ovn_const.OVN_CHASSIS_REDIRECT: + return False + try: + lsp = self.driver._nb_ovn.lookup('Logical_Switch_Port', + row.logical_port) + except idlutils.RowNotFound: + LOG.warning("Logical Switch Port %(port)s not found for " + "Port_Binding %(binding)s", + {'port': row.logical_port, 'binding': row.uuid}) + return False + + return bool(lsp.up) + + def run(self, event, row, old=None): + self.driver.set_port_status_up(row.logical_port) + + +class PortBindingChassisEvent(row_event.RowEvent): + """Port_Binding update event - set chassis for chassisredirect port. + + When a chassisredirect port is updated with chassis, this event get + generated. We will update corresponding router's gateway port with + the chassis's host_id. Later, users can check router's gateway port + host_id to find the location of master HA router. + """ + + def __init__(self, driver): + self.driver = driver + self.l3_plugin = directory.get_plugin(constants.L3) + table = 'Port_Binding' + events = (self.ROW_UPDATE,) + super(PortBindingChassisEvent, self).__init__( + events, table, (('type', '=', ovn_const.OVN_CHASSIS_REDIRECT),)) + self.event_name = 'PortBindingChassisEvent' + + def run(self, event, row, old): + if not utils.is_ovn_l3(self.l3_plugin): + return + router = host = None + chassis = getattr(row, 'chassis', None) + if chassis: + router = row.datapath.external_ids.get('name', '').replace( + 'neutron-', '') + host = chassis[0].hostname + LOG.info("Router %(router)s is bound to host %(host)s", + {'router': router, 'host': host}) + self.l3_plugin.update_router_gateway_port_bindings( + router, host) + + +class LogicalSwitchPortCreateUpEvent(row_event.RowEvent): + """Row create event - Logical_Switch_Port 'up' = True. + + On connection, we get a dump of all ports, so if there is a neutron + port that is down that has since been activated, we'll catch it here. + This event will not be generated for new ports getting created. + """ + + def __init__(self, driver): + self.driver = driver + table = 'Logical_Switch_Port' + events = (self.ROW_CREATE,) + super(LogicalSwitchPortCreateUpEvent, self).__init__( + events, table, (('up', '=', True),)) + self.event_name = 'LogicalSwitchPortCreateUpEvent' + + def run(self, event, row, old): + self.driver.set_port_status_up(row.name) + + +class LogicalSwitchPortCreateDownEvent(row_event.RowEvent): + """Row create event - Logical_Switch_Port 'up' = False + + On connection, we get a dump of all ports, so if there is a neutron + port that is up that has since been deactivated, we'll catch it here. + This event will not be generated for new ports getting created. + """ + def __init__(self, driver): + self.driver = driver + table = 'Logical_Switch_Port' + events = (self.ROW_CREATE,) + super(LogicalSwitchPortCreateDownEvent, self).__init__( + events, table, (('up', '=', False),)) + self.event_name = 'LogicalSwitchPortCreateDownEvent' + + def run(self, event, row, old): + self.driver.set_port_status_down(row.name) + + +class LogicalSwitchPortUpdateUpEvent(row_event.RowEvent): + """Row update event - Logical_Switch_Port 'up' going from False to True + + This happens when the VM goes up. + New value of Logical_Switch_Port 'up' will be True and the old value will + be False. + """ + def __init__(self, driver): + self.driver = driver + table = 'Logical_Switch_Port' + events = (self.ROW_UPDATE,) + super(LogicalSwitchPortUpdateUpEvent, self).__init__( + events, table, (('up', '=', True),), + old_conditions=(('up', '=', False),)) + self.event_name = 'LogicalSwitchPortUpdateUpEvent' + + def run(self, event, row, old): + self.driver.set_port_status_up(row.name) + + +class LogicalSwitchPortUpdateDownEvent(row_event.RowEvent): + """Row update event - Logical_Switch_Port 'up' going from True to False + + This happens when the VM goes down. + New value of Logical_Switch_Port 'up' will be False and the old value will + be True. + """ + def __init__(self, driver): + self.driver = driver + table = 'Logical_Switch_Port' + events = (self.ROW_UPDATE,) + super(LogicalSwitchPortUpdateDownEvent, self).__init__( + events, table, (('up', '=', False),), + old_conditions=(('up', '=', True),)) + self.event_name = 'LogicalSwitchPortUpdateDownEvent' + + def run(self, event, row, old): + self.driver.set_port_status_down(row.name) + + +class FIPAddDeleteEvent(row_event.RowEvent): + """Row event - NAT 'dnat_and_snat' entry added or deleted + + This happens when a FIP is created or removed. + """ + def __init__(self, driver): + self.driver = driver + table = 'NAT' + events = (self.ROW_CREATE, self.ROW_DELETE) + super(FIPAddDeleteEvent, self).__init__( + events, table, (('type', '=', 'dnat_and_snat'),)) + self.event_name = 'FIPAddDeleteEvent' + + def run(self, event, row, old): + # When a FIP is added or deleted, we will delete all entries in the + # MAC_Binding table of SB OVSDB corresponding to that IP Address. + # TODO(dalvarez): Remove this workaround once fixed in core OVN: + # https://mail.openvswitch.org/pipermail/ovs-discuss/2018-October/047604.html + self.driver.delete_mac_binding_entries(row.external_ip) + + +class OvnDbNotifyHandler(event.RowEventHandler): + def __init__(self, driver): + super(OvnDbNotifyHandler, self).__init__() + self.driver = driver + + +class BaseOvnIdl(connection.OvsdbIdl): + @classmethod + def from_server(cls, connection_string, schema_name): + _check_and_set_ssl_files(schema_name) + helper = idlutils.get_schema_helper(connection_string, schema_name) + helper.register_all() + return cls(connection_string, helper) + + +class BaseOvnSbIdl(connection.OvsdbIdl): + @classmethod + def from_server(cls, connection_string, schema_name): + _check_and_set_ssl_files(schema_name) + helper = idlutils.get_schema_helper(connection_string, schema_name) + helper.register_table('Chassis') + helper.register_table('Encap') + helper.register_table('Port_Binding') + helper.register_table('Datapath_Binding') + return cls(connection_string, helper) + + +class OvnIdl(BaseOvnIdl): + + def __init__(self, driver, remote, schema): + super(OvnIdl, self).__init__(remote, schema) + self.driver = driver + self.notify_handler = OvnDbNotifyHandler(driver) + # ovsdb lock name to acquire. + # This event lock is used to handle the notify events sent by idl.Idl + # idl.Idl will call notify function for the "update" rpc method it + # receives from the ovsdb-server. + # This event lock is required for the following reasons + # - If there are multiple neutron servers running, OvnWorkers of + # these neutron servers would receive the notify events from + # idl.Idl + # + # - we do not want all the neutron servers to handle these events + # + # - only the neutron server which has the lock will handle the + # notify events. + # + # - In case the neutron server which owns this lock goes down, + # ovsdb server would assign the lock to one of the other neutron + # servers. + self.event_lock_name = "neutron_ovn_event_lock" + + def notify(self, event, row, updates=None): + # Do not handle the notification if the event lock is requested, + # but not granted by the ovsdb-server. + if self.is_lock_contended: + return + self.notify_handler.notify(event, row, updates) + + @abc.abstractmethod + def post_connect(self): + """Should be called after the idl has been initialized""" + + +class OvnIdlDistributedLock(BaseOvnIdl): + + def __init__(self, driver, remote, schema): + super(OvnIdlDistributedLock, self).__init__(remote, schema) + self.driver = driver + self.notify_handler = OvnDbNotifyHandler(driver) + self._node_uuid = self.driver.node_uuid + self._hash_ring = hash_ring_manager.HashRingManager( + self.driver.hash_ring_group) + self._last_touch = None + + def notify(self, event, row, updates=None): + try: + target_node = self._hash_ring.get_node(str(row.uuid)) + except exceptions.HashRingIsEmpty as e: + LOG.error('HashRing is empty, error: %s', e) + return + + if target_node != self._node_uuid: + return + + # If the worker hasn't been health checked by the maintenance + # thread (see bug #1834498), indicate that it's alive here + time_now = timeutils.utcnow() + touch_timeout = time_now - datetime.timedelta( + seconds=ovn_const.HASH_RING_TOUCH_INTERVAL) + if not self._last_touch or touch_timeout >= self._last_touch: + # NOTE(lucasagomes): Guard the db operation with an exception + # handler. If heartbeating fails for whatever reason, log + # the error and continue with processing the event + try: + ctx = neutron_context.get_admin_context() + ovn_hash_ring_db.touch_node(ctx, self._node_uuid) + self._last_touch = time_now + except Exception: + LOG.exception('Hash Ring node %s failed to heartbeat', + self._node_uuid) + + LOG.debug('Hash Ring: Node %(node)s (host: %(hostname)s) ' + 'handling event "%(event)s" for row %(row)s ' + '(table: %(table)s)', + {'node': self._node_uuid, 'hostname': CONF.host, + 'event': event, 'row': row.uuid, 'table': row._table.name}) + self.notify_handler.notify(event, row, updates) + + @abc.abstractmethod + def post_connect(self): + """Should be called after the idl has been initialized""" + + +class OvnNbIdl(OvnIdlDistributedLock): + + def __init__(self, driver, remote, schema): + super(OvnNbIdl, self).__init__(driver, remote, schema) + self._lsp_update_up_event = LogicalSwitchPortUpdateUpEvent(driver) + self._lsp_update_down_event = LogicalSwitchPortUpdateDownEvent(driver) + self._lsp_create_up_event = LogicalSwitchPortCreateUpEvent(driver) + self._lsp_create_down_event = LogicalSwitchPortCreateDownEvent(driver) + self._fip_create_delete_event = FIPAddDeleteEvent(driver) + + self.notify_handler.watch_events([self._lsp_create_up_event, + self._lsp_create_down_event, + self._lsp_update_up_event, + self._lsp_update_down_event, + self._fip_create_delete_event]) + + @classmethod + def from_server(cls, connection_string, schema_name, driver): + + _check_and_set_ssl_files(schema_name) + helper = idlutils.get_schema_helper(connection_string, schema_name) + helper.register_all() + return cls(driver, connection_string, helper) + + def unwatch_logical_switch_port_create_events(self): + """Unwatch the logical switch port create events. + + When the ovs idl client connects to the ovsdb-server, it gets + a dump of all logical switch ports as events and we need to process + them at start up. + After the startup, there is no need to watch these events. + So unwatch these events. + """ + self.notify_handler.unwatch_events([self._lsp_create_up_event, + self._lsp_create_down_event]) + self._lsp_create_up_event = None + self._lsp_create_down_event = None + + def post_connect(self): + self.unwatch_logical_switch_port_create_events() + + +class OvnSbIdl(OvnIdlDistributedLock): + + @classmethod + def from_server(cls, connection_string, schema_name, driver): + _check_and_set_ssl_files(schema_name) + helper = idlutils.get_schema_helper(connection_string, schema_name) + helper.register_table('Chassis') + helper.register_table('Encap') + helper.register_table('Port_Binding') + helper.register_table('Datapath_Binding') + helper.register_table('MAC_Binding') + return cls(driver, connection_string, helper) + + def post_connect(self): + """Watch Chassis events. + + When the ovs idl client connects to the ovsdb-server, it gets + a dump of all Chassis create event. We don't need to process them + because there will be sync up at startup. After that, we will watch + the events to make notify work. + """ + self._chassis_event = ChassisEvent(self.driver) + self._portbinding_event = PortBindingChassisEvent(self.driver) + self.notify_handler.watch_events( + [self._chassis_event, self._portbinding_event, + PortBindingChassisUpdateEvent(self.driver)]) + + +def _check_and_set_ssl_files(schema_name): + if schema_name == 'OVN_Southbound': + priv_key_file = ovn_conf.get_ovn_sb_private_key() + cert_file = ovn_conf.get_ovn_sb_certificate() + ca_cert_file = ovn_conf.get_ovn_sb_ca_cert() + else: + priv_key_file = ovn_conf.get_ovn_nb_private_key() + cert_file = ovn_conf.get_ovn_nb_certificate() + ca_cert_file = ovn_conf.get_ovn_nb_ca_cert() + + if priv_key_file: + Stream.ssl_set_private_key_file(priv_key_file) + + if cert_file: + Stream.ssl_set_certificate_file(cert_file) + + if ca_cert_file: + Stream.ssl_set_ca_cert_file(ca_cert_file) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/schemas/ovn-nb.ovsschema b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/schemas/ovn-nb.ovsschema new file mode 100644 index 00000000000..2c87cbba713 --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/schemas/ovn-nb.ovsschema @@ -0,0 +1,449 @@ +{ + "name": "OVN_Northbound", + "version": "5.16.0", + "cksum": "923459061 23095", + "tables": { + "NB_Global": { + "columns": { + "nb_cfg": {"type": {"key": "integer"}}, + "sb_cfg": {"type": {"key": "integer"}}, + "hv_cfg": {"type": {"key": "integer"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "connections": { + "type": {"key": {"type": "uuid", + "refTable": "Connection"}, + "min": 0, + "max": "unlimited"}}, + "ssl": { + "type": {"key": {"type": "uuid", + "refTable": "SSL"}, + "min": 0, "max": 1}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "ipsec": {"type": "boolean"}}, + "maxRows": 1, + "isRoot": true}, + "Logical_Switch": { + "columns": { + "name": {"type": "string"}, + "ports": {"type": {"key": {"type": "uuid", + "refTable": "Logical_Switch_Port", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "acls": {"type": {"key": {"type": "uuid", + "refTable": "ACL", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "qos_rules": {"type": {"key": {"type": "uuid", + "refTable": "QoS", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "load_balancer": {"type": {"key": {"type": "uuid", + "refTable": "Load_Balancer", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "dns_records": {"type": {"key": {"type": "uuid", + "refTable": "DNS", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "other_config": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "Logical_Switch_Port": { + "columns": { + "name": {"type": "string"}, + "type": {"type": "string"}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "parent_name": {"type": {"key": "string", "min": 0, "max": 1}}, + "tag_request": { + "type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 4095}, + "min": 0, "max": 1}}, + "tag": { + "type": {"key": {"type": "integer", + "minInteger": 1, + "maxInteger": 4095}, + "min": 0, "max": 1}}, + "addresses": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "dynamic_addresses": {"type": {"key": "string", + "min": 0, + "max": 1}}, + "port_security": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "up": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "dhcpv4_options": {"type": {"key": {"type": "uuid", + "refTable": "DHCP_Options", + "refType": "weak"}, + "min": 0, + "max": 1}}, + "dhcpv6_options": {"type": {"key": {"type": "uuid", + "refTable": "DHCP_Options", + "refType": "weak"}, + "min": 0, + "max": 1}}, + "ha_chassis_group": { + "type": {"key": {"type": "uuid", + "refTable": "HA_Chassis_Group", + "refType": "strong"}, + "min": 0, + "max": 1}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": false}, + "Address_Set": { + "columns": { + "name": {"type": "string"}, + "addresses": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Port_Group": { + "columns": { + "name": {"type": "string"}, + "ports": {"type": {"key": {"type": "uuid", + "refTable": "Logical_Switch_Port", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "acls": {"type": {"key": {"type": "uuid", + "refTable": "ACL", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Load_Balancer": { + "columns": { + "name": {"type": "string"}, + "vips": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "protocol": { + "type": {"key": {"type": "string", + "enum": ["set", ["tcp", "udp"]]}, + "min": 0, "max": 1}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "ACL": { + "columns": { + "name": {"type": {"key": {"type": "string", + "maxLength": 63}, + "min": 0, "max": 1}}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "direction": {"type": {"key": {"type": "string", + "enum": ["set", ["from-lport", "to-lport"]]}}}, + "match": {"type": "string"}, + "action": {"type": {"key": {"type": "string", + "enum": ["set", ["allow", "allow-related", "drop", "reject"]]}}}, + "log": {"type": "boolean"}, + "severity": {"type": {"key": {"type": "string", + "enum": ["set", + ["alert", "warning", + "notice", "info", + "debug"]]}, + "min": 0, "max": 1}}, + "meter": {"type": {"key": "string", "min": 0, "max": 1}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "QoS": { + "columns": { + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "direction": {"type": {"key": {"type": "string", + "enum": ["set", ["from-lport", "to-lport"]]}}}, + "match": {"type": "string"}, + "action": {"type": {"key": {"type": "string", + "enum": ["set", ["dscp"]]}, + "value": {"type": "integer", + "minInteger": 0, + "maxInteger": 63}, + "min": 0, "max": "unlimited"}}, + "bandwidth": {"type": {"key": {"type": "string", + "enum": ["set", ["rate", + "burst"]]}, + "value": {"type": "integer", + "minInteger": 1, + "maxInteger": 4294967295}, + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "Meter": { + "columns": { + "name": {"type": "string"}, + "unit": {"type": {"key": {"type": "string", + "enum": ["set", ["kbps", "pktps"]]}}}, + "bands": {"type": {"key": {"type": "uuid", + "refTable": "Meter_Band", + "refType": "strong"}, + "min": 1, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Meter_Band": { + "columns": { + "action": {"type": {"key": {"type": "string", + "enum": ["set", ["drop"]]}}}, + "rate": {"type": {"key": {"type": "integer", + "minInteger": 1, + "maxInteger": 4294967295}}}, + "burst_size": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 4294967295}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "Logical_Router": { + "columns": { + "name": {"type": "string"}, + "ports": {"type": {"key": {"type": "uuid", + "refTable": "Logical_Router_Port", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "static_routes": {"type": {"key": {"type": "uuid", + "refTable": "Logical_Router_Static_Route", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "policies": { + "type": {"key": {"type": "uuid", + "refTable": "Logical_Router_Policy", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "nat": {"type": {"key": {"type": "uuid", + "refTable": "NAT", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "load_balancer": {"type": {"key": {"type": "uuid", + "refTable": "Load_Balancer", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "Logical_Router_Port": { + "columns": { + "name": {"type": "string"}, + "gateway_chassis": { + "type": {"key": {"type": "uuid", + "refTable": "Gateway_Chassis", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "ha_chassis_group": { + "type": {"key": {"type": "uuid", + "refTable": "HA_Chassis_Group", + "refType": "strong"}, + "min": 0, + "max": 1}}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "networks": {"type": {"key": "string", + "min": 1, + "max": "unlimited"}}, + "mac": {"type": "string"}, + "peer": {"type": {"key": "string", "min": 0, "max": 1}}, + "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "ipv6_ra_configs": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": false}, + "Logical_Router_Static_Route": { + "columns": { + "ip_prefix": {"type": "string"}, + "policy": {"type": {"key": {"type": "string", + "enum": ["set", ["src-ip", + "dst-ip"]]}, + "min": 0, "max": 1}}, + "nexthop": {"type": "string"}, + "output_port": {"type": {"key": "string", "min": 0, "max": 1}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "Logical_Router_Policy": { + "columns": { + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "match": {"type": "string"}, + "action": {"type": { + "key": {"type": "string", + "enum": ["set", ["allow", "drop", "reroute"]]}}}, + "nexthop": {"type": {"key": "string", "min": 0, "max": 1}}}, + "isRoot": false}, + "NAT": { + "columns": { + "external_ip": {"type": "string"}, + "external_mac": {"type": {"key": "string", + "min": 0, "max": 1}}, + "logical_ip": {"type": "string"}, + "logical_port": {"type": {"key": "string", + "min": 0, "max": 1}}, + "type": {"type": {"key": {"type": "string", + "enum": ["set", ["dnat", + "snat", + "dnat_and_snat" + ]]}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "DHCP_Options": { + "columns": { + "cidr": {"type": "string"}, + "options": {"type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "Connection": { + "columns": { + "target": {"type": "string"}, + "max_backoff": {"type": {"key": {"type": "integer", + "minInteger": 1000}, + "min": 0, + "max": 1}}, + "inactivity_probe": {"type": {"key": "integer", + "min": 0, + "max": 1}}, + "other_config": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "is_connected": {"type": "boolean", "ephemeral": true}, + "status": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}, + "ephemeral": true}}, + "indexes": [["target"]]}, + "DNS": { + "columns": { + "records": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}}, + "isRoot": true}, + "SSL": { + "columns": { + "private_key": {"type": "string"}, + "certificate": {"type": "string"}, + "ca_cert": {"type": "string"}, + "bootstrap_ca_cert": {"type": "boolean"}, + "ssl_protocols": {"type": "string"}, + "ssl_ciphers": {"type": "string"}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}}, + "maxRows": 1}, + "Gateway_Chassis": { + "columns": { + "name": {"type": "string"}, + "chassis_name": {"type": "string"}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": false}, + "HA_Chassis": { + "columns": { + "chassis_name": {"type": "string"}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "HA_Chassis_Group": { + "columns": { + "name": {"type": "string"}, + "ha_chassis": { + "type": {"key": {"type": "uuid", + "refTable": "HA_Chassis", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}} + } diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/schemas/ovn-sb.ovsschema b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/schemas/ovn-sb.ovsschema new file mode 100644 index 00000000000..2b7bc57a7f8 --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/schemas/ovn-sb.ovsschema @@ -0,0 +1,404 @@ +{ + "name": "OVN_Southbound", + "version": "2.4.0", + "cksum": "3059284885 20260", + "tables": { + "SB_Global": { + "columns": { + "nb_cfg": {"type": {"key": "integer"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "connections": { + "type": {"key": {"type": "uuid", + "refTable": "Connection"}, + "min": 0, + "max": "unlimited"}}, + "ssl": { + "type": {"key": {"type": "uuid", + "refTable": "SSL"}, + "min": 0, "max": 1}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "ipsec": {"type": "boolean"}}, + "maxRows": 1, + "isRoot": true}, + "Chassis": { + "columns": { + "name": {"type": "string"}, + "hostname": {"type": "string"}, + "encaps": {"type": {"key": {"type": "uuid", + "refTable": "Encap"}, + "min": 1, "max": "unlimited"}}, + "vtep_logical_switches" : {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "nb_cfg": {"type": {"key": "integer"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "transport_zones" : {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}}, + "isRoot": true, + "indexes": [["name"]]}, + "Encap": { + "columns": { + "type": {"type": {"key": { + "type": "string", + "enum": ["set", ["geneve", "stt", "vxlan"]]}}}, + "options": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "ip": {"type": "string"}, + "chassis_name": {"type": "string"}}, + "indexes": [["type", "ip"]]}, + "Address_Set": { + "columns": { + "name": {"type": "string"}, + "addresses": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Port_Group": { + "columns": { + "name": {"type": "string"}, + "ports": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Logical_Flow": { + "columns": { + "logical_datapath": {"type": {"key": {"type": "uuid", + "refTable": "Datapath_Binding"}}}, + "pipeline": {"type": {"key": {"type": "string", + "enum": ["set", ["ingress", + "egress"]]}}}, + "table_id": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 23}}}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 65535}}}, + "match": {"type": "string"}, + "actions": {"type": "string"}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "Multicast_Group": { + "columns": { + "datapath": {"type": {"key": {"type": "uuid", + "refTable": "Datapath_Binding"}}}, + "name": {"type": "string"}, + "tunnel_key": { + "type": {"key": {"type": "integer", + "minInteger": 32768, + "maxInteger": 65535}}}, + "ports": {"type": {"key": {"type": "uuid", + "refTable": "Port_Binding", + "refType": "weak"}, + "min": 1, "max": "unlimited"}}}, + "indexes": [["datapath", "tunnel_key"], + ["datapath", "name"]], + "isRoot": true}, + "Meter": { + "columns": { + "name": {"type": "string"}, + "unit": {"type": {"key": {"type": "string", + "enum": ["set", ["kbps", "pktps"]]}}}, + "bands": {"type": {"key": {"type": "uuid", + "refTable": "Meter_Band", + "refType": "strong"}, + "min": 1, + "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Meter_Band": { + "columns": { + "action": {"type": {"key": {"type": "string", + "enum": ["set", ["drop"]]}}}, + "rate": {"type": {"key": {"type": "integer", + "minInteger": 1, + "maxInteger": 4294967295}}}, + "burst_size": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 4294967295}}}}, + "isRoot": false}, + "Datapath_Binding": { + "columns": { + "tunnel_key": { + "type": {"key": {"type": "integer", + "minInteger": 1, + "maxInteger": 16777215}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["tunnel_key"]], + "isRoot": true}, + "Port_Binding": { + "columns": { + "logical_port": {"type": "string"}, + "type": {"type": "string"}, + "gateway_chassis": { + "type": {"key": {"type": "uuid", + "refTable": "Gateway_Chassis", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "ha_chassis_group": { + "type": {"key": {"type": "uuid", + "refTable": "HA_Chassis_Group", + "refType": "strong"}, + "min": 0, + "max": 1}}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "datapath": {"type": {"key": {"type": "uuid", + "refTable": "Datapath_Binding"}}}, + "tunnel_key": { + "type": {"key": {"type": "integer", + "minInteger": 1, + "maxInteger": 32767}}}, + "parent_port": {"type": {"key": "string", "min": 0, "max": 1}}, + "tag": { + "type": {"key": {"type": "integer", + "minInteger": 1, + "maxInteger": 4095}, + "min": 0, "max": 1}}, + "chassis": {"type": {"key": {"type": "uuid", + "refTable": "Chassis", + "refType": "weak"}, + "min": 0, "max": 1}}, + "encap": {"type": {"key": {"type": "uuid", + "refTable": "Encap", + "refType": "weak"}, + "min": 0, "max": 1}}, + "mac": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "nat_addresses": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}}, + "indexes": [["datapath", "tunnel_key"], ["logical_port"]], + "isRoot": true}, + "MAC_Binding": { + "columns": { + "logical_port": {"type": "string"}, + "ip": {"type": "string"}, + "mac": {"type": "string"}, + "datapath": {"type": {"key": {"type": "uuid", + "refTable": "Datapath_Binding"}}}}, + "indexes": [["logical_port", "ip"]], + "isRoot": true}, + "DHCP_Options": { + "columns": { + "name": {"type": "string"}, + "code": { + "type": {"key": {"type": "integer", + "minInteger": 0, "maxInteger": 254}}}, + "type": { + "type": {"key": { + "type": "string", + "enum": ["set", ["bool", "uint8", "uint16", "uint32", + "ipv4", "static_routes", "str"]]}}}}, + "isRoot": true}, + "DHCPv6_Options": { + "columns": { + "name": {"type": "string"}, + "code": { + "type": {"key": {"type": "integer", + "minInteger": 0, "maxInteger": 254}}}, + "type": { + "type": {"key": { + "type": "string", + "enum": ["set", ["ipv6", "str", "mac"]]}}}}, + "isRoot": true}, + "Connection": { + "columns": { + "target": {"type": "string"}, + "max_backoff": {"type": {"key": {"type": "integer", + "minInteger": 1000}, + "min": 0, + "max": 1}}, + "inactivity_probe": {"type": {"key": "integer", + "min": 0, + "max": 1}}, + "read_only": {"type": "boolean"}, + "role": {"type": "string"}, + "other_config": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "is_connected": {"type": "boolean", "ephemeral": true}, + "status": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}, + "ephemeral": true}}, + "indexes": [["target"]]}, + "SSL": { + "columns": { + "private_key": {"type": "string"}, + "certificate": {"type": "string"}, + "ca_cert": {"type": "string"}, + "bootstrap_ca_cert": {"type": "boolean"}, + "ssl_protocols": {"type": "string"}, + "ssl_ciphers": {"type": "string"}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}}, + "maxRows": 1}, + "DNS": { + "columns": { + "records": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "datapaths": {"type": {"key": {"type": "uuid", + "refTable": "Datapath_Binding"}, + "min": 1, + "max": "unlimited"}}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}}, + "isRoot": true}, + "RBAC_Role": { + "columns": { + "name": {"type": "string"}, + "permissions": { + "type": {"key": {"type": "string"}, + "value": {"type": "uuid", + "refTable": "RBAC_Permission", + "refType": "weak"}, + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "RBAC_Permission": { + "columns": { + "table": {"type": "string"}, + "authorization": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "insert_delete": {"type": "boolean"}, + "update" : {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}}, + "isRoot": true}, + "Gateway_Chassis": { + "columns": { + "name": {"type": "string"}, + "chassis": {"type": {"key": {"type": "uuid", + "refTable": "Chassis", + "refType": "weak"}, + "min": 0, "max": 1}}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": false}, + "HA_Chassis": { + "columns": { + "chassis": {"type": {"key": {"type": "uuid", + "refTable": "Chassis", + "refType": "weak"}, + "min": 0, "max": 1}}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "HA_Chassis_Group": { + "columns": { + "name": {"type": "string"}, + "ha_chassis": { + "type": {"key": {"type": "uuid", + "refTable": "HA_Chassis", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "ref_chassis": {"type": {"key": {"type": "uuid", + "refTable": "Chassis", + "refType": "weak"}, + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Controller_Event": { + "columns": { + "event_type": {"type": {"key": {"type": "string", + "enum": ["set", ["empty_lb_backends"]]}}}, + "event_info": {"type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "chassis": {"type": {"key": {"type": "uuid", + "refTable": "Chassis", + "refType": "weak"}, + "min": 0, "max": 1}}, + "seq_num": {"type": {"key": "integer"}} + }, + "isRoot": true}, + "IP_Multicast": { + "columns": { + "datapath": {"type": {"key": {"type": "uuid", + "refTable": "Datapath_Binding", + "refType": "weak"}}}, + "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "querier": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "eth_src": {"type": "string"}, + "ip4_src": {"type": "string"}, + "table_size": {"type": {"key": "integer", + "min": 0, "max": 1}}, + "idle_timeout": {"type": {"key": "integer", + "min": 0, "max": 1}}, + "query_interval": {"type": {"key": "integer", + "min": 0, "max": 1}}, + "query_max_resp": {"type": {"key": "integer", + "min": 0, "max": 1}}, + "seq_no": {"type": "integer"}}, + "indexes": [["datapath"]], + "isRoot": true}, + "IGMP_Group": { + "columns": { + "address": {"type": "string"}, + "datapath": {"type": {"key": {"type": "uuid", + "refTable": "Datapath_Binding", + "refType": "weak"}, + "min": 0, + "max": 1}}, + "chassis": {"type": {"key": {"type": "uuid", + "refTable": "Chassis", + "refType": "weak"}, + "min": 0, + "max": 1}}, + "ports": {"type": {"key": {"type": "uuid", + "refTable": "Port_Binding", + "refType": "weak"}, + "min": 0, "max": "unlimited"}}}, + "indexes": [["address", "datapath", "chassis"]], + "isRoot": true}}} diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py new file mode 100644 index 00000000000..ed57baf9933 --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py @@ -0,0 +1,773 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import collections +import copy +import uuid + +import mock + +from neutron.common.ovn import constants as ovn_const +from neutron.common.ovn import utils +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import impl_idl_ovn +from neutron.tests import base +from neutron.tests.unit import fake_resources as fakes + + +class TestDBImplIdlOvn(base.BaseTestCase): + + def _load_ovsdb_fake_rows(self, table, fake_attrs): + for fake_attr in fake_attrs: + fake_row = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs=fake_attr) + # Pre-populate ovs idl "._data" + fake_data = copy.deepcopy(fake_attr) + try: + del fake_data["unit_test_id"] + except KeyError: + pass + setattr(fake_row, "_data", fake_data) + table.rows[fake_row.uuid] = fake_row + + def _find_ovsdb_fake_row(self, table, key, value): + for fake_row in table.rows.values(): + if getattr(fake_row, key) == value: + return fake_row + return None + + def _construct_ovsdb_references(self, fake_associations, + parent_table, child_table, + parent_key, child_key, + reference_column_name): + for p_name, c_names in fake_associations.items(): + p_row = self._find_ovsdb_fake_row(parent_table, parent_key, p_name) + c_uuids = [] + for c_name in c_names: + c_row = self._find_ovsdb_fake_row(child_table, child_key, + c_name) + if not c_row: + continue + # Fake IDL processing (uuid -> row) + c_uuids.append(c_row) + setattr(p_row, reference_column_name, c_uuids) + + +class TestNBImplIdlOvn(TestDBImplIdlOvn): + + fake_set = { + 'lswitches': [ + {'name': utils.ovn_name('ls-id-1'), + 'external_ids': {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: + 'ls-name-1'}}, + {'name': utils.ovn_name('ls-id-2'), + 'external_ids': {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: + 'ls-name-2'}}, + {'name': utils.ovn_name('ls-id-3'), + 'external_ids': {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: + 'ls-name-3'}}, + {'name': 'ls-id-4', + 'external_ids': {'not-neutron:network_name': 'ls-name-4'}}, + {'name': utils.ovn_name('ls-id-5'), + 'external_ids': {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: + 'ls-name-5'}}], + 'lswitch_ports': [ + {'name': 'lsp-id-11', 'addresses': ['10.0.1.1'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-name-11'}}, + {'name': 'lsp-id-12', 'addresses': ['10.0.1.2'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-name-12'}}, + {'name': 'lsp-rp-id-1', 'addresses': ['10.0.1.254'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-rp-name-1'}, + 'options': {'router-port': + utils.ovn_lrouter_port_name('orp-id-a1')}}, + {'name': 'provnet-ls-id-1', 'addresses': ['unknown'], + 'external_ids': {}, + 'options': {'network_name': 'physnet1'}}, + {'name': 'lsp-id-21', 'addresses': ['10.0.2.1'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-name-21'}}, + {'name': 'lsp-id-22', 'addresses': ['10.0.2.2'], + 'external_ids': {}}, + {'name': 'lsp-id-23', 'addresses': ['10.0.2.3'], + 'external_ids': {'not-neutron:port_name': 'lsp-name-23'}}, + {'name': 'lsp-rp-id-2', 'addresses': ['10.0.2.254'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-rp-name-2'}, + 'options': {'router-port': + utils.ovn_lrouter_port_name('orp-id-a2')}}, + {'name': 'provnet-ls-id-2', 'addresses': ['unknown'], + 'external_ids': {}, + 'options': {'network_name': 'physnet2'}}, + {'name': 'lsp-id-31', 'addresses': ['10.0.3.1'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-name-31'}}, + {'name': 'lsp-id-32', 'addresses': ['10.0.3.2'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-name-32'}}, + {'name': 'lsp-rp-id-3', 'addresses': ['10.0.3.254'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-rp-name-3'}, + 'options': {'router-port': + utils.ovn_lrouter_port_name('orp-id-a3')}}, + {'name': 'lsp-vpn-id-3', 'addresses': ['10.0.3.253'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-vpn-name-3'}}, + {'name': 'lsp-id-41', 'addresses': ['20.0.1.1'], + 'external_ids': {'not-neutron:port_name': 'lsp-name-41'}}, + {'name': 'lsp-rp-id-4', 'addresses': ['20.0.1.254'], + 'external_ids': {}, + 'options': {'router-port': 'xrp-id-b1'}}, + {'name': 'lsp-id-51', 'addresses': ['20.0.2.1'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-name-51'}}, + {'name': 'lsp-id-52', 'addresses': ['20.0.2.2'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-name-52'}}, + {'name': 'lsp-rp-id-5', 'addresses': ['20.0.2.254'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-rp-name-5'}, + 'options': {'router-port': + utils.ovn_lrouter_port_name('orp-id-b2')}}, + {'name': 'lsp-vpn-id-5', 'addresses': ['20.0.2.253'], + 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: + 'lsp-vpn-name-5'}}], + 'lrouters': [ + {'name': utils.ovn_name('lr-id-a'), + 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: + 'lr-name-a'}}, + {'name': utils.ovn_name('lr-id-b'), + 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: + 'lr-name-b'}}, + {'name': utils.ovn_name('lr-id-c'), + 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: + 'lr-name-c'}}, + {'name': utils.ovn_name('lr-id-d'), + 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: + 'lr-name-d'}}, + {'name': utils.ovn_name('lr-id-e'), + 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: + 'lr-name-e'}}], + 'lrouter_ports': [ + {'name': utils.ovn_lrouter_port_name('orp-id-a1'), + 'external_ids': {}, 'networks': ['10.0.1.0/24'], + 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-1'}}, + {'name': utils.ovn_lrouter_port_name('orp-id-a2'), + 'external_ids': {}, 'networks': ['10.0.2.0/24'], + 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-1'}}, + {'name': utils.ovn_lrouter_port_name('orp-id-a3'), + 'external_ids': {}, 'networks': ['10.0.3.0/24'], + 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: + ovn_const.OVN_GATEWAY_INVALID_CHASSIS}}, + {'name': 'xrp-id-b1', + 'external_ids': {}, 'networks': ['20.0.1.0/24']}, + {'name': utils.ovn_lrouter_port_name('orp-id-b2'), + 'external_ids': {}, 'networks': ['20.0.2.0/24'], + 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'}}, + {'name': utils.ovn_lrouter_port_name('orp-id-b3'), + 'external_ids': {}, 'networks': ['20.0.3.0/24'], + 'options': {}}], + 'static_routes': [{'ip_prefix': '20.0.0.0/16', + 'nexthop': '10.0.3.253'}, + {'ip_prefix': '10.0.0.0/16', + 'nexthop': '20.0.2.253'}], + 'nats': [{'external_ip': '10.0.3.1', 'logical_ip': '20.0.0.0/16', + 'type': 'snat'}, + {'external_ip': '20.0.2.1', 'logical_ip': '10.0.0.0/24', + 'type': 'snat'}, + {'external_ip': '20.0.2.4', 'logical_ip': '10.0.0.4', + 'type': 'dnat_and_snat', 'external_mac': [], + 'logical_port': []}, + {'external_ip': '20.0.2.5', 'logical_ip': '10.0.0.5', + 'type': 'dnat_and_snat', + 'external_mac': ['00:01:02:03:04:05'], + 'logical_port': ['lsp-id-001']}], + 'acls': [ + {'unit_test_id': 1, + 'action': 'allow-related', 'direction': 'from-lport', + 'external_ids': {'neutron:lport': 'lsp-id-11'}, + 'match': 'inport == "lsp-id-11" && ip4'}, + {'unit_test_id': 2, + 'action': 'allow-related', 'direction': 'to-lport', + 'external_ids': {'neutron:lport': 'lsp-id-11'}, + 'match': 'outport == "lsp-id-11" && ip4.src == $as_ip4_id_1'}, + {'unit_test_id': 3, + 'action': 'allow-related', 'direction': 'from-lport', + 'external_ids': {'neutron:lport': 'lsp-id-12'}, + 'match': 'inport == "lsp-id-12" && ip4'}, + {'unit_test_id': 4, + 'action': 'allow-related', 'direction': 'to-lport', + 'external_ids': {'neutron:lport': 'lsp-id-12'}, + 'match': 'outport == "lsp-id-12" && ip4.src == $as_ip4_id_1'}, + {'unit_test_id': 5, + 'action': 'allow-related', 'direction': 'from-lport', + 'external_ids': {'neutron:lport': 'lsp-id-21'}, + 'match': 'inport == "lsp-id-21" && ip4'}, + {'unit_test_id': 6, + 'action': 'allow-related', 'direction': 'to-lport', + 'external_ids': {'neutron:lport': 'lsp-id-21'}, + 'match': 'outport == "lsp-id-21" && ip4.src == $as_ip4_id_2'}, + {'unit_test_id': 7, + 'action': 'allow-related', 'direction': 'from-lport', + 'external_ids': {'neutron:lport': 'lsp-id-41'}, + 'match': 'inport == "lsp-id-41" && ip4'}, + {'unit_test_id': 8, + 'action': 'allow-related', 'direction': 'to-lport', + 'external_ids': {'neutron:lport': 'lsp-id-41'}, + 'match': 'outport == "lsp-id-41" && ip4.src == $as_ip4_id_4'}, + {'unit_test_id': 9, + 'action': 'allow-related', 'direction': 'from-lport', + 'external_ids': {'neutron:lport': 'lsp-id-52'}, + 'match': 'inport == "lsp-id-52" && ip4'}, + {'unit_test_id': 10, + 'action': 'allow-related', 'direction': 'to-lport', + 'external_ids': {'neutron:lport': 'lsp-id-52'}, + 'match': 'outport == "lsp-id-52" && ip4.src == $as_ip4_id_5'}], + 'dhcp_options': [ + {'cidr': '10.0.1.0/24', + 'external_ids': {'subnet_id': 'subnet-id-10-0-1-0'}, + 'options': {'mtu': '1442', 'router': '10.0.1.254'}}, + {'cidr': '10.0.2.0/24', + 'external_ids': {'subnet_id': 'subnet-id-10-0-2-0'}, + 'options': {'mtu': '1442', 'router': '10.0.2.254'}}, + {'cidr': '10.0.1.0/26', + 'external_ids': {'subnet_id': 'subnet-id-10-0-1-0', + 'port_id': 'lsp-vpn-id-3'}, + 'options': {'mtu': '1442', 'router': '10.0.1.1'}}, + {'cidr': '20.0.1.0/24', + 'external_ids': {'subnet_id': 'subnet-id-20-0-1-0'}, + 'options': {'mtu': '1442', 'router': '20.0.1.254'}}, + {'cidr': '20.0.2.0/24', + 'external_ids': {'subnet_id': 'subnet-id-20-0-2-0', + 'port_id': 'lsp-vpn-id-5'}, + 'options': {'mtu': '1442', 'router': '20.0.2.254'}}, + {'cidr': '2001:dba::/64', + 'external_ids': {'subnet_id': 'subnet-id-2001-dba', + 'port_id': 'lsp-vpn-id-5'}, + 'options': {'server_id': '12:34:56:78:9a:bc'}}, + {'cidr': '30.0.1.0/24', + 'external_ids': {'port_id': 'port-id-30-0-1-0'}, + 'options': {'mtu': '1442', 'router': '30.0.2.254'}}, + {'cidr': '30.0.2.0/24', 'external_ids': {}, 'options': {}}], + 'address_sets': [ + {'name': '$as_ip4_id_1', + 'addresses': ['10.0.1.1', '10.0.1.2'], + 'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'id_1'}}, + {'name': '$as_ip4_id_2', + 'addresses': ['10.0.2.1'], + 'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'id_2'}}, + {'name': '$as_ip4_id_3', + 'addresses': ['10.0.3.1', '10.0.3.2'], + 'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'id_3'}}, + {'name': '$as_ip4_id_4', + 'addresses': ['20.0.1.1', '20.0.1.2'], + 'external_ids': {}}, + {'name': '$as_ip4_id_5', + 'addresses': ['20.0.2.1', '20.0.2.2'], + 'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'id_5'}}], + } + + fake_associations = { + 'lstolsp': { + utils.ovn_name('ls-id-1'): [ + 'lsp-id-11', 'lsp-id-12', 'lsp-rp-id-1', 'provnet-ls-id-1'], + utils.ovn_name('ls-id-2'): [ + 'lsp-id-21', 'lsp-id-22', 'lsp-id-23', 'lsp-rp-id-2', + 'provnet-ls-id-2'], + utils.ovn_name('ls-id-3'): [ + 'lsp-id-31', 'lsp-id-32', 'lsp-rp-id-3', 'lsp-vpn-id-3'], + 'ls-id-4': [ + 'lsp-id-41', 'lsp-rp-id-4'], + utils.ovn_name('ls-id-5'): [ + 'lsp-id-51', 'lsp-id-52', 'lsp-rp-id-5', 'lsp-vpn-id-5']}, + 'lrtolrp': { + utils.ovn_name('lr-id-a'): [ + utils.ovn_lrouter_port_name('orp-id-a1'), + utils.ovn_lrouter_port_name('orp-id-a2'), + utils.ovn_lrouter_port_name('orp-id-a3')], + utils.ovn_name('lr-id-b'): [ + 'xrp-id-b1', + utils.ovn_lrouter_port_name('orp-id-b2')]}, + 'lrtosroute': { + utils.ovn_name('lr-id-a'): ['20.0.0.0/16'], + utils.ovn_name('lr-id-b'): ['10.0.0.0/16'] + }, + 'lrtonat': { + utils.ovn_name('lr-id-a'): ['10.0.3.1'], + utils.ovn_name('lr-id-b'): ['20.0.2.1', '20.0.2.4', '20.0.2.5'], + }, + 'lstoacl': { + utils.ovn_name('ls-id-1'): [1, 2, 3, 4], + utils.ovn_name('ls-id-2'): [5, 6], + 'ls-id-4': [7, 8], + utils.ovn_name('ls-id-5'): [9, 10]} + } + + def setUp(self): + super(TestNBImplIdlOvn, self).setUp() + + self.lswitch_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self.lsp_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self.lrouter_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self.lrp_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self.sroute_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self.nat_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self.acl_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self.dhcp_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self.address_set_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + + self._tables = {} + self._tables['Logical_Switch'] = self.lswitch_table + self._tables['Logical_Switch_Port'] = self.lsp_table + self._tables['Logical_Router'] = self.lrouter_table + self._tables['Logical_Router_Port'] = self.lrp_table + self._tables['Logical_Router_Static_Route'] = self.sroute_table + self._tables['ACL'] = self.acl_table + self._tables['DHCP_Options'] = self.dhcp_table + self._tables['Address_Set'] = self.address_set_table + + with mock.patch.object(impl_idl_ovn, 'get_connection', + return_value=mock.Mock()): + impl_idl_ovn.OvsdbNbOvnIdl.ovsdb_connection = None + self.nb_ovn_idl = impl_idl_ovn.OvsdbNbOvnIdl(mock.Mock()) + + self.nb_ovn_idl.idl.tables = self._tables + + def _load_nb_db(self): + # Load Switches and Switch Ports + fake_lswitches = TestNBImplIdlOvn.fake_set['lswitches'] + self._load_ovsdb_fake_rows(self.lswitch_table, fake_lswitches) + fake_lsps = TestNBImplIdlOvn.fake_set['lswitch_ports'] + self._load_ovsdb_fake_rows(self.lsp_table, fake_lsps) + # Associate switches and ports + self._construct_ovsdb_references( + TestNBImplIdlOvn.fake_associations['lstolsp'], + self.lswitch_table, self.lsp_table, + 'name', 'name', 'ports') + # Load Routers and Router Ports + fake_lrouters = TestNBImplIdlOvn.fake_set['lrouters'] + self._load_ovsdb_fake_rows(self.lrouter_table, fake_lrouters) + fake_lrps = TestNBImplIdlOvn.fake_set['lrouter_ports'] + self._load_ovsdb_fake_rows(self.lrp_table, fake_lrps) + # Associate routers and router ports + self._construct_ovsdb_references( + TestNBImplIdlOvn.fake_associations['lrtolrp'], + self.lrouter_table, self.lrp_table, + 'name', 'name', 'ports') + # Load static routes + fake_sroutes = TestNBImplIdlOvn.fake_set['static_routes'] + self._load_ovsdb_fake_rows(self.sroute_table, fake_sroutes) + # Associate routers and static routes + self._construct_ovsdb_references( + TestNBImplIdlOvn.fake_associations['lrtosroute'], + self.lrouter_table, self.sroute_table, + 'name', 'ip_prefix', 'static_routes') + # Load nats + fake_nats = TestNBImplIdlOvn.fake_set['nats'] + self._load_ovsdb_fake_rows(self.nat_table, fake_nats) + # Associate routers and nats + self._construct_ovsdb_references( + TestNBImplIdlOvn.fake_associations['lrtonat'], + self.lrouter_table, self.nat_table, + 'name', 'external_ip', 'nat') + # Load acls + fake_acls = TestNBImplIdlOvn.fake_set['acls'] + self._load_ovsdb_fake_rows(self.acl_table, fake_acls) + # Associate switches and acls + self._construct_ovsdb_references( + TestNBImplIdlOvn.fake_associations['lstoacl'], + self.lswitch_table, self.acl_table, + 'name', 'unit_test_id', 'acls') + # Load dhcp options + fake_dhcp_options = TestNBImplIdlOvn.fake_set['dhcp_options'] + self._load_ovsdb_fake_rows(self.dhcp_table, fake_dhcp_options) + # Load address sets + fake_address_sets = TestNBImplIdlOvn.fake_set['address_sets'] + self._load_ovsdb_fake_rows(self.address_set_table, fake_address_sets) + + @mock.patch.object(impl_idl_ovn.OvsdbNbOvnIdl, 'ovsdb_connection', None) + @mock.patch.object(impl_idl_ovn, 'get_connection', mock.Mock()) + def test_setting_ovsdb_probe_timeout_default_value(self): + inst = impl_idl_ovn.OvsdbNbOvnIdl(mock.Mock()) + inst.idl._session.reconnect.set_probe_interval.assert_called_with( + 60000) + + @mock.patch.object(impl_idl_ovn.OvsdbNbOvnIdl, 'ovsdb_connection', None) + @mock.patch.object(impl_idl_ovn, 'get_connection', mock.Mock()) + @mock.patch.object(ovn_conf, 'get_ovn_ovsdb_probe_interval') + def test_setting_ovsdb_probe_timeout(self, mock_get_probe_interval): + mock_get_probe_interval.return_value = 5000 + inst = impl_idl_ovn.OvsdbNbOvnIdl(mock.Mock()) + inst.idl._session.reconnect.set_probe_interval.assert_called_with(5000) + + def test_get_all_logical_switches_with_ports(self): + # Test empty + mapping = self.nb_ovn_idl.get_all_logical_switches_with_ports() + self.assertItemsEqual(mapping, {}) + # Test loaded values + self._load_nb_db() + mapping = self.nb_ovn_idl.get_all_logical_switches_with_ports() + expected = [{'name': utils.ovn_name('ls-id-1'), + 'ports': ['lsp-id-11', 'lsp-id-12', 'lsp-rp-id-1'], + 'provnet_port': 'provnet-ls-id-1'}, + {'name': utils.ovn_name('ls-id-2'), + 'ports': ['lsp-id-21', 'lsp-rp-id-2'], + 'provnet_port': 'provnet-ls-id-2'}, + {'name': utils.ovn_name('ls-id-3'), + 'ports': ['lsp-id-31', 'lsp-id-32', 'lsp-rp-id-3', + 'lsp-vpn-id-3'], + 'provnet_port': None}, + {'name': utils.ovn_name('ls-id-5'), + 'ports': ['lsp-id-51', 'lsp-id-52', 'lsp-rp-id-5', + 'lsp-vpn-id-5'], + 'provnet_port': None}] + self.assertItemsEqual(mapping, expected) + + def test_get_all_logical_routers_with_rports(self): + # Test empty + mapping = self.nb_ovn_idl.get_all_logical_switches_with_ports() + self.assertItemsEqual(mapping, {}) + # Test loaded values + self._load_nb_db() + mapping = self.nb_ovn_idl.get_all_logical_routers_with_rports() + expected = [{'name': 'lr-id-a', + 'ports': {'orp-id-a1': ['10.0.1.0/24'], + 'orp-id-a2': ['10.0.2.0/24'], + 'orp-id-a3': ['10.0.3.0/24']}, + 'static_routes': [{'destination': '20.0.0.0/16', + 'nexthop': '10.0.3.253'}], + 'snats': [{'external_ip': '10.0.3.1', + 'logical_ip': '20.0.0.0/16', + 'type': 'snat'}], + 'dnat_and_snats': []}, + {'name': 'lr-id-b', + 'ports': {'xrp-id-b1': ['20.0.1.0/24'], + 'orp-id-b2': ['20.0.2.0/24']}, + 'static_routes': [{'destination': '10.0.0.0/16', + 'nexthop': '20.0.2.253'}], + 'snats': [{'external_ip': '20.0.2.1', + 'logical_ip': '10.0.0.0/24', + 'type': 'snat'}], + 'dnat_and_snats': [{'external_ip': '20.0.2.4', + 'logical_ip': '10.0.0.4', + 'type': 'dnat_and_snat'}, + {'external_ip': '20.0.2.5', + 'logical_ip': '10.0.0.5', + 'type': 'dnat_and_snat', + 'external_mac': '00:01:02:03:04:05', + 'logical_port': 'lsp-id-001'}]}, + {'name': 'lr-id-c', 'ports': {}, 'static_routes': [], + 'snats': [], 'dnat_and_snats': []}, + {'name': 'lr-id-d', 'ports': {}, 'static_routes': [], + 'snats': [], 'dnat_and_snats': []}, + {'name': 'lr-id-e', 'ports': {}, 'static_routes': [], + 'snats': [], 'dnat_and_snats': []}] + self.assertItemsEqual(mapping, expected) + + def test_get_acls_for_lswitches(self): + self._load_nb_db() + # Test neutron switches + lswitches = ['ls-id-1', 'ls-id-2', 'ls-id-3', 'ls-id-5'] + acl_values, acl_objs, lswitch_ovsdb_dict = \ + self.nb_ovn_idl.get_acls_for_lswitches(lswitches) + excepted_acl_values = { + 'lsp-id-11': [ + {'action': 'allow-related', 'lport': 'lsp-id-11', + 'lswitch': 'neutron-ls-id-1', + 'external_ids': {'neutron:lport': 'lsp-id-11'}, + 'direction': 'from-lport', + 'match': 'inport == "lsp-id-11" && ip4'}, + {'action': 'allow-related', 'lport': 'lsp-id-11', + 'lswitch': 'neutron-ls-id-1', + 'external_ids': {'neutron:lport': 'lsp-id-11'}, + 'direction': 'to-lport', + 'match': 'outport == "lsp-id-11" && ip4.src == $as_ip4_id_1'} + ], + 'lsp-id-12': [ + {'action': 'allow-related', 'lport': 'lsp-id-12', + 'lswitch': 'neutron-ls-id-1', + 'external_ids': {'neutron:lport': 'lsp-id-12'}, + 'direction': 'from-lport', + 'match': 'inport == "lsp-id-12" && ip4'}, + {'action': 'allow-related', 'lport': 'lsp-id-12', + 'lswitch': 'neutron-ls-id-1', + 'external_ids': {'neutron:lport': 'lsp-id-12'}, + 'direction': 'to-lport', + 'match': 'outport == "lsp-id-12" && ip4.src == $as_ip4_id_1'} + ], + 'lsp-id-21': [ + {'action': 'allow-related', 'lport': 'lsp-id-21', + 'lswitch': 'neutron-ls-id-2', + 'external_ids': {'neutron:lport': 'lsp-id-21'}, + 'direction': 'from-lport', + 'match': 'inport == "lsp-id-21" && ip4'}, + {'action': 'allow-related', 'lport': 'lsp-id-21', + 'lswitch': 'neutron-ls-id-2', + 'external_ids': {'neutron:lport': 'lsp-id-21'}, + 'direction': 'to-lport', + 'match': 'outport == "lsp-id-21" && ip4.src == $as_ip4_id_2'} + ], + 'lsp-id-52': [ + {'action': 'allow-related', 'lport': 'lsp-id-52', + 'lswitch': 'neutron-ls-id-5', + 'external_ids': {'neutron:lport': 'lsp-id-52'}, + 'direction': 'from-lport', + 'match': 'inport == "lsp-id-52" && ip4'}, + {'action': 'allow-related', 'lport': 'lsp-id-52', + 'lswitch': 'neutron-ls-id-5', + 'external_ids': {'neutron:lport': 'lsp-id-52'}, + 'direction': 'to-lport', + 'match': 'outport == "lsp-id-52" && ip4.src == $as_ip4_id_5'} + ]} + self.assertItemsEqual(acl_values, excepted_acl_values) + self.assertEqual(len(acl_objs), 8) + self.assertEqual(len(lswitch_ovsdb_dict), len(lswitches)) + + # Test non-neutron switches + lswitches = ['ls-id-4'] + acl_values, acl_objs, lswitch_ovsdb_dict = \ + self.nb_ovn_idl.get_acls_for_lswitches(lswitches) + self.assertItemsEqual(acl_values, {}) + self.assertEqual(len(acl_objs), 0) + self.assertEqual(len(lswitch_ovsdb_dict), 0) + + def test_get_all_chassis_gateway_bindings(self): + self._load_nb_db() + bindings = self.nb_ovn_idl.get_all_chassis_gateway_bindings() + expected = {'host-1': [utils.ovn_lrouter_port_name('orp-id-a1'), + utils.ovn_lrouter_port_name('orp-id-a2')], + 'host-2': [utils.ovn_lrouter_port_name('orp-id-b2')], + ovn_const.OVN_GATEWAY_INVALID_CHASSIS: [ + utils.ovn_name('orp-id-a3')]} + self.assertItemsEqual(bindings, expected) + + bindings = self.nb_ovn_idl.get_all_chassis_gateway_bindings([]) + self.assertItemsEqual(bindings, expected) + + bindings = self.nb_ovn_idl.get_all_chassis_gateway_bindings(['host-1']) + expected = {'host-1': [utils.ovn_lrouter_port_name('orp-id-a1'), + utils.ovn_lrouter_port_name('orp-id-a2')]} + self.assertItemsEqual(bindings, expected) + + def test_get_gateway_chassis_binding(self): + self._load_nb_db() + chassis = self.nb_ovn_idl.get_gateway_chassis_binding( + utils.ovn_lrouter_port_name('orp-id-a1')) + self.assertEqual(chassis, ['host-1']) + chassis = self.nb_ovn_idl.get_gateway_chassis_binding( + utils.ovn_lrouter_port_name('orp-id-b2')) + self.assertEqual(chassis, ['host-2']) + chassis = self.nb_ovn_idl.get_gateway_chassis_binding( + utils.ovn_lrouter_port_name('orp-id-a3')) + self.assertEqual(chassis, ['neutron-ovn-invalid-chassis']) + chassis = self.nb_ovn_idl.get_gateway_chassis_binding( + utils.ovn_lrouter_port_name('orp-id-b3')) + self.assertEqual([], chassis) + chassis = self.nb_ovn_idl.get_gateway_chassis_binding('bad') + self.assertEqual([], chassis) + + def test_get_unhosted_gateways(self): + self._load_nb_db() + # Test only host-1 in the valid list + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + {}, {'host-1': 'physnet1'}, []) + expected = ['lrp-orp-id-a1', 'lrp-orp-id-a2', + 'lrp-orp-id-a3', 'lrp-orp-id-b2'] + self.assertItemsEqual(unhosted_gateways, expected) + # Test both host-1, host-2 in valid list + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + {}, {'host-1': 'physnet1', 'host-2': 'physnet2'}, []) + self.assertItemsEqual(unhosted_gateways, expected) + # Schedule unhosted_gateways on host-2 + for unhosted_gateway in unhosted_gateways: + router_row = self._find_ovsdb_fake_row(self.lrp_table, + 'name', unhosted_gateway) + setattr(router_row, 'options', { + ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'}) + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + {}, {'host-1': 'physnet1', 'host-2': 'physnet2'}, []) + self.assertItemsEqual(unhosted_gateways, expected) + + def test_unhosted_gateway_max_chassis(self): + gw_chassis_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self._tables['Gateway_Chassis'] = gw_chassis_table + gw_chassis = collections.namedtuple('gw_chassis', + 'chassis_name priority') + TestNBImplIdlOvn.fake_set['lrouter_ports'][0]['gateway_chassis'] = [ + gw_chassis(chassis_name='host-%s' % x, + priority=x) for x in range(1, 6)] + for port in TestNBImplIdlOvn.fake_set['lrouter_ports'][1:]: + port['gateway_chassis'] = [] + self._load_nb_db() + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + {}, {'host-1': 'physnet1', 'host-2': 'physnet2', + 'host-3': 'physnet1', 'host-4': 'physnet2', + 'host-5': 'physnet1', 'host-6': 'physnet2'}, []) + expected = [] + self.assertItemsEqual(unhosted_gateways, expected) + + def test_get_subnet_dhcp_options(self): + self._load_nb_db() + subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( + 'subnet-id-10-0-2-0') + expected_row = self._find_ovsdb_fake_row(self.dhcp_table, + 'cidr', '10.0.2.0/24') + self.assertEqual({ + 'subnet': {'cidr': expected_row.cidr, + 'external_ids': expected_row.external_ids, + 'options': expected_row.options, + 'uuid': expected_row.uuid}, + 'ports': []}, subnet_options) + subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( + 'subnet-id-11-0-2-0')['subnet'] + self.assertIsNone(subnet_options) + subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( + 'port-id-30-0-1-0')['subnet'] + self.assertIsNone(subnet_options) + + def test_get_subnet_dhcp_options_with_ports(self): + # Test empty + subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( + 'subnet-id-10-0-1-0', with_ports=True) + self.assertItemsEqual({'subnet': None, 'ports': []}, subnet_options) + # Test loaded values + self._load_nb_db() + # Test getting both subnet and port dhcp options + subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( + 'subnet-id-10-0-1-0', with_ports=True) + dhcp_rows = [ + self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '10.0.1.0/24'), + self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '10.0.1.0/26')] + expected_rows = [{'cidr': dhcp_row.cidr, + 'external_ids': dhcp_row.external_ids, + 'options': dhcp_row.options, + 'uuid': dhcp_row.uuid} for dhcp_row in dhcp_rows] + self.assertItemsEqual(expected_rows, [ + subnet_options['subnet']] + subnet_options['ports']) + # Test getting only subnet dhcp options + subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( + 'subnet-id-10-0-2-0', with_ports=True) + dhcp_rows = [ + self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '10.0.2.0/24')] + expected_rows = [{'cidr': dhcp_row.cidr, + 'external_ids': dhcp_row.external_ids, + 'options': dhcp_row.options, + 'uuid': dhcp_row.uuid} for dhcp_row in dhcp_rows] + self.assertItemsEqual(expected_rows, [ + subnet_options['subnet']] + subnet_options['ports']) + # Test getting no dhcp options + subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( + 'subnet-id-11-0-2-0', with_ports=True) + self.assertItemsEqual({'subnet': None, 'ports': []}, subnet_options) + + def test_get_subnets_dhcp_options(self): + self._load_nb_db() + + def get_row_dict(row): + return {'cidr': row.cidr, 'external_ids': row.external_ids, + 'options': row.options, 'uuid': row.uuid} + + subnets_options = self.nb_ovn_idl.get_subnets_dhcp_options( + ['subnet-id-10-0-1-0', 'subnet-id-10-0-2-0']) + expected_rows = [ + get_row_dict( + self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', cidr)) + for cidr in ('10.0.1.0/24', '10.0.2.0/24')] + self.assertItemsEqual(expected_rows, subnets_options) + + subnets_options = self.nb_ovn_idl.get_subnets_dhcp_options( + ['subnet-id-11-0-2-0', 'subnet-id-20-0-1-0']) + expected_row = get_row_dict( + self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '20.0.1.0/24')) + self.assertItemsEqual([expected_row], subnets_options) + + subnets_options = self.nb_ovn_idl.get_subnets_dhcp_options( + ['port-id-30-0-1-0', 'fake-not-exist']) + self.assertEqual([], subnets_options) + + def test_get_all_dhcp_options(self): + self._load_nb_db() + dhcp_options = self.nb_ovn_idl.get_all_dhcp_options() + self.assertEqual(len(dhcp_options['subnets']), 3) + self.assertEqual(len(dhcp_options['ports_v4']), 2) + + def test_get_address_sets(self): + self._load_nb_db() + address_sets = self.nb_ovn_idl.get_address_sets() + self.assertEqual(len(address_sets), 4) + + def test_get_port_group_not_supported(self): + self._load_nb_db() + # Make sure that PG tables doesn't exist in fake db. + self._tables.pop('Port_Group', None) + port_group = self.nb_ovn_idl.get_port_group(str(uuid.uuid4())) + self.assertIsNone(port_group) + + def test_get_port_groups_not_supported(self): + self._load_nb_db() + # Make sure that PG tables doesn't exist in fake db. + self._tables.pop('Port_Group', None) + port_groups = self.nb_ovn_idl.get_port_groups() + self.assertEqual({}, port_groups) + + +class TestSBImplIdlOvn(TestDBImplIdlOvn): + + fake_set = { + 'chassis': [ + {'name': 'host-1', 'hostname': 'host-1.localdomain.com', + 'external_ids': {'ovn-bridge-mappings': + 'public:br-ex,private:br-0'}}, + {'name': 'host-2', 'hostname': 'host-2.localdomain.com', + 'external_ids': {'ovn-bridge-mappings': + 'public:br-ex,public2:br-ex'}}, + {'name': 'host-3', 'hostname': 'host-3.localdomain.com', + 'external_ids': {'ovn-bridge-mappings': + 'public:br-ex'}}], + } + + def setUp(self): + super(TestSBImplIdlOvn, self).setUp() + + self.chassis_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() + self._tables = {} + self._tables['Chassis'] = self.chassis_table + + with mock.patch.object(impl_idl_ovn, 'get_connection', + return_value=mock.Mock()): + impl_idl_ovn.OvsdbSbOvnIdl.ovsdb_connection = None + self.sb_ovn_idl = impl_idl_ovn.OvsdbSbOvnIdl(mock.Mock()) + + self.sb_ovn_idl.idl.tables = self._tables + + def _load_sb_db(self): + # Load Chassis + fake_chassis = TestSBImplIdlOvn.fake_set['chassis'] + self._load_ovsdb_fake_rows(self.chassis_table, fake_chassis) + + @mock.patch.object(impl_idl_ovn.OvsdbSbOvnIdl, 'ovsdb_connection', None) + @mock.patch.object(impl_idl_ovn, 'get_connection', mock.Mock()) + def test_setting_ovsdb_probe_timeout_default_value(self): + inst = impl_idl_ovn.OvsdbSbOvnIdl(mock.Mock()) + inst.idl._session.reconnect.set_probe_interval.assert_called_with( + 60000) + + @mock.patch.object(impl_idl_ovn.OvsdbSbOvnIdl, 'ovsdb_connection', None) + @mock.patch.object(impl_idl_ovn, 'get_connection', mock.Mock()) + @mock.patch.object(ovn_conf, 'get_ovn_ovsdb_probe_interval') + def test_setting_ovsdb_probe_timeout(self, mock_get_probe_interval): + mock_get_probe_interval.return_value = 5000 + inst = impl_idl_ovn.OvsdbSbOvnIdl(mock.Mock()) + inst.idl._session.reconnect.set_probe_interval.assert_called_with(5000) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py new file mode 100644 index 00000000000..94ac360dba9 --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py @@ -0,0 +1,294 @@ +# Copyright 2016 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import os + +import mock +from oslo_utils import timeutils +from oslo_utils import uuidutils +from ovs.db import idl as ovs_idl +from ovs import poller +from ovs.stream import Stream +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp.backend.ovs_idl import idlutils + +from neutron.common.ovn import constants as ovn_const +from neutron.common.ovn import hash_ring_manager +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from neutron.db import ovn_hash_ring_db +from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor +from neutron.tests import base +from neutron.tests.unit import fake_resources as fakes + + +basedir = os.path.dirname(os.path.abspath(__file__)) +schema_files = { + 'OVN_Northbound': os.path.join(basedir, 'schemas', 'ovn-nb.ovsschema'), + 'OVN_Southbound': os.path.join(basedir, 'schemas', 'ovn-sb.ovsschema'), +} + +OVN_NB_SCHEMA = { + "name": "OVN_Northbound", "version": "3.0.0", + "tables": { + "Logical_Switch_Port": { + "columns": { + "name": {"type": "string"}, + "type": {"type": "string"}, + "addresses": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "port_security": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "up": {"type": {"key": "boolean", "min": 0, "max": 1}}}, + "indexes": [["name"]], + "isRoot": False, + }, + "Logical_Switch": { + "columns": {"name": {"type": "string"}}, + "indexes": [["name"]], + "isRoot": True, + } + } +} + + +OVN_SB_SCHEMA = { + "name": "OVN_Southbound", "version": "1.3.0", + "tables": { + "Chassis": { + "columns": { + "name": {"type": "string"}, + "hostname": {"type": "string"}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": True, + "indexes": [["name"]] + } + } +} + + +ROW_CREATE = ovsdb_monitor.BaseEvent.ROW_CREATE +ROW_UPDATE = ovsdb_monitor.BaseEvent.ROW_UPDATE + + +class TestOvnDbNotifyHandler(base.BaseTestCase): + + def setUp(self): + super(TestOvnDbNotifyHandler, self).setUp() + self.handler = ovsdb_monitor.OvnDbNotifyHandler(mock.ANY) + self.watched_events = self.handler._RowEventHandler__watched_events + + def test_watch_and_unwatch_events(self): + expected_events = set() + networking_event = mock.Mock() + ovn_event = mock.Mock() + unknown_event = mock.Mock() + + self.assertItemsEqual(set(), self.watched_events) + + expected_events.add(networking_event) + self.handler.watch_event(networking_event) + self.assertItemsEqual(expected_events, self.watched_events) + + expected_events.add(ovn_event) + self.handler.watch_events([ovn_event]) + self.assertItemsEqual(expected_events, self.watched_events) + + self.handler.unwatch_events([networking_event, ovn_event]) + self.handler.unwatch_event(unknown_event) + self.handler.unwatch_events([unknown_event]) + self.assertItemsEqual(set(), self.watched_events) + + def test_shutdown(self): + self.handler.shutdown() + + +# class TestOvnBaseConnection(base.TestCase): +# +# Each test is being deleted, but for reviewers sake I wanted to exaplain why: +# +# @mock.patch.object(idlutils, 'get_schema_helper') +# def testget_schema_helper_success(self, mock_gsh): +# +# 1. OvnBaseConnection and OvnConnection no longer exist +# 2. get_schema_helper is no longer a part of the Connection class +# +# @mock.patch.object(idlutils, 'get_schema_helper') +# def testget_schema_helper_initial_exception(self, mock_gsh): +# +# @mock.patch.object(idlutils, 'get_schema_helper') +# def testget_schema_helper_all_exception(self, mock_gsh): +# +# 3. The only reason get_schema_helper had a retry loop was for Neutron's +# use case of trying to set the Manager to listen on ptcp:127.0.0.1:6640 +# if it wasn't already set up. Since that code being removed was the whole +# reason to re-implement get_schema_helper here,the exception retry is not +# needed and therefor is not a part of ovsdbapp's implementation of +# idlutils.get_schema_helper which we now use directly in from_server() +# 4. These tests now would be testing the various from_server() calls, but +# there is almost nothing to test in those except maybe SSL being set up +# but that was done below. + +class TestOvnConnection(base.BaseTestCase): + + def setUp(self): + super(TestOvnConnection, self).setUp() + + @mock.patch.object(idlutils, 'get_schema_helper') + @mock.patch.object(idlutils, 'wait_for_change') + def _test_connection_start(self, mock_wfc, mock_gsh, + idl_class, schema): + mock_gsh.return_value = ovs_idl.SchemaHelper( + location=schema_files[schema]) + _idl = idl_class.from_server('punix:/tmp/fake', schema, mock.Mock()) + self.ovn_connection = connection.Connection(_idl, mock.Mock()) + with mock.patch.object(poller, 'Poller'), \ + mock.patch('threading.Thread'): + self.ovn_connection.start() + # A second start attempt shouldn't re-register. + self.ovn_connection.start() + + self.ovn_connection.thread.start.assert_called_once_with() + + def test_connection_nb_start(self): + ovn_conf.cfg.CONF.set_override('ovn_nb_private_key', 'foo-key', 'ovn') + Stream.ssl_set_private_key_file = mock.Mock() + Stream.ssl_set_certificate_file = mock.Mock() + Stream.ssl_set_ca_cert_file = mock.Mock() + + self._test_connection_start(idl_class=ovsdb_monitor.OvnNbIdl, + schema='OVN_Northbound') + + Stream.ssl_set_private_key_file.assert_called_once_with('foo-key') + Stream.ssl_set_certificate_file.assert_not_called() + Stream.ssl_set_ca_cert_file.assert_not_called() + + def test_connection_sb_start(self): + self._test_connection_start(idl_class=ovsdb_monitor.OvnSbIdl, + schema='OVN_Southbound') + + +class TestOvnIdlDistributedLock(base.BaseTestCase): + + def setUp(self): + super(TestOvnIdlDistributedLock, self).setUp() + self.node_uuid = uuidutils.generate_uuid() + self.fake_driver = mock.Mock() + self.fake_driver.node_uuid = self.node_uuid + self.fake_event = 'fake-event' + self.fake_row = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'_table': mock.Mock(name='FakeTable')}) + helper = ovs_idl.SchemaHelper(schema_json=OVN_NB_SCHEMA) + helper.register_all() + + with mock.patch.object(ovsdb_monitor, 'OvnDbNotifyHandler'): + self.idl = ovsdb_monitor.OvnIdlDistributedLock( + self.fake_driver, 'punix:/tmp/fake', helper) + + self.mock_get_node = mock.patch.object( + hash_ring_manager.HashRingManager, + 'get_node', return_value=self.node_uuid).start() + + @mock.patch.object(ovn_hash_ring_db, 'touch_node') + def test_notify(self, mock_touch_node): + self.idl.notify(self.fake_event, self.fake_row) + + mock_touch_node.assert_called_once_with(mock.ANY, self.node_uuid) + self.idl.notify_handler.notify.assert_called_once_with( + self.fake_event, self.fake_row, None) + + @mock.patch.object(ovn_hash_ring_db, 'touch_node') + def test_notify_skip_touch_node(self, mock_touch_node): + # Set a time for last touch + self.idl._last_touch = timeutils.utcnow() + self.idl.notify(self.fake_event, self.fake_row) + + # Assert that touch_node() wasn't called + self.assertFalse(mock_touch_node.called) + self.idl.notify_handler.notify.assert_called_once_with( + self.fake_event, self.fake_row, None) + + @mock.patch.object(ovn_hash_ring_db, 'touch_node') + def test_notify_last_touch_expired(self, mock_touch_node): + # Set a time for last touch + self.idl._last_touch = timeutils.utcnow() + + # Let's expire the touch node interval for the next utcnow() + with mock.patch.object(timeutils, 'utcnow') as mock_utcnow: + mock_utcnow.return_value = ( + self.idl._last_touch + datetime.timedelta( + seconds=ovn_const.HASH_RING_TOUCH_INTERVAL + 1)) + self.idl.notify(self.fake_event, self.fake_row) + + # Assert that touch_node() was invoked + mock_touch_node.assert_called_once_with(mock.ANY, self.node_uuid) + self.idl.notify_handler.notify.assert_called_once_with( + self.fake_event, self.fake_row, None) + + @mock.patch.object(ovsdb_monitor.LOG, 'exception') + @mock.patch.object(ovn_hash_ring_db, 'touch_node') + def test_notify_touch_node_exception(self, mock_touch_node, mock_log): + mock_touch_node.side_effect = Exception('BoOooOmmMmmMm') + self.idl.notify(self.fake_event, self.fake_row) + + # Assert that in an eventual failure on touch_node() the event + # will continue to be processed by notify_handler.notify() + mock_touch_node.assert_called_once_with(mock.ANY, self.node_uuid) + # Assert we are logging the exception + self.assertTrue(mock_log.called) + self.idl.notify_handler.notify.assert_called_once_with( + self.fake_event, self.fake_row, None) + + def test_notify_different_node(self): + self.mock_get_node.return_value = 'different-node-uuid' + self.idl.notify('fake-event', self.fake_row) + # Assert that notify() wasn't called for a different node uuid + self.assertFalse(self.idl.notify_handler.notify.called) + + +class TestPortBindingChassisUpdateEvent(base.BaseTestCase): + def setUp(self): + super(TestPortBindingChassisUpdateEvent, self).setUp() + self.driver = mock.Mock() + self.event = ovsdb_monitor.PortBindingChassisUpdateEvent(self.driver) + + def _test_event(self, event, row, old): + if self.event.matches(event, row, old): + self.event.run(event, row, old) + self.driver.set_port_status_up.assert_called() + else: + self.driver.set_port_status_up.assert_not_called() + + def test_event_matches(self): + # NOTE(twilson) This primarily tests implementation details. If a + # scenario test is written that handles shutting down a compute + # node uncleanly and performing a 'host-evacuate', this can be removed + pbtable = fakes.FakeOvsdbTable.create_one_ovsdb_table( + attrs={'name': 'Port_Binding'}) + ovsdb_row = fakes.FakeOvsdbRow.create_one_ovsdb_row + self.driver._nb_ovn.lookup.return_value = ovsdb_row(attrs={'up': True}) + self._test_event( + self.event.ROW_UPDATE, + ovsdb_row(attrs={'_table': pbtable, 'chassis': 'one', + 'type': '_fake_', 'logical_port': 'foo'}), + ovsdb_row(attrs={'_table': pbtable, 'chassis': 'two', + 'type': '_fake_'})) + + +# NOTE(ralonsoh): once the OVN mech driver is implemented, we'll be able to +# test OvnNbIdl and OvnSbIdl properly.