From 49366ecada529a929712ded7681d797c75b386cb Mon Sep 17 00:00:00 2001 From: Miguel Lavalle Date: Tue, 23 May 2023 19:48:49 -0500 Subject: [PATCH] Router flavors and service type for OVN Support is added to the OVN L3 service plugin for the router flavors and service type framework Partial-Bug: #2020823 Change-Id: If40d7b39e7b59a39ff7622bd823dbdb14bfc69d2 --- doc/source/admin/config-router-flavor-ovn.rst | 148 +++++++ doc/source/admin/config.rst | 1 + neutron/common/ovn/extensions.py | 4 + neutron/common/ovn/utils.py | 5 + neutron/db/l3_db.py | 17 +- .../drivers/ovn/mech_driver/mech_driver.py | 12 +- .../ovn/mech_driver/ovsdb/maintenance.py | 2 +- .../ovn/mech_driver/ovsdb/ovn_client.py | 6 +- .../service_providers/driver_controller.py | 8 +- neutron/services/ovn_l3/plugin.py | 188 ++------- .../ovn_l3/service_providers/__init__.py | 0 .../service_providers/driver_controller.py | 65 +++ .../services/ovn_l3/service_providers/ovn.py | 252 ++++++++++++ .../ovn_l3/service_providers/user_defined.py | 170 ++++++++ .../ovn/mech_driver/ovsdb/test_maintenance.py | 103 ++--- .../ovn/mech_driver/test_mech_driver.py | 7 +- .../tests/unit/db/test_db_base_plugin_v2.py | 20 +- .../ovn/mech_driver/test_mech_driver.py | 5 +- neutron/tests/unit/plugins/ml2/test_plugin.py | 8 +- .../ovn_l3/service_providers/__init__.py | 0 .../service_providers/test_user_defined.py | 124 ++++++ .../tests/unit/services/ovn_l3/test_plugin.py | 387 +++++++++++++----- ...vn-l3-router-flavors-5c2e14fca15723fa.yaml | 8 + 23 files changed, 1211 insertions(+), 329 deletions(-) create mode 100644 doc/source/admin/config-router-flavor-ovn.rst create mode 100644 neutron/services/ovn_l3/service_providers/__init__.py create mode 100644 neutron/services/ovn_l3/service_providers/driver_controller.py create mode 100644 neutron/services/ovn_l3/service_providers/ovn.py create mode 100644 neutron/services/ovn_l3/service_providers/user_defined.py create mode 100644 neutron/tests/unit/services/ovn_l3/service_providers/__init__.py create mode 100644 neutron/tests/unit/services/ovn_l3/service_providers/test_user_defined.py create mode 100644 releasenotes/notes/add-ovn-l3-router-flavors-5c2e14fca15723fa.yaml diff --git a/doc/source/admin/config-router-flavor-ovn.rst b/doc/source/admin/config-router-flavor-ovn.rst new file mode 100644 index 00000000000..25b167369e5 --- /dev/null +++ b/doc/source/admin/config-router-flavor-ovn.rst @@ -0,0 +1,148 @@ +.. _config-router-flavor-ovn: + +=================================================== +Creating a L3 OVN router with a user-defined flavor +=================================================== + +In this section we describe the steps necessary to create a router with a user +defined flavor. + + .. note:: + The following example refers to a dummy user-defined service provider, + which in a real situation must be replaced with user provided code. + +#. Add the service provider to neutron.conf: + + .. code-block:: console + + [service_providers] + service_provider = L3_ROUTER_NAT:user-defined:neutron.services.ovn_l3.service_providers.user_defined.UserDefined + +#. Re-start the neutron server and verify the user-defined provider has been + loaded: + + .. code-block:: console + + $ openstack network service provider list + +---------------+--------------+---------+ + | Service Type | Name | Default | + +---------------+--------------+---------+ + | L3_ROUTER_NAT | user-defined | False | + | L3_ROUTER_NAT | ovn | True | + +---------------+--------------+---------+ + +#. Create a service profile for the router flavor: + + .. code-block:: console + + $ openstack network flavor profile create --description "User-defined router flavor profile" --enable --driver neutron.services.ovn_l3.service_providers.user_defined.UserDefined + +-------------+--------------------------------------------------------------------+ + | Field | Value | + +-------------+--------------------------------------------------------------------+ + | description | User-defined router flavor profile | + | driver | neutron.services.ovn_l3.service_providers.user_defined.UserDefined | + | enabled | True | + | id | a717c92c-63f7-47e8-9efb-6ad0d61c4875 | + | meta_info | | + | project_id | None | + +-------------+--------------------------------------------------------------------+ + +#. Create the router flavor: + + .. code-block:: console + + $ openstack network flavor create --service-type L3_ROUTER_NAT --description "User-defined flavor for routers in the L3 OVN plugin" user-defined-router-flavor + +---------------------+------------------------------------------------------+ + | Field | Value | + +---------------------+------------------------------------------------------+ + | description | User-defined flavor for routers in the L3 OVN plugin | + | enabled | True | + | id | e47c1c5c-629b-4c48-b49a-78abe6ac7696 | + | name | user-defined-router-flavor | + | service_profile_ids | [] | + | service_type | L3_ROUTER_NAT | + +---------------------+------------------------------------------------------+ + +#. Add service profile to router flavor: + + .. code-block:: console + + $ openstack network flavor add profile user-defined-router-flavor a717c92c-63f7-47e8-9efb-6ad0d61c4875 + +#. Create router specifying user-defined flavor: + + .. code-block:: console + + $ openstack router create router-of-user-defined-flavor --external-gateway public --flavor-id e47c1c5c-629b-4c48-b49a-78abe6ac7696 --max-width 100 + +-------------------------+------------------------------------------------------------------------+ + | Field | Value | + +-------------------------+------------------------------------------------------------------------+ + | admin_state_up | UP | + | availability_zone_hints | | + | availability_zones | | + | created_at | 2023-05-25T22:34:16Z | + | description | | + | enable_ndp_proxy | None | + | external_gateway_info | {"network_id": "ba485dc9-2459-41c1-9d4f-71914a7fba2a", | + | | "external_fixed_ips": [{"subnet_id": | + | | "2e3adb94-c544-4916-a9fb-27a9dea21820", "ip_address": "172.24.8.69"}, | + | | {"subnet_id": "996ed143-917b-4783-8349-03c6a6d9603e", "ip_address": | + | | "2001:db8::261"}], "enable_snat": true} | + | flavor_id | e47c1c5c-629b-4c48-b49a-78abe6ac7696 | + | id | 9f5fec56-1829-4bad-abe5-7b4221649c8e | + | name | router-of-user-defined-flavor | + | project_id | b807321af03f44dc808ff06bbc845804 | + | revision_number | 3 | + | routes | | + | status | ACTIVE | + | tags | | + | tenant_id | b807321af03f44dc808ff06bbc845804 | + | updated_at | 2023-05-25T22:34:16Z | + +-------------------------+------------------------------------------------------------------------+ + + +#. Create an OVN flavor router to verify they co-exist with the user-defined + flavor: + + .. code-block:: console + + $ openstack router create ovn-flavor-router --external-gateway public --max-width 100 + +-------------------------+------------------------------------------------------------------------+ + | Field | Value | + +-------------------------+------------------------------------------------------------------------+ + | admin_state_up | UP | + | availability_zone_hints | | + | availability_zones | | + | created_at | 2023-05-25T23:34:20Z | + | description | | + | enable_ndp_proxy | None | + | external_gateway_info | {"network_id": "ba485dc9-2459-41c1-9d4f-71914a7fba2a", | + | | "external_fixed_ips": [{"subnet_id": | + | | "2e3adb94-c544-4916-a9fb-27a9dea21820", "ip_address": "172.24.8.195"}, | + | | {"subnet_id": "996ed143-917b-4783-8349-03c6a6d9603e", "ip_address": | + | | "2001:db8::263"}], "enable_snat": true} | + | flavor_id | None | + | id | 21889ed3-b8df-4b0e-9a64-92ba9fab655d | + | name | ovn-flavor-router | + | project_id | b807321af03f44dc808ff06bbc845804 | + | revision_number | 3 | + | routes | | + | status | ACTIVE | + | tags | | + | tenant_id | e6d6b109d16b4e5e857a10034f4ba558 | + | updated_at | 2023-07-20T23:34:21Z | + +-------------------------+------------------------------------------------------------------------+ + + +#. List routers to verify: + + .. code-block:: console + + $ openstack router list + +--------------------------------------+-------------------------------+--------+-------+----------------------------------+ + | ID | Name | Status | State | Project | + +--------------------------------------+-------------------------------+--------+-------+----------------------------------+ + | 21889ed3-b8df-4b0e-9a64-92ba9fab655d | ovn-flavor-router | ACTIVE | UP | b807321af03f44dc808ff06bbc845804 | + | 9f5fec56-1829-4bad-abe5-7b4221649c8e | router-of-user-defined-flavor | ACTIVE | UP | b807321af03f44dc808ff06bbc845804 | + | e9f25566-ff73-4a76-aeb4-969c819f9c47 | router1 | ACTIVE | UP | 1bf97e3957654c0182a48727d619e00f | + +--------------------------------------+-------------------------------+--------+-------+----------------------------------+ diff --git a/doc/source/admin/config.rst b/doc/source/admin/config.rst index cb598beedab..dcdfec0a719 100644 --- a/doc/source/admin/config.rst +++ b/doc/source/admin/config.rst @@ -38,6 +38,7 @@ Configuration config-qos-min-pps config-rbac config-routed-networks + config-router-flavor-ovn config-sriov config-sfc config-service-subnets diff --git a/neutron/common/ovn/extensions.py b/neutron/common/ovn/extensions.py index 903b7a09dc4..09417ec57e1 100644 --- a/neutron/common/ovn/extensions.py +++ b/neutron/common/ovn/extensions.py @@ -36,10 +36,12 @@ from neutron_lib.api.definitions import fip_pf_port_range from neutron_lib.api.definitions import fip_port_details from neutron_lib.api.definitions import firewall_v2 from neutron_lib.api.definitions import firewall_v2_stdattrs +from neutron_lib.api.definitions import flavors from neutron_lib.api.definitions import floating_ip_port_forwarding from neutron_lib.api.definitions import floatingip_pools from neutron_lib.api.definitions import l3 from neutron_lib.api.definitions import l3_ext_gw_mode +from neutron_lib.api.definitions import l3_flavors from neutron_lib.api.definitions import logging from neutron_lib.api.definitions import multiprovidernet from neutron_lib.api.definitions import network_availability_zone @@ -114,6 +116,8 @@ ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [ agent_def.ALIAS, az_def.ALIAS, raz_def.ALIAS, + flavors.ALIAS, + l3_flavors.ALIAS, ] ML2_SUPPORTED_API_EXTENSIONS = [ address_group.ALIAS, diff --git a/neutron/common/ovn/utils.py b/neutron/common/ovn/utils.py index 2b2c79255cf..ea69a93af23 100644 --- a/neutron/common/ovn/utils.py +++ b/neutron/common/ovn/utils.py @@ -1215,3 +1215,8 @@ def is_additional_chassis_supported(idl): def is_nat_gateway_port_supported(idl): return idl.is_col_present('NAT', 'gateway_port') + + +def is_ovn_provider_router(router): + flavor_id = router.get('flavor_id') + return flavor_id is None or flavor_id is const.ATTR_NOT_SPECIFIED diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 5a4d9f257d9..4460c65291f 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -1216,20 +1216,21 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, gw_ips = [x['ip_address'] for x in router.gw_port.fixed_ips] cidrs = [x['cidr'] for x in subnets] + subnet_ids = [subnet['id'] for subnet in subnets] metadata = {'interface_info': interface_info, 'port': port, 'gateway_ips': gw_ips, - 'network_id': gw_network_id, 'cidrs': cidrs} + 'network_id': gw_network_id, 'cidrs': cidrs, + 'subnet_ids': subnet_ids} registry.publish(resources.ROUTER_INTERFACE, events.AFTER_DELETE, self, payload=events.DBEventPayload( context, metadata=metadata, - resource_id=router_id)) + resource_id=router_id, + states=(router,))) return self._make_router_interface_info(router_id, port['tenant_id'], port['id'], port['network_id'], - subnets[0]['id'], - [subnet['id'] for subnet in - subnets]) + subnets[0]['id'], subnet_ids) def _get_floatingip(self, context, id): floatingip = l3_obj.FloatingIP.get_object(context, id=id) @@ -1571,7 +1572,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, payload=events.DBEventPayload( context, states=(floatingip_dict,), resource_id=floatingip_obj.id, - metadata={'association_event': assoc_result})) + metadata={'association_event': assoc_result}, + request_body=floatingip)) if assoc_result: LOG.info(FIP_ASSOC_MSG, {'fip_id': floatingip_obj.id, @@ -1637,7 +1639,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, payload=events.DBEventPayload( context, states=(old_floatingip, floatingip_dict), resource_id=floatingip_obj.id, - metadata={'association_event': assoc_result})) + metadata={'association_event': assoc_result}, + request_body=floatingip)) if assoc_result is not None: port_id = old_fixed_port_id or floatingip_obj.fixed_port_id assoc = 'associated' if assoc_result else 'disassociated' diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py index 9d4a472ebd9..cc0d5647492 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py @@ -57,6 +57,7 @@ from neutron.db import ovn_hash_ring_db from neutron.db import ovn_revision_numbers_db from neutron.db import provisioning_blocks from neutron.extensions import securitygroup as ext_sg +from neutron.objects import router from neutron.plugins.ml2 import db as ml2_db from neutron.plugins.ml2.drivers.ovn.agent import neutron_agent as n_agent from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \ @@ -689,12 +690,18 @@ class OVNMechanismDriver(api.MechanismDriver): # in the case of router ports we also need to # track the creation and update of the LRP OVN objects - if ovn_utils.is_lsp_router_port(port): + if (ovn_utils.is_lsp_router_port(port) and + self._is_ovn_router_flavor_port(context, port)): ovn_revision_numbers_db.create_initial_revision( context.plugin_context, port['id'], ovn_const.TYPE_ROUTER_PORTS, std_attr_id=context.current['standard_attr_id']) + def _is_ovn_router_flavor_port(self, context, port): + router_obj = router.Router.get_object(context.plugin_context, + id=port['device_id']) + return ovn_utils.is_ovn_provider_router(router_obj) + def _is_port_provisioning_required(self, port, host, original_host=None): vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL) if vnic_type not in self.supported_vnic_types: @@ -829,7 +836,8 @@ class OVNMechanismDriver(api.MechanismDriver): self._insert_port_provisioning_block(context.plugin_context, port['id']) - if ovn_utils.is_lsp_router_port(port): + if (ovn_utils.is_lsp_router_port(port) and + self._is_ovn_router_flavor_port(context, port)): # handle the case when an existing port is added to a # logical router so we need to track the creation of the lrp if not ovn_utils.is_lsp_router_port(original_port): diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py index e5697024545..6f036a1be4e 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py @@ -391,7 +391,7 @@ class DBInconsistenciesPeriodics(SchemaAwarePeriodicsBase): def _create_lrouter_port(self, context, port): router_id = port['device_id'] iface_info = self._ovn_client._l3_plugin._add_neutron_router_interface( - context, router_id, {'port_id': port['id']}, may_exist=True) + context, router_id, {'port_id': port['id']}) self._ovn_client.create_router_port(context, router_id, iface_info) def _check_subnet_global_dhcp_opts(self): diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py index af920d0afee..c47c00a2ca7 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py @@ -51,6 +51,7 @@ from neutron.conf.agent import ovs_conf from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf from neutron.db import ovn_revision_numbers_db as db_rev from neutron.db import segments_db +from neutron.objects import router from neutron.plugins.ml2 import db as ml2_db from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \ import placement as placement_extension @@ -646,7 +647,10 @@ class OVNClient(object): # LogicalSwitchPortUpdateDownEvent, that will most likely # cause a revision conflict. # https://bugs.launchpad.net/neutron/+bug/1955578 - columns_dict['type'] = ovn_const.LSP_TYPE_ROUTER + router_obj = router.Router.get_object(context, + id=port['device_id']) + if utils.is_ovn_provider_router(router_obj): + columns_dict['type'] = ovn_const.LSP_TYPE_ROUTER port_info.options.update( self._nb_idl.get_router_port_options(port['id'])) else: diff --git a/neutron/services/l3_router/service_providers/driver_controller.py b/neutron/services/l3_router/service_providers/driver_controller.py index fc8ae2e4f1d..729ac7054b0 100644 --- a/neutron/services/l3_router/service_providers/driver_controller.py +++ b/neutron/services/l3_router/service_providers/driver_controller.py @@ -87,7 +87,7 @@ class DriverController(object): router = payload.latest_state router_db = payload.metadata['router_db'] router_id = payload.resource_id - if _flavor_specified(router): + if flavor_specified(router): router_db.flavor_id = router['flavor_id'] drv = self._get_provider_for_create(context, router) self._stm.add_resource_association(context, plugin_constants.L3, @@ -127,7 +127,7 @@ class DriverController(object): drv = self.get_provider_for_router(payload.context, payload.resource_id) new_drv = None - if _flavor_specified(payload.request_body): + if flavor_specified(payload.request_body): if (payload.request_body['flavor_id'] != payload.states[0]['flavor_id']): # TODO(kevinbenton): this is currently disallowed by the API @@ -210,7 +210,7 @@ class DriverController(object): def _get_provider_for_create(self, context, router): """Get provider based on flavor or ha/distributed flags.""" - if not _flavor_specified(router): + if not flavor_specified(router): return self._attrs_to_driver(router) return self._get_l3_driver_by_flavor(context, router['flavor_id']) @@ -293,7 +293,7 @@ def _is_ha(ha_attr): return True -def _flavor_specified(router): +def flavor_specified(router): return ('flavor_id' in router and router['flavor_id'] != lib_const.ATTR_NOT_SPECIFIED) diff --git a/neutron/services/ovn_l3/plugin.py b/neutron/services/ovn_l3/plugin.py index 99c15b431c9..de1d35b2ec8 100644 --- a/neutron/services/ovn_l3/plugin.py +++ b/neutron/services/ovn_l3/plugin.py @@ -13,6 +13,7 @@ # from neutron_lib.api.definitions import external_net +from neutron_lib.api.definitions import l3 as l3_apidef from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net as pnet from neutron_lib.api.definitions import qos_fip as qos_fip_apidef @@ -23,19 +24,18 @@ from neutron_lib.callbacks import resources from neutron_lib import constants as n_const from neutron_lib import context as n_context from neutron_lib.db import api as db_api +from neutron_lib.db import resource_extend from neutron_lib import exceptions as n_exc from neutron_lib.exceptions import availability_zone as az_exc from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.services import base as service_base from oslo_log import log -from oslo_utils import excutils from neutron._i18n import _ from neutron.common.ovn import constants as ovn_const from neutron.common.ovn import extensions from neutron.common.ovn import utils -from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf from neutron.db.availability_zone import router as router_az_db from neutron.db import dns_db from neutron.db import extraroute_db @@ -46,11 +46,11 @@ from neutron.db import l3_fip_qos from neutron.db import l3_gateway_ip_qos from neutron.db import l3_gwmode_db from neutron.db.models import l3 as l3_models -from neutron.db import ovn_revision_numbers_db as db_rev from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client from neutron.quota import resource_registry from neutron.scheduler import l3_ovn_scheduler from neutron.services.ovn_l3 import exceptions as ovn_l3_exc +from neutron.services.ovn_l3.service_providers import driver_controller from neutron.services.portforwarding.drivers.ovn import driver \ as port_forwarding @@ -94,15 +94,7 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, self._ovn_client_inst = None self.scheduler = l3_ovn_scheduler.get_scheduler() self.port_forwarding = port_forwarding.OVNPortForwarding(self) - self._register_precommit_callbacks() - - def _register_precommit_callbacks(self): - registry.subscribe( - self.create_router_precommit, resources.ROUTER, - events.PRECOMMIT_CREATE) - registry.subscribe( - self.create_floatingip_precommit, resources.FLOATING_IP, - events.PRECOMMIT_CREATE) + self.l3_driver_controller = driver_controller.DriverController(self) @staticmethod def _disable_qos_extensions_by_extension_drivers(aliases): @@ -170,67 +162,13 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, return ("L3 Router Service Plugin for basic L3 forwarding" " using OVN") - def create_router_precommit(self, resource, event, trigger, payload): - context = payload.context - context.session.flush() - router_id = payload.resource_id - router_db = payload.metadata['router_db'] - # NOTE(ralonsoh): the "distributed" flag is a static configuration - # parameter that needs to be defined only during the router creation. - extra_attr = router_db['extra_attributes'] - extra_attr.distributed = ovn_conf.is_ovn_distributed_floating_ip() - - db_rev.create_initial_revision( - context, router_id, ovn_const.TYPE_ROUTERS, - std_attr_id=router_db.standard_attr.id) - - def create_router(self, context, router): - router = super(OVNL3RouterPlugin, self).create_router(context, router) - try: - self._ovn_client.create_router(context, router) - except Exception: - with excutils.save_and_reraise_exception(): - # Delete the logical router - LOG.exception('Unable to create lrouter %s', router['id']) - super(OVNL3RouterPlugin, self).delete_router(context, - router['id']) - return router - - def update_router(self, context, id, router): - original_router = self.get_router(context, id) - result = super(OVNL3RouterPlugin, self).update_router(context, id, - router) - try: - self._ovn_client.update_router(context, result, - original_router) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.exception('Unable to update lrouter %s', id) - revert_router = {'router': original_router} - super(OVNL3RouterPlugin, self).update_router(context, id, - revert_router) - return result - - def delete_router(self, context, id): - original_router = self.get_router(context, id) - super(OVNL3RouterPlugin, self).delete_router(context, id) - try: - self._ovn_client.delete_router(context, id) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.exception('Unable to delete lrouter %s', id) - super(OVNL3RouterPlugin, self).create_router( - context, {'router': original_router}) - def _add_neutron_router_interface(self, context, router_id, - interface_info, may_exist=False): + interface_info): try: router_interface_info = ( super(OVNL3RouterPlugin, self).add_router_interface( context, router_id, interface_info)) except n_exc.PortInUse: - if not may_exist: - raise # NOTE(lucasagomes): If the port is already being used it means # the interface has been created already, let's just fetch it from # the database. Perhaps the code below should live in Neutron @@ -247,70 +185,24 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, return router_interface_info - def add_router_interface(self, context, router_id, interface_info=None): - router_interface_info = self._add_neutron_router_interface( - context, router_id, interface_info) - try: - self._ovn_client.create_router_port( - context, router_id, router_interface_info) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.exception( - 'Unable to add router interface to lrouter %s. ' - 'Interface info: %s', router_id, interface_info) - super(OVNL3RouterPlugin, self).remove_router_interface( - context, router_id, router_interface_info) - - return router_interface_info - - def remove_router_interface(self, context, router_id, interface_info): - router_interface_info = ( - super(OVNL3RouterPlugin, self).remove_router_interface( - context, router_id, interface_info)) - try: - port_id = router_interface_info['port_id'] - subnet_ids = router_interface_info.get('subnet_ids') - self._ovn_client.delete_router_port( - context, port_id, router_id=router_id, subnet_ids=subnet_ids) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.exception( - 'Unable to remove router interface from lrouter %s. ' - 'Interface info: %s', router_id, interface_info) - super(OVNL3RouterPlugin, self).add_router_interface( - context, router_id, interface_info) - return router_interface_info - - def create_floatingip_precommit(self, resource, event, trigger, payload): - context = payload.context - floatingip_id = payload.resource_id - floatingip_db = payload.desired_state - - db_rev.create_initial_revision( - context, floatingip_id, ovn_const.TYPE_FLOATINGIPS, - std_attr_id=floatingip_db.standard_attr.id) - def create_floatingip(self, context, floatingip, initial_status=n_const.FLOATINGIP_STATUS_DOWN): - fip = super(OVNL3RouterPlugin, self).create_floatingip( + # The OVN L3 plugin creates floating IPs in down status by default, + # whereas the L3 DB layer creates them in active status. So we keep + # this method to create the floating IP in the DB with status down, + # while the flavor drivers are responsible for calling the correct + # backend to instatiate the floating IP in the data plane + return super(OVNL3RouterPlugin, self).create_floatingip( context, floatingip, initial_status) - self._ovn_client.create_floatingip(context, fip) - return fip - - def delete_floatingip(self, context, id): - super(OVNL3RouterPlugin, self).delete_floatingip(context, id) - self._ovn_client.delete_floatingip(context, id) - - def update_floatingip(self, context, id, floatingip): - fip = super(OVNL3RouterPlugin, self).update_floatingip(context, id, - floatingip) - self._ovn_client.update_floatingip(context, fip, floatingip) - return fip def update_floatingip_status(self, context, floatingip_id, status): fip = self.update_floatingip_status_retry( context, floatingip_id, status) - self._ovn_client.update_floatingip_status(context, fip) + registry.publish( + resources.FLOATING_IP, events.AFTER_STATUS_UPDATE, self, + payload=events.DBEventPayload( + context, states=(fip,), + resource_id=floatingip_id)) return fip @db_api.retry_if_session_inactive() @@ -319,30 +211,6 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, return super(OVNL3RouterPlugin, self).update_floatingip_status( context, floatingip_id, status) - def disassociate_floatingips(self, context, port_id, do_notify=True): - fips = self.get_floatingips(context.elevated(), - filters={'port_id': [port_id]}) - router_ids = super(OVNL3RouterPlugin, self).disassociate_floatingips( - context, port_id, do_notify) - for fip in fips: - router_id = fip.get('router_id') - fixed_ip_address = fip.get('fixed_ip_address') - if router_id and fixed_ip_address: - update_fip = { - 'id': fip['id'], - 'logical_ip': fixed_ip_address, - 'external_ip': fip['floating_ip_address'], - 'floating_network_id': fip['floating_network_id']} - try: - self._ovn_client.disassociate_floatingip(update_fip, - router_id) - self.update_floatingip_status( - context, fip['id'], n_const.FLOATINGIP_STATUS_DOWN) - except Exception as e: - LOG.error('Error in disassociating floatingip %(id)s: ' - '%(error)s', {'id': fip['id'], 'error': e}) - return router_ids - def _get_gateway_port_physnet_mapping(self): # This function returns all gateway ports with corresponding # external network's physnet @@ -359,8 +227,10 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, net_physnet_dict[net['id']] = net.get(pnet.PHYSICAL_NETWORK) for port in l3plugin._plugin.get_ports(context, filters={ 'device_owner': [n_const.DEVICE_OWNER_ROUTER_GW]}): - port_physnet_dict[port['id']] = net_physnet_dict.get( - port['network_id']) + if utils.is_ovn_provider_router( + l3plugin.get_router(context, port['device_id'])): + port_physnet_dict[port['id']] = net_physnet_dict.get( + port['network_id']) return port_physnet_dict def update_router_gateway_port_bindings(self, router, host): @@ -474,7 +344,9 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, 'device_owner': [n_const.DEVICE_OWNER_ROUTER_GW], 'fixed_ips': {'subnet_id': [orig['id']]}, }) - router_ids = {port['device_id'] for port in gw_ports} + router_ids = {port['device_id'] for port in gw_ports + if utils.is_ovn_provider_router( + l3plugin.get_router(context, port['device_id']))} remove = [{'destination': '0.0.0.0/0', 'nexthop': orig_gw_ip} ] if orig_gw_ip else [] add = [{'destination': '0.0.0.0/0', 'nexthop': current_gw_ip} @@ -508,11 +380,16 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, # https://bugs.launchpad.net/neutron/+bug/1948457 if (event == events.BEFORE_UPDATE and 'fixed_ips' in current and not current['fixed_ips'] and - utils.is_lsp_router_port(original)): + utils.is_lsp_router_port(original) and + utils.is_ovn_provider_router( + l3plugin.get_router(context, original['device_id']))): reason = _("Router port must have at least one IP.") raise n_exc.ServicePortInUse(port_id=original['id'], reason=reason) - if event == events.AFTER_UPDATE and utils.is_lsp_router_port(current): + if (event == events.AFTER_UPDATE and + utils.is_lsp_router_port(current) and + utils.is_ovn_provider_router( + l3plugin.get_router(context, current['device_id']))): # We call the update_router port with if_exists, because neutron, # internally creates the port, and then calls update, which will # trigger this callback even before we had the chance to create @@ -541,3 +418,8 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, if diff: raise az_exc.AvailabilityZoneNotFound( availability_zone=', '.join(diff)) + + @staticmethod + @resource_extend.extends([l3_apidef.ROUTERS]) + def add_flavor_id(router_res, router_db): + router_res['flavor_id'] = router_db['flavor_id'] diff --git a/neutron/services/ovn_l3/service_providers/__init__.py b/neutron/services/ovn_l3/service_providers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/ovn_l3/service_providers/driver_controller.py b/neutron/services/ovn_l3/service_providers/driver_controller.py new file mode 100644 index 00000000000..aec34d389ab --- /dev/null +++ b/neutron/services/ovn_l3/service_providers/driver_controller.py @@ -0,0 +1,65 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib import exceptions as lib_exc +from neutron_lib.plugins import constants as plugin_constants +from oslo_log import log + +from neutron.db import servicetype_db as st_db +from neutron.services.l3_router.service_providers import driver_controller +from neutron.services import provider_configuration + + +LOG = log.getLogger(__name__) + + +class DriverController(driver_controller.DriverController): + """Driver controller for the OVN L3 service plugin. + + This component is responsible for dispatching router requests to L3 + service providers and for performing the bookkeeping about which + driver is associated with a given router. + + This is not intended to be accessed by the drivers or the l3 plugin. + All of the methods are marked as private to reflect this. + """ + + def __init__(self, l3_plugin): + self.l3_plugin = l3_plugin + self._stm = st_db.ServiceTypeManager.get_instance() + self._stm.add_provider_configuration( + plugin_constants.L3, _OvnPlusProviderConfiguration()) + self._load_drivers() + + def _get_provider_for_create(self, context, router): + """Get provider based on flavor or default provider.""" + if not driver_controller.flavor_specified(router): + return self.drivers[self.default_provider] + return self._get_l3_driver_by_flavor(context, router['flavor_id']) + + +class _OvnPlusProviderConfiguration( + provider_configuration.ProviderConfiguration): + + def __init__(self): + # loads up the OVN provider automatically and sets it as default. + super(_OvnPlusProviderConfiguration, self).__init__( + svc_type=plugin_constants.L3) + path = 'neutron.services.ovn_l3.service_providers.ovn.OvnDriver' + try: + self.add_provider({'service_type': plugin_constants.L3, + 'name': 'ovn', 'driver': path, 'default': True}) + except lib_exc.Invalid: + LOG.debug("Could not add L3 provider ovn, it may have " + "already been explicitly defined.") diff --git a/neutron/services/ovn_l3/service_providers/ovn.py b/neutron/services/ovn_l3/service_providers/ovn.py new file mode 100644 index 00000000000..6154c2cb694 --- /dev/null +++ b/neutron/services/ovn_l3/service_providers/ovn.py @@ -0,0 +1,252 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import copy + +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib import constants +from neutron_lib.db import api as db_api +from oslo_log import log as logging +from oslo_utils import excutils + +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.db import ovn_revision_numbers_db as db_rev +from neutron.extensions import revisions +from neutron.objects import router as l3_obj +from neutron.services.l3_router.service_providers import base +from neutron.services.portforwarding import constants as pf_consts + + +LOG = logging.getLogger(__name__) + + +@registry.has_registry_receivers +class OvnDriver(base.L3ServiceProvider): + ha_support = base.MANDATORY + dvr_support = base.MANDATORY + + @registry.receives(resources.ROUTER, [events.PRECOMMIT_CREATE]) + def _process_router_create_precommit(self, resource, event, trigger, + payload): + context = payload.context + context.session.flush() + router_id = payload.resource_id + router_db = payload.metadata['router_db'] + router = payload.states[0] + if not utils.is_ovn_provider_router(router): + return + + # NOTE(ralonsoh): the "distributed" flag is a static configuration + # parameter that needs to be defined only during the router creation. + extra_attr = router_db['extra_attributes'] + extra_attr.distributed = ovn_conf.is_ovn_distributed_floating_ip() + + db_rev.create_initial_revision( + context, router_id, ovn_const.TYPE_ROUTERS, + std_attr_id=router_db.standard_attr.id) + + @registry.receives(resources.ROUTER, [events.AFTER_CREATE]) + def _process_router_create(self, resource, event, trigger, payload): + router = payload.states[0] + if not utils.is_ovn_provider_router(router): + return + context = payload.context + try: + self.l3plugin._ovn_client.create_router(context, router) + except Exception: + with excutils.save_and_reraise_exception(): + # Delete the logical router + LOG.exception('Unable to create lrouter %s', router['id']) + self.l3plugin.delete_router(context, router['id']) + + @registry.receives(resources.ROUTER, [events.AFTER_UPDATE]) + def _process_router_update(self, resource, event, trigger, payload): + router_id = payload.resource_id + original = payload.states[0] + updated = payload.states[1] + if not utils.is_ovn_provider_router(original): + # flavor_id attribute is not allowed in router PUTs, so we only + # need to check the original router + return + context = payload.context + try: + self.l3plugin._ovn_client.update_router(context, updated, original) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception('Unable to update lrouter %s', router_id) + revert_router = {'router': original} + self.l3plugin.update_router(context, router_id, revert_router) + + @registry.receives(resources.ROUTER, [events.AFTER_DELETE]) + def _process_router_delete(self, resource, event, trigger, payload): + router_id = payload.resource_id + router = payload.states[0] + if not utils.is_ovn_provider_router(router): + return + context = payload.context + try: + self.l3plugin._ovn_client.delete_router(context, router_id) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception('Unable to delete lrouter %s', router['id']) + self.l3plugin.create_router(context, {'router': router}) + + @registry.receives(resources.ROUTER_INTERFACE, [events.AFTER_CREATE]) + def _process_add_router_interface(self, resource, event, trigger, payload): + router = payload.states[0] + if not utils.is_ovn_provider_router(router): + return + context = payload.context + port = payload.metadata['port'] + subnets = payload.metadata['subnets'] + router_interface_info = self.l3plugin._make_router_interface_info( + router.id, port['tenant_id'], port['id'], port['network_id'], + subnets[-1]['id'], [subnet['id'] for subnet in subnets]) + try: + self.l3plugin._ovn_client.create_router_port(context, router.id, + router_interface_info) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception('Unable to add router interface to lrouter %s. ' + 'Interface info: %s', router['id'], + router_interface_info) + self.l3plugin.remove_router_interface(context, router.id, + router_interface_info) + + @registry.receives(resources.ROUTER_INTERFACE, [events.AFTER_DELETE]) + def _process_remove_router_interface(self, resource, event, trigger, + payload): + router = payload.states[0] + if not utils.is_ovn_provider_router(router): + return + context = payload.context + port = payload.metadata['port'] + subnet_ids = payload.metadata['subnet_ids'] + router_interface_info = self.l3plugin._make_router_interface_info( + router.id, port['tenant_id'], port['id'], port['network_id'], + subnet_ids[0], subnet_ids) + try: + self.l3plugin._ovn_client.delete_router_port(context, port['id'], + router_id=router.id, + subnet_ids=subnet_ids) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception('Unable to remove router interface from lrouter ' + '%s. Interface info: %s', router['id'], + router_interface_info) + self.l3plugin.add_router_interface( + context, router.id, payload.metadata['interface_info']) + + def _create_floatingip_initial_revision(self, context, floatingip_db): + if not floatingip_db.router_id: + return + # We get the router with elevated context because floating IPs may be + # created to be associated with a router created by a different + # project. Please see + # https://review.opendev.org/c/openstack/neutron/+/2727B09 + router = self.l3plugin.get_router(context.elevated(), + floatingip_db.router_id) + if not utils.is_ovn_provider_router(router): + return + db_rev.create_initial_revision( + context, floatingip_db.id, ovn_const.TYPE_FLOATINGIPS, + may_exist=True, std_attr_id=floatingip_db.standard_attr.id) + + @registry.receives(resources.FLOATING_IP, + [events.PRECOMMIT_CREATE, events.PRECOMMIT_UPDATE]) + def _process_floatingip_create_update_precommit(self, resource, event, + trigger, payload): + context = payload.context + floatingip_db = payload.desired_state + self._create_floatingip_initial_revision(context, floatingip_db) + + @registry.receives(pf_consts.PORT_FORWARDING, [events.AFTER_CREATE]) + def _process_portforwarding_create(self, resource, event, trigger, + payload): + context = payload.context + pf_obj = payload.states[0] + with db_api.CONTEXT_WRITER.using(context): + fip_db = l3_obj.FloatingIP.get_object( + context, id=pf_obj.floatingip_id).db_obj + self._create_floatingip_initial_revision(context, fip_db) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_CREATE]) + def _process_floatingip_create(self, resource, event, trigger, payload): + revision_row = db_rev.get_revision_row(payload.context, + payload.resource_id) + if not revision_row: + return + # The floating ip dictionary that is sent by the L3 DB plugin in the + # notification doesn't include the revision number yet. We add it here + # to the dictionary that is passed to the ovn client + floatingip = copy.deepcopy(payload.states[0]) + floatingip[revisions.REVISION] = revision_row.revision_number + qos_policy_id = payload.request_body['floatingip'].get('qos_policy_id') + if qos_policy_id and 'qos_policy_id' not in floatingip: + floatingip['qos_policy_id'] = qos_policy_id + self.l3plugin._ovn_client.create_floatingip(payload.context, + floatingip) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_UPDATE]) + def _process_floatingip_update(self, resource, event, trigger, payload): + if not db_rev.get_revision_row(payload.context, payload.resource_id): + return + fip = payload.states[1] + old_fip = payload.states[0] + fip_request = payload.request_body + if fip_request: + self.l3plugin._ovn_client.update_floatingip(payload.context, fip, + fip_request) + else: + router_id = old_fip.get('router_id') + fixed_ip_address = old_fip.get('fixed_ip_address') + if router_id and fixed_ip_address: + update_fip = { + 'id': old_fip['id'], + 'logical_ip': fixed_ip_address, + 'external_ip': old_fip['floating_ip_address'], + 'floating_network_id': old_fip['floating_network_id'] + } + try: + self.l3plugin._ovn_client.disassociate_floatingip( + update_fip, router_id) + self.l3plugin.update_floatingip_status( + payload.context, old_fip['id'], + constants.FLOATINGIP_STATUS_DOWN) + except Exception as e: + LOG.error('Error in disassociating floatingip %(id)s: ' + '%(error)s', {'id': old_fip['id'], 'error': e}) + if not fip['router_id']: + db_rev.delete_revision(payload.context, payload.resource_id, + ovn_const.TYPE_FLOATINGIPS) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_DELETE]) + def _process_floatingip_delete(self, resource, event, trigger, payload): + if not db_rev.get_revision_row(payload.context, payload.resource_id): + return + self.l3plugin._ovn_client.delete_floatingip(payload.context, + payload.resource_id) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_STATUS_UPDATE]) + def _process_floatingip_status_update(self, resource, event, trigger, + payload): + if not db_rev.get_revision_row(payload.context, payload.resource_id): + return + self.l3plugin._ovn_client.update_floatingip_status(payload.context, + payload.states[0]) diff --git a/neutron/services/ovn_l3/service_providers/user_defined.py b/neutron/services/ovn_l3/service_providers/user_defined.py new file mode 100644 index 00000000000..ea1af67e3a7 --- /dev/null +++ b/neutron/services/ovn_l3/service_providers/user_defined.py @@ -0,0 +1,170 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib.plugins import constants as plugin_constants +from neutron_lib.plugins import directory +from oslo_log import log as logging + +from neutron.services.l3_router.service_providers import base + + +LOG = logging.getLogger(__name__) + + +@registry.has_registry_receivers +class UserDefined(base.L3ServiceProvider): + + def __init__(self, l3_plugin): + super(UserDefined, self).__init__(l3_plugin) + self._user_defined_provider = __name__ + "." + self.__class__.__name__ + + @property + def _flavor_plugin(self): + try: + return self._flavor_plugin_ref + except AttributeError: + self._flavor_plugin_ref = directory.get_plugin( + plugin_constants.FLAVORS) + return self._flavor_plugin_ref + + def _is_user_defined_provider(self, context, router): + flavor_id = router.get('flavor_id') + if flavor_id is None: + return False + flavor = self._flavor_plugin.get_flavor(context, flavor_id) + provider = self._flavor_plugin.get_flavor_next_provider( + context, flavor['id'])[0] + return str(provider['driver']) == self._user_defined_provider + + @registry.receives(resources.ROUTER_CONTROLLER, + [events.PRECOMMIT_ADD_ASSOCIATION]) + def _process_router_add_association(self, resource, event, trigger, + payload=None): + router = payload.states[0] + context = payload.context + if not self._is_user_defined_provider(context, router): + return + LOG.debug('Got request to associate user defined flavor to router %s', + router) + + @registry.receives(resources.ROUTER, [events.AFTER_CREATE]) + def _process_router_create(self, resource, event, trigger, payload=None): + router = payload.states[0] + context = payload.context + if not self._is_user_defined_provider(context, router): + return + LOG.debug('Got request to create a user defined flavor router %s', + router) + + @registry.receives(resources.ROUTER, [events.AFTER_UPDATE]) + def _process_router_update(self, resource, event, trigger, payload=None): + original = payload.states[0] + updated = payload.states[1] + context = payload.context + if not self._is_user_defined_provider(context, original): + # flavor_id attribute is not allowed in router PUTs, so we only + # need to check the original router + return + router_id = payload.resource_id + LOG.debug('Got request to update a user defined flavor router with id ' + '%s. Original: %s. Updated: %s', router_id, original, + updated) + + @registry.receives(resources.ROUTER, [events.AFTER_DELETE]) + def _process_router_delete(self, resource, event, trigger, payload=None): + router = payload.states[0] + context = payload.context + if not self._is_user_defined_provider(context, router): + return + router_id = payload.resource_id + LOG.debug('Got request to delete a user defined flavor router with ', + 'id %s:', router_id) + + @registry.receives(resources.ROUTER_INTERFACE, [events.AFTER_CREATE]) + def _process_add_router_interface(self, resource, event, trigger, payload): + router = payload.states[0] + context = payload.context + if not self._is_user_defined_provider(context, router): + return + port = payload.metadata['port'] + subnets = payload.metadata['subnets'] + router_interface_info = self.l3plugin._make_router_interface_info( + router.id, port['tenant_id'], port['id'], port['network_id'], + subnets[-1]['id'], [subnet['id'] for subnet in subnets]) + LOG.debug('Got request to add interface %s to a user defined flavor ' + 'router with id %s', router_interface_info, router.id) + + @registry.receives(resources.ROUTER_INTERFACE, [events.AFTER_DELETE]) + def _process_remove_router_interface(self, resource, event, trigger, + payload): + router = payload.states[0] + context = payload.context + if not self._is_user_defined_provider(context, router): + return + subnet_ids = payload.metadata['subnet_ids'] + LOG.debug('Got request to remove interface to subnets %s from a user ' + 'defined flavor router with id %s', subnet_ids, router.id) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_CREATE]) + def _process_floatingip_create(self, resource, event, trigger, payload): + context = payload.context + fip = payload.states[0] + if not fip['router_id']: + return + router = self.l3plugin.get_router(context, fip['router_id']) + if not self._is_user_defined_provider(context, router): + return + LOG.debug('Got request to create a floating ip associated to a router ' + 'of user defined flavor %s', fip) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_UPDATE]) + def _process_floatingip_update(self, resource, event, trigger, payload): + context = payload.context + fip = payload.states[1] + if not fip['router_id']: + return + router = self.l3plugin.get_router(context, fip['router_id']) + if not self._is_user_defined_provider(context, router): + return + LOG.debug('Got request to update a floating ip associated to a router ' + 'of user defined flavor %s', fip) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_DELETE]) + def _process_floatingip_delete(self, resource, event, trigger, payload): + context = payload.context + fip = payload.states[0] + if not fip['router_id']: + return + router = self.l3plugin.get_router(context, fip['router_id']) + if not self._is_user_defined_provider(context, router): + return + LOG.debug('Got request to delete a floating ip associated to a router ' + 'of user defined flavor %s', fip) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_STATUS_UPDATE]) + def _process_floatingip_status_update(self, resource, event, trigger, + payload): + context = payload.context + fip = payload.states[0] + if not fip['router_id']: + return + router = self.l3plugin.get_router(context, fip['router_id']) + if not self._is_user_defined_provider(context, router): + return + LOG.debug('Got request to update the status of a floating ip ' + 'associated to a router of user defined flavor %s', fip) diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py index 05ec88c0901..6b961f60dcf 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py @@ -21,6 +21,8 @@ from futurist import periodics from neutron_lib.api.definitions import external_net as extnet_apidef from neutron_lib.api.definitions import floating_ip_port_forwarding as pf_def from neutron_lib.api.definitions import provider_net as provnet_apidef +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry from neutron_lib import constants as n_const from neutron_lib import context as n_context from neutron_lib.exceptions import l3 as lib_l3_exc @@ -30,6 +32,7 @@ from neutron.common.ovn import utils from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as ovn_config from neutron.db import ovn_revision_numbers_db as db_rev from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import maintenance +from neutron.services.portforwarding import constants as pf_consts from neutron.tests.functional import base from neutron.tests.functional.services.logapi.drivers.ovn \ import test_driver as test_log_driver @@ -925,65 +928,69 @@ class TestMaintenance(_TestMaintenanceHelper): p1 = self._create_port('testp1', net1['id']) p1_ip = p1['fixed_ips'][0]['ip_address'] - with mock.patch('neutron_lib.callbacks.registry.publish') as m_publish: - # > Create - fip_pf_args = { - pf_def.EXTERNAL_PORT: 2222, - pf_def.INTERNAL_PORT: 22, - pf_def.INTERNAL_PORT_ID: p1['id'], - pf_def.PROTOCOL: 'tcp', - pf_def.INTERNAL_IP_ADDRESS: p1_ip} - pf_obj = self.pf_plugin.create_floatingip_port_forwarding( - self.context, fip_id, **fip_attrs(fip_pf_args)) - call = mock.call('port_forwarding', 'after_create', self.pf_plugin, - payload=mock.ANY) - m_publish.assert_has_calls([call]) + callbacks = registry._get_callback_manager()._callbacks + pf_cb = callbacks[pf_consts.PORT_FORWARDING] + key = list(pf_cb[events.AFTER_UPDATE][0][1].keys())[0] + pf_cb[events.AFTER_CREATE][0][1][key] = mock.MagicMock() - # Assert load balancer for port forwarding was not created - self.assertFalse(self._find_pf_lb(router_id, fip_id)) + # > Create + fip_pf_args = { + pf_def.EXTERNAL_PORT: 2222, + pf_def.INTERNAL_PORT: 22, + pf_def.INTERNAL_PORT_ID: p1['id'], + pf_def.PROTOCOL: 'tcp', + pf_def.INTERNAL_IP_ADDRESS: p1_ip} + pf_obj = self.pf_plugin.create_floatingip_port_forwarding( + self.context, fip_id, **fip_attrs(fip_pf_args)) + call = mock.call('port_forwarding', 'after_create', self.pf_plugin, + payload=mock.ANY) + pf_cb[events.AFTER_CREATE][0][1][key].assert_has_calls([call]) - # Call the maintenance thread to fix the problem - self.maint.check_for_inconsistencies() + # Assert load balancer for port forwarding was not created + self.assertFalse(self._find_pf_lb(router_id, fip_id)) - # Assert load balancer for port forwarding was created - _verify_lb(self, 'tcp', 2222, 22) + # Call the maintenance thread to fix the problem + self.maint.check_for_inconsistencies() - # > Update - fip_pf_args = {pf_def.EXTERNAL_PORT: 5353, - pf_def.INTERNAL_PORT: 53, - pf_def.PROTOCOL: 'udp'} - m_publish.reset_mock() - self.pf_plugin.update_floatingip_port_forwarding( - self.context, pf_obj['id'], fip_id, **fip_attrs(fip_pf_args)) - call = mock.call('port_forwarding', 'after_update', self.pf_plugin, - payload=mock.ANY) - m_publish.assert_has_calls([call]) + # Assert load balancer for port forwarding was created + _verify_lb(self, 'tcp', 2222, 22) - # Assert load balancer for port forwarding is stale - _verify_lb(self, 'tcp', 2222, 22) + # > Update + fip_pf_args = {pf_def.EXTERNAL_PORT: 5353, + pf_def.INTERNAL_PORT: 53, + pf_def.PROTOCOL: 'udp'} + pf_cb[events.AFTER_UPDATE][0][1][key] = mock.MagicMock() + self.pf_plugin.update_floatingip_port_forwarding( + self.context, pf_obj['id'], fip_id, **fip_attrs(fip_pf_args)) + call = mock.call('port_forwarding', 'after_update', self.pf_plugin, + payload=mock.ANY) + pf_cb[events.AFTER_UPDATE][0][1][key].assert_has_calls([call]) - # Call the maintenance thread to fix the problem - self.maint.check_for_inconsistencies() + # Assert load balancer for port forwarding is stale + _verify_lb(self, 'tcp', 2222, 22) - # Assert load balancer for port forwarding was updated - _verify_lb(self, 'udp', 5353, 53) + # Call the maintenance thread to fix the problem + self.maint.check_for_inconsistencies() - # > Delete - m_publish.reset_mock() - self.pf_plugin.delete_floatingip_port_forwarding( - self.context, pf_obj['id'], fip_id) - call = mock.call('port_forwarding', 'after_delete', self.pf_plugin, - payload=mock.ANY) - m_publish.assert_has_calls([call]) + # Assert load balancer for port forwarding was updated + _verify_lb(self, 'udp', 5353, 53) - # Assert load balancer for port forwarding is stale - _verify_lb(self, 'udp', 5353, 53) + # > Delete + pf_cb[events.AFTER_DELETE][0][1][key] = mock.MagicMock() + self.pf_plugin.delete_floatingip_port_forwarding( + self.context, pf_obj['id'], fip_id) + call = mock.call('port_forwarding', 'after_delete', self.pf_plugin, + payload=mock.ANY) + pf_cb[events.AFTER_DELETE][0][1][key].assert_has_calls([call]) - # Call the maintenance thread to fix the problem - self.maint.check_for_inconsistencies() + # Assert load balancer for port forwarding is stale + _verify_lb(self, 'udp', 5353, 53) - # Assert load balancer for port forwarding is gone - self.assertFalse(self._find_pf_lb(router_id, fip_id)) + # Call the maintenance thread to fix the problem + self.maint.check_for_inconsistencies() + + # Assert load balancer for port forwarding is gone + self.assertFalse(self._find_pf_lb(router_id, fip_id)) def test_check_for_ha_chassis_group(self): net1 = self._create_network('network1test', external=False) diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py index cf932b27163..5623c63ee01 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py @@ -620,7 +620,8 @@ class TestExternalPorts(base.TestOVNFunctionalBase): 'ports', port_upt_data, port['id'], self.fmt) port_res = port_req.get_response(self.api) - def test_add_external_port_avoid_flapping(self): + @mock.patch('neutron.objects.router.Router.get_object') + def test_add_external_port_avoid_flapping(self, gr): class LogicalSwitchPortUpdateUpEventTest(event.RowEvent): def __init__(self): self.count = 0 @@ -651,6 +652,7 @@ class TestExternalPorts(base.TestOVNFunctionalBase): def get_count(self): return self.count + gr.return_value = {'flavor_id': None} og_up_event = ovsdb_monitor.LogicalSwitchPortUpdateUpEvent(None) og_down_event = ovsdb_monitor.LogicalSwitchPortUpdateDownEvent(None) test_down_event = LogicalSwitchPortUpdateDownEventTest() @@ -666,7 +668,8 @@ class TestExternalPorts(base.TestOVNFunctionalBase): # status as up, triggering only a LogicalSwitchPortUpdateUpEvent. self._create_router_port(portbindings.VNIC_DIRECT) self.assertEqual(test_down_event.get_count(), 0) - self.assertEqual(test_up_event.get_count(), 1) + n_utils.wait_until_true(lambda: test_up_event.get_count() == 1, + timeout=10) def test_external_port_create_vnic_direct(self): self._test_external_port_create(portbindings.VNIC_DIRECT) diff --git a/neutron/tests/unit/db/test_db_base_plugin_v2.py b/neutron/tests/unit/db/test_db_base_plugin_v2.py index 1efca99ef4b..da5187f06be 100644 --- a/neutron/tests/unit/db/test_db_base_plugin_v2.py +++ b/neutron/tests/unit/db/test_db_base_plugin_v2.py @@ -2224,7 +2224,9 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s self.assertEqual(webob.exc.HTTPClientError.code, res.status_int) - def test_requested_fixed_ip_address_v6_slaac_router_iface(self): + @mock.patch('neutron.objects.router.Router.get_object') + def test_requested_fixed_ip_address_v6_slaac_router_iface(self, gr): + gr.return_value = {'flavor_id': ''} with self.subnet(gateway_ip='fe80::1', cidr='fe80::/64', ip_version=constants.IP_VERSION_6, @@ -2284,7 +2286,9 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s self.assertIn({'ip_address': eui_addr, 'subnet_id': subnet2['subnet']['id']}, ips) - def test_create_router_port_ipv4_and_ipv6_slaac_no_fixed_ips(self): + @mock.patch('neutron.objects.router.Router.get_object') + def test_create_router_port_ipv4_and_ipv6_slaac_no_fixed_ips(self, gr): + gr.return_value = {'flavor_id': ''} with self.network() as network: # Create an IPv4 and an IPv6 SLAAC subnet on the network with self.subnet(network) as subnet_v4,\ @@ -3764,8 +3768,10 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): sport = self.deserialize(self.fmt, req.get_response(self.api)) self.assertEqual(0, len(sport['port']['fixed_ips'])) - def test_delete_subnet_ipv6_slaac_router_port_exists(self): + @mock.patch('neutron.objects.router.Router.get_object') + def test_delete_subnet_ipv6_slaac_router_port_exists(self, gr): """Test IPv6 SLAAC subnet delete with a router port using the subnet""" + gr.return_value = {'flavor_id': ''} subnet, port = self._create_slaac_subnet_and_port( constants.DEVICE_OWNER_ROUTER_INTF) # Delete the subnet and assert that we get a HTTP 409 error @@ -4502,7 +4508,9 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): ipv6_ra_mode=constants.IPV6_SLAAC, ipv6_address_mode=constants.IPV6_SLAAC) - def test_create_subnet_ipv6_first_ip_owned_by_router(self): + @mock.patch('neutron.objects.router.Router.get_object') + def test_create_subnet_ipv6_first_ip_owned_by_router(self, gr): + gr.return_value = {'flavor_id': ''} cidr = '2001::/64' with self.network() as network: net_id = network['network']['id'] @@ -4598,10 +4606,12 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): self.assertEqual(webob.exc.HTTPClientError.code, ctx_manager.exception.code) + @mock.patch('neutron.objects.router.Router.get_object') def _test_create_subnet_ipv6_auto_addr_with_port_on_network( - self, addr_mode, device_owner=DEVICE_OWNER_COMPUTE, + self, addr_mode, gr, device_owner=DEVICE_OWNER_COMPUTE, insert_db_reference_error=False, insert_port_not_found=False, insert_address_allocated=False): + gr.return_value = {'flavor_id': ''} # Create a network with one IPv4 subnet and one port with self.network() as network,\ self.subnet(network=network) as v4_subnet,\ diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py index 9ec5d336fdc..cd42cd0a964 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py @@ -2495,9 +2495,12 @@ class TestOVNMechanismDriver(TestOVNMechanismDriverBase): def test__update_dnat_entry_if_needed_down_no_dvr(self): self._test__update_dnat_entry_if_needed(up=False, dvr=False) + @mock.patch('neutron.objects.router.Router.get_object') @mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.' 'ovn_client.OVNClient._get_router_ports') - def _test_update_network_fragmentation(self, new_mtu, expected_opts, grps): + def _test_update_network_fragmentation(self, new_mtu, expected_opts, grps, + gr): + gr.return_value = {'flavor_id': ''} network_attrs = {external_net.EXTERNAL: True} network = self._make_network( self.fmt, 'net1', True, as_admin=True, diff --git a/neutron/tests/unit/plugins/ml2/test_plugin.py b/neutron/tests/unit/plugins/ml2/test_plugin.py index 25eb2249c5e..ab2c3b23095 100644 --- a/neutron/tests/unit/plugins/ml2/test_plugin.py +++ b/neutron/tests/unit/plugins/ml2/test_plugin.py @@ -1344,15 +1344,19 @@ class TestMl2PortsV2(test_plugin.TestPortsV2, Ml2PluginV2TestCase): with self.port(device_owner="fake:test"): self.assertTrue(cp.called) + @mock.patch('neutron.objects.router.Router.get_object') def _test_no_dhcp_provisioning_blocks_removed_empty_device_owner( - self, device_owner): + self, device_owner, gr): + gr.return_value = {'flavor_id': ''} with mock.patch.object(provisioning_blocks, 'remove_provisioning_component') as cp: with self.port(device_owner=device_owner): self.assertFalse(cp.called) + @mock.patch('neutron.objects.router.Router.get_object') def _test_no_dhcp_provisioning_blocks_added_empty_device_owner( - self, device_owner): + self, device_owner, gr): + gr.return_value = {'flavor_id': ''} with mock.patch.object(provisioning_blocks, 'add_provisioning_component') as cp: with self.port(device_owner=device_owner): diff --git a/neutron/tests/unit/services/ovn_l3/service_providers/__init__.py b/neutron/tests/unit/services/ovn_l3/service_providers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/services/ovn_l3/service_providers/test_user_defined.py b/neutron/tests/unit/services/ovn_l3/service_providers/test_user_defined.py new file mode 100644 index 00000000000..ba02b957ec6 --- /dev/null +++ b/neutron/tests/unit/services/ovn_l3/service_providers/test_user_defined.py @@ -0,0 +1,124 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from unittest import mock + +from neutron_lib.callbacks import events + + +from neutron.db.models import l3 +from neutron.services.ovn_l3.service_providers import user_defined +from neutron.tests.unit import testlib_api + + +DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2' + + +class TestUserDefined(testlib_api.SqlTestCase): + + def setUp(self): + super(TestUserDefined, self).setUp() + self.setup_coreplugin(DB_PLUGIN_KLASS) + self.fake_l3 = mock.MagicMock() + self.fake_l3._make_router_interface_info = mock.MagicMock( + return_value='router_interface_info') + self.provider = user_defined.UserDefined(self.fake_l3) + self.context = 'fake-context' + self.router = l3.Router(id='fake-uuid', + flavor_id='fake-uuid') + self.fake_l3.get_router = mock.MagicMock(return_value=self.router) + self.fip = {'router_id': 'fake-uuid'} + mock_flavor_plugin = mock.MagicMock() + mock_flavor_plugin.get_flavor = mock.MagicMock( + return_value={'id': 'fake-uuid'}) + mock_flavor_plugin.get_flavor_next_provider = mock.MagicMock( + return_value=[{'driver': self.provider._user_defined_provider}]) + self.provider._flavor_plugin_ref = mock_flavor_plugin + + def test__is_user_defined_provider(self): + # test the positive case + self.assertTrue(self.provider._is_user_defined_provider( + self.context, self.router)) + + # test the negative case + self.provider._flavor_plugin_ref.get_flavor_next_provider = ( + mock.MagicMock(return_value=[{'driver': None}])) + self.assertFalse(self.provider._is_user_defined_provider( + self.context, self.router)) + + def test_router_processing(self): + with mock.patch.object(user_defined.LOG, 'debug') as log: + payload = events.DBEventPayload( + self.context, + states=(self.router, self.router), + resource_id=self.router['id'], + metadata={'subnet_ids': ['subnet-id']}) + fl_plg = self.provider._flavor_plugin_ref + methods = [self.provider._process_router_add_association, + self.provider._process_router_create, + self.provider._process_router_update, + self.provider._process_router_delete, + self.provider._process_remove_router_interface] + for method in methods: + method('resource', 'event', self, payload) + fl_plg.get_flavor.assert_called_once() + fl_plg.get_flavor_next_provider.assert_called_once() + log.assert_called_once() + fl_plg.get_flavor.reset_mock() + fl_plg.get_flavor_next_provider.reset_mock() + log.reset_mock() + + def test_add_router_interface(self): + with mock.patch.object(user_defined.LOG, 'debug') as log: + payload = events.DBEventPayload( + self.context, + states=(self.router, self.router), + resource_id=self.router['id'], + metadata={'subnet_ids': ['subnet-id'], + 'port': {'tenant_id': 'tenant-id', + 'id': 'id', + 'network_id': 'network-id'}, + 'subnets': [{'id': 'id'}]}) + fl_plg = self.provider._flavor_plugin_ref + l3_plg = self.fake_l3 + self.provider._process_add_router_interface('resource', + 'event', + self, + payload) + l3_plg._make_router_interface_info.assert_called_once() + fl_plg.get_flavor.assert_called_once() + fl_plg.get_flavor_next_provider.assert_called_once() + log.assert_called_once() + + def test_floatingip_processing(self): + # Test all the methods related to FIP processing + with mock.patch.object(user_defined.LOG, 'debug') as log: + payload = events.DBEventPayload( + self.context, + states=(self.fip, self.fip)) + fl_plg = self.provider._flavor_plugin_ref + l3_plg = self.fake_l3 + methods = [self.provider._process_floatingip_create, + self.provider._process_floatingip_update, + self.provider._process_floatingip_delete, + self.provider._process_floatingip_status_update] + for method in methods: + method('resource', 'event', self, payload) + l3_plg.get_router.assert_called_once() + fl_plg.get_flavor.assert_called_once() + fl_plg.get_flavor_next_provider.assert_called_once() + log.assert_called_once() + l3_plg.get_router.reset_mock() + fl_plg.get_flavor.reset_mock() + fl_plg.get_flavor_next_provider.reset_mock() + log.reset_mock() diff --git a/neutron/tests/unit/services/ovn_l3/test_plugin.py b/neutron/tests/unit/services/ovn_l3/test_plugin.py index 6827c54c110..4b0be37a4ee 100644 --- a/neutron/tests/unit/services/ovn_l3/test_plugin.py +++ b/neutron/tests/unit/services/ovn_l3/test_plugin.py @@ -37,6 +37,8 @@ from neutron.common.ovn import utils from neutron.conf.plugins.ml2.drivers import driver_type as driver_type_conf from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as config from neutron.db import extraroute_db +from neutron.db.models import l3 as l3_models +from neutron.db.models import ovn as ovn_models from neutron import manager as neutron_manager from neutron.plugins.ml2 import managers from neutron.services.ovn_l3 import exceptions as ovn_l3_exc @@ -78,6 +80,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): attrs=network_attrs).info() self.fake_router_port = {'device_id': '', 'network_id': self.fake_network['id'], + 'tenant_id': 'tenant-id', 'device_owner': 'network:router_interface', 'mac_address': 'aa:aa:aa:aa:aa:aa', 'status': constants.PORT_STATUS_ACTIVE, @@ -103,6 +106,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.fake_router = {'id': 'router-id', 'name': 'router', 'admin_state_up': False, + 'flavor_id': None, 'routes': [{'destination': '1.1.1.0/24', 'nexthop': '10.0.0.2'}]} self.fake_router_interface_info = { @@ -122,6 +126,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'id': 'router-id', 'name': 'router', 'admin_state_up': True, + 'flavor_id': None, 'external_gateway_info': self.fake_external_fixed_ips, 'gw_port_id': 'gw-port-id' } @@ -179,6 +184,8 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.fake_floating_ip['router_id'])}, })) self.l3_inst = directory.get_plugin(plugin_constants.L3) + ovn_provider = self.l3_inst.l3_driver_controller.default_provider + self.ovn_drv = self.l3_inst.l3_driver_controller.drivers[ovn_provider] self.lb_id = uuidutils.generate_uuid() self.member_subnet = {'id': 'subnet-id', 'ip_version': 4, @@ -292,6 +299,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.get_rev_p = self._start_mock( 'neutron.common.ovn.utils.get_revision_number', return_value=1) + self.get_rev_row_p = self._start_mock( + 'neutron.db.ovn_revision_numbers_db.get_revision_row', + return_value=ovn_models.OVNRevisionNumbers(revision_number=1)) self.admin_context = mock.Mock() self._start_mock( 'neutron_lib.context.get_admin_context', @@ -326,13 +336,26 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): driver, None) self.assertEqual(fake_mech_driver.obj, result) + def _create_payload_for_router_interface(self, router_id, + pass_subnet=True): + router_obj = l3_models.Router(id=router_id, flavor_id=None) + metadata = {'port': self.fake_router_port} + if pass_subnet: + metadata['subnets'] = [self.fake_subnet] + else: + metadata['subnet_ids'] = [self.fake_subnet['id']] + return events.DBEventPayload(self.context, metadata=metadata, + states=(router_obj,), + resource_id=router_id) + @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface') def test_add_router_interface(self, func): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id'} func.return_value = self.fake_router_interface_info - self.l3_inst.add_router_interface(self.context, router_id, - interface_info) + payload = self._create_payload_for_router_interface(router_id) + self.ovn_drv._process_add_router_interface(resources.ROUTER_INTERFACE, + events.AFTER_CREATE, + self, payload) self.l3_inst._nb_ovn.add_lrouter_port.assert_called_once_with( **self.fake_router_port_assert) self.l3_inst._nb_ovn.set_lrouter_port_in_lswitch_port.\ @@ -346,7 +369,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface') def test_add_router_interface_update_lrouter_port(self, func): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id'} func.return_value = {'id': router_id, 'port_id': 'router-port-id', 'subnet_id': 'subnet-id1', @@ -366,8 +388,10 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'mac_address': 'aa:aa:aa:aa:aa:aa', 'network_id': 'network-id1'} fake_rtr_intf_networks = ['2001:db8::1/24', '2001:dba::1/24'] - self.l3_inst.add_router_interface(self.context, router_id, - interface_info) + payload = self._create_payload_for_router_interface(router_id) + self.ovn_drv._process_add_router_interface(resources.ROUTER_INTERFACE, + events.AFTER_CREATE, + self, payload) called_args_dict = ( self.l3_inst._nb_ovn.update_lrouter_port.call_args_list[0][1]) @@ -382,12 +406,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): def test_remove_router_interface(self): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id'} self.get_port.side_effect = n_exc.PortNotFound( port_id='router-port-id') - self.l3_inst.remove_router_interface( - self.context, router_id, interface_info) + payload = self._create_payload_for_router_interface(router_id, + pass_subnet=False) + self.ovn_drv._process_remove_router_interface( + resources.ROUTER_INTERFACE, events.AFTER_DELETE, self, payload) self.l3_inst._nb_ovn.lrp_del.assert_called_once_with( 'lrp-router-port-id', 'neutron-router-id', if_exists=True) @@ -396,9 +421,10 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): def test_remove_router_interface_update_lrouter_port(self): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id'} - self.l3_inst.remove_router_interface( - self.context, router_id, interface_info) + payload = self._create_payload_for_router_interface(router_id, + pass_subnet=False) + self.ovn_drv._process_remove_router_interface( + resources.ROUTER_INTERFACE, events.AFTER_DELETE, self, payload) self.l3_inst._nb_ovn.update_lrouter_port.assert_called_once_with( if_exists=False, name='lrp-router-port-id', @@ -413,14 +439,15 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): def test_remove_router_interface_router_not_found(self): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id'} self.get_port.side_effect = n_exc.PortNotFound( port_id='router-port-id') self.get_router.side_effect = l3_exc.RouterNotFound( router_id='router-id') - self.l3_inst.remove_router_interface( - self.context, router_id, interface_info) + payload = self._create_payload_for_router_interface(router_id, + pass_subnet=False) + self.ovn_drv._process_remove_router_interface( + resources.ROUTER_INTERFACE, events.AFTER_DELETE, self, payload) self.get_router.assert_called_once_with(self.context, 'router-id') self.l3_inst._nb_ovn.lrp_del.assert_called_once_with( @@ -433,31 +460,40 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): @mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb' '.ovn_client.OVNClient._get_v4_network_of_all_router_ports') def test_update_router_admin_state_change(self, get_rps, func): - router_id = 'router-id' new_router = self.fake_router.copy() updated_data = {'admin_state_up': True} new_router.update(updated_data) func.return_value = new_router - self.l3_inst.update_router(self.context, router_id, - {'router': updated_data}) + payload = self._create_payload_for_router_update(self.fake_router, + new_router) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) self.l3_inst._nb_ovn.update_lrouter.assert_called_once_with( 'neutron-router-id', enabled=True, external_ids={ ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router', ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: ''}) + def _create_payload_for_router_update(self, original, updated): + return events.DBEventPayload(self.context, + states=(original, updated), + resource_id=original['id']) + @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.' 'ovn_client.OVNClient._get_v4_network_of_all_router_ports') def test_update_router_name_change(self, get_rps, func): - router_id = 'router-id' new_router = self.fake_router.copy() updated_data = {'name': 'test'} new_router.update(updated_data) func.return_value = new_router - self.l3_inst.update_router(self.context, router_id, - {'router': updated_data}) + payload = self._create_payload_for_router_update(self.fake_router, + new_router) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) self.l3_inst._nb_ovn.update_lrouter.assert_called_once_with( 'neutron-router-id', enabled=False, external_ids={ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'test', @@ -492,7 +528,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'ovn_client.OVNClient._get_v4_network_of_all_router_ports') def test_update_router_static_route_change(self, get_rps, func, mock_routes): - router_id = 'router-id' get_rps.return_value = [{'device_id': '', 'device_owner': 'network:router_interface', 'mac_address': 'aa:aa:aa:aa:aa:aa', @@ -506,8 +541,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'nexthop': '10.0.0.3'}]} new_router.update(updated_data) func.return_value = new_router - self.l3_inst.update_router(self.context, router_id, - {'router': updated_data}) + payload = self._create_payload_for_router_update(self.fake_router, + new_router) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) self.l3_inst._nb_ovn.add_static_route.assert_called_once_with( 'neutron-router-id', ip_prefix='2.2.2.0/24', nexthop='10.0.0.3') @@ -522,7 +560,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'ovn_client.OVNClient._get_v4_network_of_all_router_ports') def test_update_router_static_route_clear(self, get_rps, func, mock_routes): - router_id = 'router-id' get_rps.return_value = [{'device_id': '', 'device_owner': 'network:router_interface', 'mac_address': 'aa:aa:aa:aa:aa:aa', @@ -535,8 +572,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): updated_data = {'routes': []} new_router.update(updated_data) func.return_value = new_router - self.l3_inst.update_router(self.context, router_id, - {'router': updated_data}) + payload = self._create_payload_for_router_update(self.fake_router, + new_router) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) self.l3_inst._nb_ovn.add_static_route.assert_not_called() self.l3_inst._nb_ovn.delete_static_route.assert_called_once_with( 'neutron-router-id', @@ -546,12 +586,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'ovn_client.OVNClient._get_v4_network_of_all_router_ports') def test_create_router_with_ext_gw(self, get_rps): self.l3_inst._nb_ovn.is_col_present.return_value = True - router = {'router': {'name': 'router'}} self.get_subnet.return_value = self.fake_ext_subnet self.get_port.return_value = self.fake_ext_gw_port get_rps.return_value = self.fake_ext_subnet['cidr'] - self.l3_inst.create_router(self.context, router) + payload = events.DBEventPayload(self.context, + states=(self.fake_router_with_ext_gw,)) + self.ovn_drv._process_router_create(resources.ROUTER, + events.AFTER_CREATE, self, payload) external_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', @@ -596,7 +638,12 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): fake_resources.FakeOVNRouter.from_neutron_router( self.fake_router_with_ext_gw)) - self.l3_inst.delete_router(self.context, 'router-id') + payload = events.DBEventPayload( + self.context, states=(self.fake_router_with_ext_gw,), + resource_id=self.fake_router_with_ext_gw['id']) + self.ovn_drv._process_router_delete(resources.ROUTER, + events.AFTER_DELETE, + self, payload) self.l3_inst._nb_ovn.delete_lrouter.assert_called_once_with( 'neutron-router-id') @@ -606,12 +653,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface') def test_add_router_interface_with_gateway_set(self, ari, grps): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id'} ari.return_value = self.fake_router_interface_info self.get_router.return_value = self.fake_router_with_ext_gw - self.l3_inst.add_router_interface(self.context, router_id, - interface_info) + payload = self._create_payload_for_router_interface(router_id) + self.ovn_drv._process_add_router_interface(resources.ROUTER_INTERFACE, + events.AFTER_CREATE, + self, payload) self.l3_inst._nb_ovn.add_lrouter_port.assert_called_once_with( **self.fake_router_port_assert) @@ -632,14 +680,15 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): def test_add_router_interface_with_gateway_set_and_snat_disabled( self, ari, grps): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id'} ari.return_value = self.fake_router_interface_info get_router = self.fake_router_with_ext_gw get_router['external_gateway_info']['enable_snat'] = False self.get_router.return_value = get_router - self.l3_inst.add_router_interface(self.context, router_id, - interface_info) + payload = self._create_payload_for_router_interface(router_id) + self.ovn_drv._process_add_router_interface(resources.ROUTER_INTERFACE, + events.AFTER_CREATE, + self, payload) self.l3_inst._nb_ovn.add_lrouter_port.assert_called_once_with( **self.fake_router_port_assert) @@ -655,7 +704,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface') def test_add_router_interface_vlan_network(self, ari, grps, gn): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id'} ari.return_value = self.fake_router_interface_info self.get_router.return_value = self.fake_router_with_ext_gw @@ -664,8 +712,10 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): fake_network_vlan[pnet.NETWORK_TYPE] = constants.TYPE_VLAN gn.return_value = fake_network_vlan - self.l3_inst.add_router_interface(self.context, router_id, - interface_info) + payload = self._create_payload_for_router_interface(router_id) + self.ovn_drv._process_add_router_interface(resources.ROUTER_INTERFACE, + events.AFTER_CREATE, + self, payload) # Make sure that the "reside-on-redirect-chassis" option was # set to the new router port @@ -689,13 +739,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): def test_remove_router_interface_with_gateway_set(self): router_id = 'router-id' - interface_info = {'port_id': 'router-port-id', - 'subnet_id': 'subnet-id'} self.get_router.return_value = self.fake_router_with_ext_gw self.get_port.side_effect = n_exc.PortNotFound( port_id='router-port-id') - self.l3_inst.remove_router_interface( - self.context, router_id, interface_info) + payload = self._create_payload_for_router_interface(router_id, + pass_subnet=False) + self.ovn_drv._process_remove_router_interface( + resources.ROUTER_INTERFACE, events.AFTER_DELETE, self, payload) nb_ovn = self.l3_inst._nb_ovn nb_ovn.lrp_del.assert_called_once_with( 'lrp-router-port-id', 'neutron-router-id', if_exists=True) @@ -711,15 +761,17 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'update_router') def test_update_router_with_ext_gw(self, ur, grps): self.l3_inst._nb_ovn.is_col_present.return_value = True - router = {'router': {'name': 'router'}} - self.get_router.return_value = self.fake_router_without_ext_gw ur.return_value = self.fake_router_with_ext_gw self.get_subnet.side_effect = lambda ctx, sid: { 'ext-subnet-id': self.fake_ext_subnet}.get(sid, self.fake_subnet) self.get_port.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports - self.l3_inst.update_router(self.context, 'router-id', router) + payload = self._create_payload_for_router_update( + self.fake_router_without_ext_gw, self.fake_router_with_ext_gw) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) self.l3_inst._nb_ovn.add_lrouter_port.assert_called_once_with( **self.fake_ext_gw_port_assert) @@ -748,7 +800,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): grps, mock_get_gw): self.l3_inst._nb_ovn.is_col_present.return_value = True mock_get_gw.return_value = [mock.sentinel.GwRoute] - router = {'router': {'name': 'router'}} fake_old_ext_subnet = {'id': 'old-ext-subnet-id', 'ip_version': 4, 'cidr': '192.168.2.0/24', @@ -768,7 +819,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.get_port.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports - self.l3_inst.update_router(self.context, 'router-id', router) + payload = self._create_payload_for_router_update( + self.get_router.return_value, self.fake_router_with_ext_gw) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) # Check deleting old router gateway self.l3_inst._nb_ovn.delete_lrouter_ext_gw.assert_called_once_with( @@ -805,7 +860,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): grps, mock_get_gw): self.l3_inst._nb_ovn.is_col_present.return_value = True mock_get_gw.return_value = [mock.sentinel.GwRoute] - router = {'router': {'name': 'router'}} # Old gateway info with same subnet and different ip address gr_value = copy.deepcopy(self.fake_router_with_ext_gw) gr_value['external_gateway_info'][ @@ -818,7 +872,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.get_port.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports - self.l3_inst.update_router(self.context, 'router-id', router) + payload = self._create_payload_for_router_update( + gr_value, self.fake_router_with_ext_gw) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) # Check deleting old router gateway self.l3_inst._nb_ovn.delete_lrouter_ext_gw.assert_called_once_with( @@ -868,8 +926,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'update_router') def test_update_router_with_ext_gw_and_disabled_snat(self, ur, grps): self.l3_inst._nb_ovn.is_col_present.return_value = True - router = {'router': {'name': 'router'}} - self.get_router.return_value = self.fake_router_without_ext_gw ur.return_value = self.fake_router_with_ext_gw ur.return_value['external_gateway_info']['enable_snat'] = False self.get_subnet.side_effect = lambda ctx, sid: { @@ -877,7 +933,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.get_port.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports - self.l3_inst.update_router(self.context, 'router-id', router) + payload = self._create_payload_for_router_update( + self.fake_router_without_ext_gw, self.fake_router_with_ext_gw) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) # Need not check lsp and lrp here, it has been tested in other cases self.l3_inst._nb_ovn.add_static_route.assert_called_once_with( @@ -892,7 +952,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') def test_enable_snat(self, ur, grps): - router = {'router': {'name': 'router'}} gr_value = copy.deepcopy(self.fake_router_with_ext_gw) gr_value['external_gateway_info']['enable_snat'] = False self.get_router.return_value = gr_value @@ -905,7 +964,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.get_port.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports - self.l3_inst.update_router(self.context, 'router-id', router) + payload = self._create_payload_for_router_update( + self.fake_router_with_ext_gw, ur.return_value) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) self.l3_inst._nb_ovn.delete_static_route.assert_not_called() self.l3_inst._nb_ovn.delete_nat_rule_in_lrouter.assert_not_called() @@ -927,7 +990,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): mock_get_gw.return_value = [mock.sentinel.GwRoute] mock_snats.return_value = [mock.sentinel.NAT] mock_ext_ips.return_value = False - router = {'router': {'name': 'router'}} self.get_router.return_value = self.fake_router_with_ext_gw ur.return_value = copy.deepcopy(self.fake_router_with_ext_gw) ur.return_value['external_gateway_info']['enable_snat'] = False @@ -936,7 +998,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.get_port.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports - self.l3_inst.update_router(self.context, 'router-id', router) + payload = self._create_payload_for_router_update( + self.fake_router_with_ext_gw, ur.return_value) + self.ovn_drv._process_router_update(resources.ROUTER, + events.AFTER_UPDATE, + self, payload) nb_ovn = self.l3_inst._nb_ovn nb_ovn.delete_static_route.assert_not_called() @@ -949,7 +1015,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): def test_create_floatingip(self): self.l3_inst._nb_ovn.is_col_present.return_value = True self._get_floatingip.return_value = {'floating_port_id': 'fip-port-id'} - self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', @@ -975,7 +1047,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self._get_floatingip.return_value = {'floating_port_id': 'fip-port-id'} config.cfg.CONF.set_override( 'enable_distributed_floating_ip', True, group='ovn') - self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', @@ -1003,7 +1081,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self._get_floatingip.return_value = {'floating_port_id': 'fip-port-id'} config.cfg.CONF.set_override( 'enable_distributed_floating_ip', True, group='ovn') - self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', @@ -1026,7 +1110,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.l3_inst._nb_ovn.get_lrouter_nat_rules.return_value = [ {'external_ip': '192.168.0.10', 'logical_ip': '10.0.0.6', 'type': 'dnat_and_snat', 'uuid': 'uuid1'}] - self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', @@ -1051,7 +1141,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.l3_inst._nb_ovn.get_lrouter_nat_rules.return_value = [ {'external_ip': '192.168.0.10', 'logical_ip': '10.0.0.0/24', 'type': 'snat', 'uuid': 'uuid1'}] - self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) self.l3_inst._nb_ovn.set_nat_rule_in_lrouter.assert_not_called() expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], @@ -1075,7 +1171,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): foo_lport = fake_resources.FakeOvsdbRow.create_one_ovsdb_row() foo_lport.uuid = 'foo-port' self.l3_inst._nb_ovn.get_lswitch_port.return_value = foo_lport - self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) calls = [mock.call( 'Logical_Switch_Port', 'foo-port', @@ -1092,7 +1194,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.l3_inst._nb_ovn.is_col_present.return_value = True self.l3_inst._nb_ovn.lookup.return_value = self.lb_network self.l3_inst._nb_ovn.get_lswitch_port.return_value = self.member_lsp - fip = self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) + fip = self.fake_floating_ip # Validate that there is no external_mac and logical_port while # setting the NAT entry. expected_ext_ids = { @@ -1121,7 +1230,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): [self.fake_ovn_nat_rule], ] self.l3_inst._nb_ovn.lookup.return_value = self.lb_network - fip = self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) + fip = self.fake_floating_ip expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: fip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: mock.ANY, @@ -1160,7 +1276,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): _nb_ovn.get_lrouter_port.return_value = lrp self.l3_inst.get_router.return_value = self.fake_router_with_ext_gw - self.l3_inst.create_floatingip(self.context, 'floatingip') + payload = events.DBEventPayload( + self.context, states=(self.fake_floating_ip,), + resource_id=self.fake_floating_ip['id'], + request_body={'floatingip': self.fake_floating_ip}) + self.ovn_drv._process_floatingip_create(resources.FLOATING_IP, + events.AFTER_CREATE, + self, payload) _nb_ovn.set_nat_rule_in_lrouter.assert_not_called() expected_ext_ids = { @@ -1278,7 +1400,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): nb_ovn.get_floatingip.return_value = ( self.fake_ovn_nat_rule) fip = {l3_def.FLOATINGIP: {'port_id': 'port1'}} - self.l3_inst.update_floatingip(self.context, 'id', fip) + payload = events.DBEventPayload( + self.context, + states=(self.fake_floating_ip_new, self.fake_floating_ip_new), + resource_id=self.fake_floating_ip_new['id'], + request_body=fip) + self.ovn_drv._process_floatingip_update(resources.FLOATING_IP, + events.AFTER_UPDATE, + self, payload) nb_ovn.delete_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='dnat_and_snat', @@ -1311,9 +1440,16 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): nb_ovn.get_floatingip.return_value = ( self.fake_ovn_nat_rule) fip = {l3_def.FLOATINGIP: {qos_consts.QOS_POLICY_ID: 'qos_id_1'}} + payload = events.DBEventPayload( + self.context, + states=(self.fake_floating_ip_new, self.fake_floating_ip_new), + resource_id=self.fake_floating_ip_new['id'], + request_body=fip) with mock.patch.object(self.l3_inst._ovn_client._qos_driver, 'update_floatingip') as ufip: - self.l3_inst.update_floatingip(self.context, 'id', fip) + self.ovn_drv._process_floatingip_update(resources.FLOATING_IP, + events.AFTER_UPDATE, + self, payload) nb_ovn.delete_nat_rule_in_lrouter.assert_not_called() nb_ovn.add_nat_rule_in_lrouter.assert_not_called() ufip.assert_called_once_with(mock.ANY, self.fake_floating_ip_new) @@ -1325,7 +1461,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.fake_floating_ip.update({'fixed_port_id': None}) uf.return_value = self.fake_floating_ip_new fip = {l3_def.FLOATINGIP: {'port_id': 'port1'}} - self.l3_inst.update_floatingip(self.context, 'id', fip) + payload = events.DBEventPayload( + self.context, + states=(self.fake_floating_ip_new, self.fake_floating_ip_new), + resource_id=self.fake_floating_ip_new['id'], + request_body=fip) + self.ovn_drv._process_floatingip_update(resources.FLOATING_IP, + events.AFTER_UPDATE, + self, payload) self.l3_inst._nb_ovn.delete_nat_rule_in_lrouter.assert_not_called() expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip_new['id'], @@ -1362,7 +1505,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): config.cfg.CONF.set_override( 'enable_distributed_floating_ip', True, group='ovn') fip = {l3_def.FLOATINGIP: {'port_id': 'port1'}} - self.l3_inst.update_floatingip(self.context, 'id', fip) + payload = events.DBEventPayload( + self.context, + states=(self.fake_floating_ip_new, self.fake_floating_ip_new), + resource_id=self.fake_floating_ip_new['id'], + request_body=fip) + self.ovn_drv._process_floatingip_update(resources.FLOATING_IP, + events.AFTER_UPDATE, + self, payload) self.l3_inst._nb_ovn.delete_nat_rule_in_lrouter.assert_not_called() expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip_new['id'], @@ -1391,7 +1541,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.fake_floating_ip_new.update({'port_id': 'foo'}) uf.return_value = self.fake_floating_ip_new fip = {l3_def.FLOATINGIP: {'port_id': 'port1'}} - self.l3_inst.update_floatingip(self.context, 'id', fip) + payload = events.DBEventPayload( + self.context, + states=(self.fake_floating_ip_new, self.fake_floating_ip_new), + resource_id=self.fake_floating_ip_new['id'], + request_body=fip) + self.ovn_drv._process_floatingip_update(resources.FLOATING_IP, + events.AFTER_UPDATE, + self, payload) nb_ovn.delete_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='dnat_and_snat', @@ -1427,7 +1584,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 'fixed_port_id': 'port_id'}) uf.return_value = self.fake_floating_ip_new fip = {l3_def.FLOATINGIP: {'port_id': 'port1'}} - self.l3_inst.update_floatingip(self.context, 'id', fip) + payload = events.DBEventPayload( + self.context, + states=(self.fake_floating_ip_new, self.fake_floating_ip_new), + resource_id=self.fake_floating_ip_new['id'], + request_body=fip) + self.ovn_drv._process_floatingip_update(resources.FLOATING_IP, + events.AFTER_UPDATE, + self, payload) nb_ovn.delete_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', @@ -1452,48 +1616,58 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): logical_port='port_id', external_ids=expected_ext_ids) - @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_floatingips') - def test_disassociate_floatingips(self, gfs): - gfs.return_value = [{'id': 'fip-id1', - 'floating_ip_address': '192.168.0.10', - 'router_id': 'router-id', - 'port_id': 'port_id', - 'floating_port_id': 'fip-port-id1', - 'fixed_ip_address': '10.0.0.10', - 'floating_network_id': 'net1'}, - {'id': 'fip-id2', - 'floating_ip_address': '192.167.0.10', - 'router_id': 'router-id', - 'port_id': 'port_id', - 'floating_port_id': 'fip-port-id2', - 'fixed_ip_address': '10.0.0.11', - 'floating_network_id': 'net2'}] - self.l3_inst.disassociate_floatingips(self.context, 'port_id', - do_notify=False) + @mock.patch('neutron.services.ovn_l3.plugin.OVNL3RouterPlugin.' + 'update_floatingip_status') + def test_disassociate_floatingips(self, status_upd_mock): + old_fips = [{'id': 'fip-id1', + 'floating_ip_address': '192.168.0.10', + 'router_id': 'router-id', + 'port_id': 'port_id', + 'floating_port_id': 'fip-port-id1', + 'fixed_ip_address': '10.0.0.10', + 'floating_network_id': 'net1'}, + {'id': 'fip-id2', + 'floating_ip_address': '192.167.0.10', + 'router_id': 'router-id', + 'port_id': 'port_id', + 'floating_port_id': 'fip-port-id2', + 'fixed_ip_address': '10.0.0.11', + 'floating_network_id': 'net2'}] + for fip in old_fips: + payload = events.DBEventPayload( + self.context, states=(fip, self.fake_floating_ip_new), + resource_id=self.fake_floating_ip_new['id']) + self.ovn_drv._process_floatingip_update(resources.FLOATING_IP, + events.AFTER_UPDATE, + self, payload) delete_nat_calls = [mock.call('neutron-router-id', type='dnat_and_snat', logical_ip=fip['fixed_ip_address'], external_ip=fip['floating_ip_address']) - for fip in gfs.return_value] + for fip in old_fips] self.assertEqual( len(delete_nat_calls), self.l3_inst._nb_ovn.delete_nat_rule_in_lrouter.call_count) self.l3_inst._nb_ovn.delete_nat_rule_in_lrouter.assert_has_calls( delete_nat_calls, any_order=True) + self.assertEqual(len(delete_nat_calls), status_upd_mock.call_count) + @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') @mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.' 'ovn_client.OVNClient.update_router_port') - def test_port_update_postcommit(self, update_rp_mock): + def test_port_update_postcommit(self, update_rp_mock, gr): context = 'fake_context' - port = {'device_owner': 'foo'} + port = {'device_owner': 'foo', 'device_id': 'id'} + gr.return_value = {'flavor_id': None} self.l3_inst._port_update(resources.PORT, events.AFTER_UPDATE, None, payload=events.DBEventPayload( context, states=(port,))) update_rp_mock.assert_not_called() - port = {'device_owner': constants.DEVICE_OWNER_ROUTER_INTF} + port = {'device_owner': constants.DEVICE_OWNER_ROUTER_INTF, + 'device_id': 'router_id'} self.l3_inst._port_update(resources.PORT, events.AFTER_UPDATE, None, payload=events.DBEventPayload( context, @@ -1503,11 +1677,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): port, if_exists=True) - def test_port_update_before_update_router_port_without_ip(self): + @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') + def test_port_update_before_update_router_port_without_ip(self, gr): context = 'fake_context' port = {'device_owner': constants.DEVICE_OWNER_ROUTER_INTF, 'fixed_ips': [], - 'id': 'port-id'} + 'id': 'port-id', + 'device_id': 'router_id'} + gr.return_value = {'flavor_id': None} self.assertRaises( n_exc.ServicePortInUse, self.l3_inst._port_update, @@ -1674,8 +1851,10 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): gn.return_value = prov_net gns.return_value = [self.fake_network] - self.l3_inst.add_router_interface(self.context, router_id, - interface_info) + payload = self._create_payload_for_router_interface(router_id) + self.ovn_drv._process_add_router_interface(resources.ROUTER_INTERFACE, + events.AFTER_CREATE, + self, payload) # Make sure that the "gateway_mtu" option was set to the router port fake_router_port_assert = self.fake_router_port_assert @@ -1708,8 +1887,6 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): config.cfg.CONF.set_override( 'ovn_emit_need_to_frag', True, group='ovn') router_id = 'router-id' - interface_info = {'port_id': 'router-port-id', - 'network_id': 'priv-net'} ari.return_value = self.fake_router_interface_info # If we remove the router halfway the return value of # _get_routers_ports will be RouterNotFound @@ -1723,8 +1900,10 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): gn.return_value = prov_net gns.return_value = [self.fake_network] - self.l3_inst.add_router_interface(self.context, router_id, - interface_info) + payload = self._create_payload_for_router_interface(router_id) + self.ovn_drv._process_add_router_interface(resources.ROUTER_INTERFACE, + events.AFTER_CREATE, + self, payload) # Make sure that the "gateway_mtu" option was set to the router port fake_router_port_assert = self.fake_router_port_assert @@ -1882,7 +2061,9 @@ class OVNL3ExtrarouteTests(test_l3_gw.ExtGwModeIntTestCase, test_floatingip_update_subnet_gateway_disabled(expected_status) # Test function _subnet_update of L3 OVN plugin. - def test_update_subnet_gateway_for_external_net(self): + @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') + def test_update_subnet_gateway_for_external_net(self, gr): + gr.return_value = {'flavor_id': None} super(OVNL3ExtrarouteTests, self). \ test_update_subnet_gateway_for_external_net() self.l3_inst._nb_ovn.add_static_route.assert_called_once_with( diff --git a/releasenotes/notes/add-ovn-l3-router-flavors-5c2e14fca15723fa.yaml b/releasenotes/notes/add-ovn-l3-router-flavors-5c2e14fca15723fa.yaml new file mode 100644 index 00000000000..1573cd20f10 --- /dev/null +++ b/releasenotes/notes/add-ovn-l3-router-flavors-5c2e14fca15723fa.yaml @@ -0,0 +1,8 @@ +--- +features: + - Neutron allows users to create routers with flavors with the L3 OVN + plugin. This new feature can be configured in the neutron.conf file. + Please see the "Creating a L3 OVN router with a user defined flavor" + section under Neutron configuration in the documentation for more + details. This document also describes the steps users have to take to + create a router with a flavor assigned to it.