diff --git a/neutron/conf/db/l3_ndpproxy_db.py b/neutron/conf/db/l3_ndpproxy_db.py new file mode 100644 index 00000000000..4cf247b38c2 --- /dev/null +++ b/neutron/conf/db/l3_ndpproxy_db.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from oslo_config import cfg + +from neutron._i18n import _ + + +L3NDPPROXY_OPTS = [ + cfg.BoolOpt('enable_ndp_proxy_by_default', default=False, + help=_('Define the default value of enable_ndp_proxy if not ' + 'provided in router.')) +] + + +def register_db_l3_ndpproxy_opts(conf=cfg.CONF): + conf.register_opts(L3NDPPROXY_OPTS) diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index ac6fb48e5d9..83abe15e953 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -cd9ef14ccf87 +34cf8b009713 diff --git a/neutron/db/migration/alembic_migrations/versions/yoga/expand/34cf8b009713_add_router_ndp_proxy_table.py b/neutron/db/migration/alembic_migrations/versions/yoga/expand/34cf8b009713_add_router_ndp_proxy_table.py new file mode 100644 index 00000000000..fe88a52cc9e --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/yoga/expand/34cf8b009713_add_router_ndp_proxy_table.py @@ -0,0 +1,71 @@ +# Copyright 2022 OpenStack Foundation +# +# 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 alembic import op +import sqlalchemy as sa + +from neutron_lib.db import constants + + +"""add router ndp proxy table + +Revision ID: 34cf8b009713 +Revises: cd9ef14ccf87 +Create Date: 2021-12-03 03:57:34.838905 + +""" + +# revision identifiers, used by Alembic. +revision = '34cf8b009713' +down_revision = 'cd9ef14ccf87' + + +def upgrade(): + op.create_table( + 'router_ndp_proxy_state', + sa.Column('router_id', sa.String(length=constants.UUID_FIELD_SIZE), + nullable=False), + sa.Column('enable_ndp_proxy', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['router_id'], ['routers.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('router_id'), + ) + op.create_table( + 'ndp_proxies', + sa.Column('project_id', sa.String( + length=constants.PROJECT_ID_FIELD_SIZE), index=True), + sa.Column('name', sa.String(length=constants.NAME_FIELD_SIZE), + nullable=True), + sa.Column('id', sa.String(length=constants.UUID_FIELD_SIZE), + nullable=False), + sa.Column('router_id', + sa.String(length=constants.UUID_FIELD_SIZE), + nullable=False), + sa.Column('port_id', + sa.String(length=constants.UUID_FIELD_SIZE), + nullable=False), + sa.Column('ip_address', sa.String(constants.IP_ADDR_FIELD_SIZE), + nullable=False), + sa.Column('standard_attr_id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint(['router_id'], ['routers.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['standard_attr_id'], + ['standardattributes.id'], + ondelete='CASCADE'), + sa.UniqueConstraint('standard_attr_id') + ) diff --git a/neutron/db/models/ndp_proxy.py b/neutron/db/models/ndp_proxy.py new file mode 100644 index 00000000000..2cdf025f71a --- /dev/null +++ b/neutron/db/models/ndp_proxy.py @@ -0,0 +1,62 @@ +# Copyright 2022 Troila +# 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.api.definitions import l3_ndp_proxy as apidef +from neutron_lib.db import constants as db_const +from neutron_lib.db import model_base +from neutron_lib.db import standard_attr +import sqlalchemy as sa +from sqlalchemy import orm + +from neutron.db.models import l3 + + +class NDPProxy(standard_attr.HasStandardAttributes, + model_base.BASEV2, model_base.HasId, + model_base.HasProject): + + __tablename__ = 'ndp_proxies' + + name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE)) + router_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE), + sa.ForeignKey('routers.id', + ondelete="CASCADE"), + nullable=False) + port_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE), + sa.ForeignKey('ports.id', + ondelete="CASCADE"), + nullable=False) + ip_address = sa.Column(sa.String(db_const.IP_ADDR_FIELD_SIZE), + nullable=False) + api_collections = [apidef.COLLECTION_NAME] + collection_resource_map = {apidef.COLLECTION_NAME: + apidef.RESOURCE_NAME} + + +class RouterNDPProxyState(model_base.BASEV2): + + __tablename__ = 'router_ndp_proxy_state' + + router_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE), + sa.ForeignKey('routers.id', + ondelete="CASCADE"), + nullable=False, primary_key=True) + enable_ndp_proxy = sa.Column(sa.Boolean(), nullable=False) + router = orm.relationship( + l3.Router, load_on_pending=True, + backref=orm.backref("ndp_proxy_state", + lazy='subquery', uselist=False, + cascade='delete') + ) diff --git a/neutron/extensions/l3_ext_ndp_proxy.py b/neutron/extensions/l3_ext_ndp_proxy.py new file mode 100644 index 00000000000..325500778cc --- /dev/null +++ b/neutron/extensions/l3_ext_ndp_proxy.py @@ -0,0 +1,21 @@ +# Copyright 2022 Troila +# 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.api.definitions import l3_ext_ndp_proxy as apidef +from neutron_lib.api import extensions + + +class L3_ext_ndp_proxy(extensions.APIExtensionDescriptor): + api_definition = apidef diff --git a/neutron/extensions/l3_ndp_proxy.py b/neutron/extensions/l3_ndp_proxy.py new file mode 100644 index 00000000000..2cfa6f340d5 --- /dev/null +++ b/neutron/extensions/l3_ndp_proxy.py @@ -0,0 +1,74 @@ +# Copyright 2022 Troila +# 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 abc + +from neutron_lib.api.definitions import l3_ndp_proxy as apidef +from neutron_lib.api import extensions as api_extensions +from neutron_lib.plugins import constants as plugin_consts +from neutron_lib.services import base as service_base + +from neutron.api.v2 import resource_helper + + +class L3_ndp_proxy(api_extensions.APIExtensionDescriptor): + """L3 NDP Proxy API extension""" + + api_definition = apidef + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + special_mappings = {'ndp_proxies': 'ndp_proxy'} + plural_mappings = resource_helper.build_plural_mappings( + special_mappings, apidef.RESOURCE_ATTRIBUTE_MAP) + return resource_helper.build_resource_info( + plural_mappings, + apidef.RESOURCE_ATTRIBUTE_MAP, + plugin_consts.NDPPROXY, + translate_name=True, + allow_bulk=True) + + +class NDPProxyBase(service_base.ServicePluginBase): + + @classmethod + def get_plugin_type(cls): + return plugin_consts.NDPPROXY + + def get_plugin_description(self): + return "NDP Proxy Service Plugin" + + @abc.abstractmethod + def create_ndp_proxy(self, context, ndp_proxy): + pass + + @abc.abstractmethod + def update_ndp_proxy(self, context, id, ndp_proxy): + pass + + @abc.abstractmethod + def get_ndp_proxy(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_ndp_proxies(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + pass + + @abc.abstractmethod + def delete_ndp_proxy(self, context, id): + pass diff --git a/neutron/objects/ndp_proxy.py b/neutron/objects/ndp_proxy.py new file mode 100644 index 00000000000..5e5c5b53181 --- /dev/null +++ b/neutron/objects/ndp_proxy.py @@ -0,0 +1,80 @@ +# Copyright (c) 2022 Troila +# 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 netaddr + +from neutron_lib.objects import common_types +from oslo_log import log as logging +from oslo_versionedobjects import fields as obj_fields + +from neutron.db.models import ndp_proxy as models +from neutron.objects import base + +LOG = logging.getLogger(__name__) + + +@base.NeutronObjectRegistry.register +class NDPProxy(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = models.NDPProxy + + primary_keys = ['id'] + foreign_keys = {'Router': {'router_id': id}, 'Port': {'port_id': id}} + + fields = { + 'id': common_types.UUIDField(), + 'name': obj_fields.StringField(nullable=True), + 'project_id': obj_fields.StringField(nullable=True), + 'router_id': common_types.UUIDField(nullable=False), + 'port_id': common_types.UUIDField(nullable=False), + 'ip_address': obj_fields.IPV6AddressField(), + 'description': obj_fields.StringField(nullable=True) + } + + fields_no_update = ['id', 'project_id'] + + @classmethod + def modify_fields_from_db(cls, db_obj): + result = super(NDPProxy, cls).modify_fields_from_db(db_obj) + if 'ip_address' in result: + result['ip_address'] = netaddr.IPAddress( + result['ip_address']) + return result + + @classmethod + def modify_fields_to_db(cls, fields): + result = super(NDPProxy, cls).modify_fields_to_db(fields) + if 'ip_address' in result: + if result['ip_address'] is not None: + result['ip_address'] = cls.filter_to_str( + result['ip_address']) + return result + + +@base.NeutronObjectRegistry.register +class RouterNDPProxyState(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + db_model = models.RouterNDPProxyState + + foreign_keys = {'Router': {'router_id': id}} + primary_keys = ['router_id'] + + fields = { + 'router_id': common_types.UUIDField(nullable=False), + 'enable_ndp_proxy': obj_fields.BooleanField(nullable=False), + } diff --git a/neutron/services/ndp_proxy/__init__.py b/neutron/services/ndp_proxy/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/ndp_proxy/exceptions.py b/neutron/services/ndp_proxy/exceptions.py new file mode 100644 index 00000000000..f9f5a68dd17 --- /dev/null +++ b/neutron/services/ndp_proxy/exceptions.py @@ -0,0 +1,65 @@ +# Copyright 2022 Troila +# 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 n_exc + +from neutron._i18n import _ + + +class RouterGatewayInUseByNDPProxy(n_exc.Conflict): + message = _("Unable to unset external gateway of router " + "%(router_id)s, There are one or more ndp proxies " + "still in use on the router.") + + +class RouterInterfaceInUseByNDPProxy(n_exc.Conflict): + message = _("Unable to remove subnet %(subnet_id)s from router " + "%(router_id)s, There are one or more ndp proxies " + "still in use on the subnet.") + + +class AddressScopeConflict(n_exc.Conflict): + message = _("The IPv6 address scope %(ext_address_scope)s of external " + "network conflict with internal network's IPv6 address " + "scope %(internal_address_scope)s.") + + +class RouterGatewayNotValid(n_exc.Conflict): + message = _("Can not enable ndp proxy no " + "router %(router_id)s, %(reason)s.") + + +class RouterNDPProxyNotEnable(n_exc.Conflict): + message = _("The enable_ndp_proxy parameter of router %(router_id)s must " + "be set as True while create ndp proxy entry on it.") + + +class PortUnreachableRouter(n_exc.Conflict): + message = _("The port %(port_id)s cannot reach the router %(router_id)s " + "by IPv6 subnet.") + + +class InvalidAddress(n_exc.BadRequest): + message = _("The address %(address)s is invaild, reason: %(reason)s.") + + +class RouterIPv6GatewayInUse(n_exc.Conflict): + message = _("Can't remove the IPv6 subnet from external gateway of " + "router %(router_id)s, the IPv6 subnet in use by the " + "router's ndp proxy.") + + +class NDPProxyNotFound(n_exc.NotFound): + message = _("NDP proxy %(id)s could not be found.") diff --git a/neutron/services/ndp_proxy/plugin.py b/neutron/services/ndp_proxy/plugin.py new file mode 100644 index 00000000000..553414a1e85 --- /dev/null +++ b/neutron/services/ndp_proxy/plugin.py @@ -0,0 +1,386 @@ +# Copyright 2022 Troila +# 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 netaddr +from neutron_lib.api.definitions import l3 as l3_apidef +from neutron_lib.api.definitions import l3_ext_ndp_proxy +from neutron_lib.api.definitions import l3_ndp_proxy as np_apidef +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib import constants as lib_consts +from neutron_lib.db import api as db_api +from neutron_lib.db import resource_extend +from neutron_lib import exceptions as lib_exc +from neutron_lib.plugins import constants +from neutron_lib.plugins import directory +from oslo_config import cfg +from oslo_log import log as logging + +from neutron._i18n import _ +from neutron.api.rpc.callbacks import events as rpc_events +from neutron.api.rpc.handlers import resources_rpc +from neutron.conf.db import l3_ndpproxy_db +from neutron.db import db_base_plugin_common +from neutron.db.models import ndp_proxy as ndp_proxy_models +from neutron.extensions import l3_ndp_proxy +from neutron.objects import base as base_obj +from neutron.objects import ndp_proxy as np +from neutron.services.ndp_proxy import exceptions as exc + +l3_ndpproxy_db.register_db_l3_ndpproxy_opts() +LOG = logging.getLogger(__name__) +V6 = lib_consts.IP_VERSION_6 + + +@resource_extend.has_resource_extenders +@registry.has_registry_receivers +class NDPProxyPlugin(l3_ndp_proxy.NDPProxyBase): + """Implementation of the NDP proxy for ipv6 + + The class implements a NDP proxy plugin. + """ + + supported_extension_aliases = [np_apidef.ALIAS, + l3_ext_ndp_proxy.ALIAS] + + __native_pagination_support = True + __native_sorting_support = True + __filter_validation_support = True + + def __init__(self): + super(NDPProxyPlugin, self).__init__() + self.push_api = resources_rpc.ResourcesPushRpcApi() + self.l3_plugin = directory.get_plugin(constants.L3) + self.core_plugin = directory.get_plugin() + LOG.info("The router's 'enable_ndp_proxy' parameter's default value " + "is %s", cfg.CONF.enable_ndp_proxy_by_default) + + @staticmethod + @resource_extend.extends([l3_apidef.ROUTERS]) + def _extend_router_dict(result_dict, router_db): + # If the router has no external gateway, the enable_ndp_proxy + # parameter is always False. + enable_ndp_proxy = False + if result_dict.get(l3_apidef.EXTERNAL_GW_INFO, None): + # For already existed routers (created before this plugin + # enabled), they have no ndp_proxy_state object. + if not router_db.ndp_proxy_state: + enable_ndp_proxy = cfg.CONF.enable_ndp_proxy_by_default + else: + enable_ndp_proxy = router_db.ndp_proxy_state.enable_ndp_proxy + result_dict[l3_ext_ndp_proxy.ENABLE_NDP_PROXY] = enable_ndp_proxy + + @registry.receives(resources.ROUTER_GATEWAY, [events.BEFORE_DELETE]) + def _check_delete_router_gw(self, resource, event, trigger, payload): + router_db = payload.states[0] + request_body = payload.request_body if payload.request_body else {} + context = payload.context + if np.NDPProxy.get_objects(context, **{'router_id': router_db.id}): + raise exc.RouterGatewayInUseByNDPProxy(router_id=router_db.id) + + # When user unset gateway and enable ndp proxy in same time we shoule + # raise exception. + ndp_proxy_state = request_body.get( + l3_ext_ndp_proxy.ENABLE_NDP_PROXY, None) + if ndp_proxy_state: + reason = _("The router's external gateway will be unset") + raise exc.RouterGatewayNotValid( + router_id=router_db.id, reason=reason) + + if router_db.ndp_proxy_state: + context.session.delete(router_db.ndp_proxy_state) + + @registry.receives(resources.ROUTER_GATEWAY, [events.BEFORE_UPDATE]) + def _check_update_router_gw(self, resource, event, trigger, payload): + # If the router's enable_ndp_proxy is true, we need ensure the external + # gateway has IPv6 address. + router_db = payload.states[0] + if not (router_db.ndp_proxy_state and + router_db.ndp_proxy_state.enable_ndp_proxy): + return + context = payload.context + request_body = payload.request_body + ext_gw = request_body[l3_apidef.EXTERNAL_GW_INFO] + ext_ips = ext_gw.get('external_fixed_ips', None) + if not ext_ips: + return + if [f['ip_address'] for f in ext_ips if + (f.get('ip_address') and + netaddr.IPNetwork(f['ip_address']).version == V6)]: + return + subnet_ids = set(f['subnet_id'] for f in ext_ips + if f.get('subnet_id')) + for subnet_id in subnet_ids: + if self.core_plugin.get_subnet( + context, subnet_id)['ip_version'] == V6: + return + raise exc.RouterIPv6GatewayInUse( + router_id=router_db.id) + + def _ensure_router_ndp_proxy_state_model(self, context, router_db, state): + if not router_db['ndp_proxy_state']: + if state is lib_consts.ATTR_NOT_SPECIFIED: + state = cfg.CONF.enable_ndp_proxy_by_default + kwargs = {'router_id': router_db.id, + 'enable_ndp_proxy': state} + new = ndp_proxy_models.RouterNDPProxyState(**kwargs) + context.session.add(new) + router_db['ndp_proxy_state'] = new + self.l3_plugin._get_router(context, router_db['id']) + else: + router_db['ndp_proxy_state'].update( + {'enable_ndp_proxy': state}) + + def _gateway_is_valid(self, context, gw_port_id): + if not gw_port_id: + return False + port_dict = self.core_plugin.get_port(context.elevated(), gw_port_id) + v6_fixed_ips = [ + fixed_ip for fixed_ip in port_dict['fixed_ips'] + if (netaddr.IPNetwork(fixed_ip['ip_address']).version == V6)] + # If the router's external gateway port user LLA address, The + # external network needn't IPv6 subnet. + if v6_fixed_ips: + return True + return False + + def _check_ext_gw_network(self, context, network_id): + ext_subnets = self.core_plugin.get_subnets( + context.elevated(), filters={'network_id': network_id}) + has_ipv6_subnet = False + for subnet in ext_subnets: + if subnet['ip_version'] == V6: + has_ipv6_subnet = True + if has_ipv6_subnet: + return True + return False + + @registry.receives(resources.ROUTER, [events.PRECOMMIT_CREATE]) + def _process_ndp_proxy_state_for_create_router( + self, resource, event, trigger, payload): + context = payload.context + router_db = payload.metadata['router_db'] + request_body = payload.states[0] + ndp_proxy_state = request_body[l3_ext_ndp_proxy.ENABLE_NDP_PROXY] + ext_gw_info = request_body.get('external_gateway_info') + + if not ext_gw_info and ndp_proxy_state is True: + reason = _("The request body not contain external " + "gateway information") + raise exc.RouterGatewayNotValid( + router_id=router_db.id, reason=reason) + if (ndp_proxy_state == lib_consts.ATTR_NOT_SPECIFIED and not + ext_gw_info) or (ext_gw_info and ndp_proxy_state is False): + return + + if ndp_proxy_state in (True, lib_consts.ATTR_NOT_SPECIFIED): + ext_ips = ext_gw_info.get( + 'external_fixed_ips', []) if ext_gw_info else [] + network_id = self.l3_plugin._validate_gw_info( + context, ext_gw_info, ext_ips, router_db) + ext_gw_support_ndp = self._check_ext_gw_network( + context, network_id) + if not ext_gw_support_ndp and ndp_proxy_state is True: + reason = _("The external network %s don't support " + "IPv6 ndp proxy, the network has no IPv6 " + "subnets.") % network_id + raise exc.RouterGatewayNotValid( + router_id=router_db.id, reason=reason) + if ndp_proxy_state == lib_consts.ATTR_NOT_SPECIFIED: + ndp_proxy_state = ( + ext_gw_support_ndp and + cfg.CONF.enable_ndp_proxy_by_default) + + self._ensure_router_ndp_proxy_state_model( + context, router_db, ndp_proxy_state) + + @registry.receives(resources.ROUTER, [events.PRECOMMIT_UPDATE]) + def _process_ndp_proxy_state_for_update_router(self, resource, event, + trigger, payload=None): + request_body = payload.request_body + context = payload.context + router_db = payload.desired_state + ndp_proxy_state = request_body.get( + l3_ext_ndp_proxy.ENABLE_NDP_PROXY, + lib_consts.ATTR_NOT_SPECIFIED) + if ndp_proxy_state == lib_consts.ATTR_NOT_SPECIFIED: + return + if self._gateway_is_valid(context, router_db['gw_port_id']): + self._ensure_router_ndp_proxy_state_model( + context, router_db, ndp_proxy_state) + elif ndp_proxy_state: + reason = _("The router has no external gateway or the external " + "gateway port has no IPv6 address") + raise exc.RouterGatewayNotValid( + router_id=router_db.id, reason=reason) + + @registry.receives(resources.ROUTER_INTERFACE, [events.BEFORE_DELETE]) + def _check_router_remove_subnet_request(self, resource, event, + trigger, payload): + context = payload.context + np_objs = np.NDPProxy.get_objects( + context, **{'router_id': payload.resource_id}) + if not np_objs: + return + for proxy in np_objs: + port_dict = self.core_plugin.get_port( + payload.context, proxy['port_id']) + v6_fixed_ips = [ + fixed_ip for fixed_ip in port_dict['fixed_ips'] + if (netaddr.IPNetwork(fixed_ip['ip_address'] + ).version == V6)] + if not v6_fixed_ips: + continue + if self._get_internal_ip_subnet( + proxy['ip_address'], + v6_fixed_ips) == payload.metadata['subnet_id']: + raise exc.RouterInterfaceInUseByNDPProxy( + router_id=payload.resource_id, + subnet_id=payload.metadata['subnet_id']) + + def _get_internal_ip_subnet(self, request_ip, fixed_ips): + request_ip = netaddr.IPNetwork(request_ip) + for fixed_ip in fixed_ips: + if netaddr.IPNetwork(fixed_ip['ip_address']) == request_ip: + return fixed_ip['subnet_id'] + + def _check_port(self, context, port_dict, ndp_proxy, router_ports): + ip_address = ndp_proxy.get('ip_address', None) + + def _get_port_v6_fixedips(port_dicts): + v6_fixed_ips = [] + for port_dict in port_dicts: + for fixed_ip in port_dict['fixed_ips']: + if netaddr.IPNetwork( + fixed_ip['ip_address']).version == V6: + v6_fixed_ips.append(fixed_ip) + return v6_fixed_ips + + port_fixedips = _get_port_v6_fixedips([port_dict]) + if not port_fixedips: + # The ndp proxy works with ipv6 addresses, if there is no ipv6 + # address, we need to raise exception. + message = _("Requested port %s must allocate one IPv6 address at " + "least") % port_dict['id'] + raise lib_exc.BadRequest(resource=np_apidef.RESOURCE_NAME, + msg=message) + + router_fixedips = _get_port_v6_fixedips(router_ports) + router_subnets = [fixedip['subnet_id'] for fixedip in router_fixedips] + # If user not specify IPv6 address, we will auto select a valid address + if not ip_address: + for fixedip in port_fixedips: + if fixedip['subnet_id'] in router_subnets: + ndp_proxy['ip_address'] = fixedip['ip_address'] + break + else: + raise exc.PortUnreachableRouter( + port_id=port_dict['id'], + router_id=ndp_proxy['router_id']) + else: + # Check whether the ip_address is valid if user specified a + # IPv6 address + subnet_id = self._get_internal_ip_subnet(ip_address, port_fixedips) + if not subnet_id: + msg = _("This address not belong to the " + "port %s") % port_dict['id'] + raise exc.InvalidAddress(address=ip_address, reason=msg) + if subnet_id not in router_subnets: + msg = _("This address cannot reach the " + "router %s") % ndp_proxy['router_id'] + raise exc.InvalidAddress(address=ip_address, reason=msg) + network_dict = self.core_plugin.get_network( + context, port_dict['network_id']) + return network_dict.get('ipv6_address_scope', None) + + @db_base_plugin_common.convert_result_to_dict + def create_ndp_proxy(self, context, ndp_proxy): + ndp_proxy = ndp_proxy.get(np_apidef.RESOURCE_NAME) + router_id = ndp_proxy['router_id'] + port_id = ndp_proxy['port_id'] + port_dict = self.core_plugin.get_port(context, port_id) + router_ports = self.core_plugin.get_ports( + context, filters={'device_id': [router_id], + 'network_id': [port_dict['network_id']]}) + if not router_ports: + raise exc.PortUnreachableRouter( + router_id=router_id, port_id=port_id) + router_dict = self.l3_plugin.get_router(context, router_id) + if not router_dict.get('enable_ndp_proxy', None): + raise exc.RouterNDPProxyNotEnable(router_id=router_dict['id']) + extrnal_gw_info = router_dict[l3_apidef.EXTERNAL_GW_INFO] + gw_network_dict = self.core_plugin.get_network( + context, extrnal_gw_info['network_id']) + ext_address_scope = gw_network_dict.get('ipv6_address_scope', None) + internal_address_scope = self._check_port( + context, port_dict, ndp_proxy, router_ports) + # If the external network and internal network not belong to same + # adddress scope, the packets can't be forwarded by route. So, in + # this case we should forbid to create ndp proxy entry. + if ext_address_scope != internal_address_scope: + raise exc.AddressScopeConflict( + ext_address_scope=ext_address_scope, + internal_address_scope=internal_address_scope) + + tenant_id = ndp_proxy.pop('tenant_id', None) + if not ndp_proxy.get('project_id', None): + ndp_proxy['project_id'] = tenant_id + + with db_api.CONTEXT_WRITER.using(context): + np_obj = np.NDPProxy(context, **ndp_proxy) + np_obj.create() + + LOG.debug("Notify l3-agent to create ndp proxy rules for " + "ndp proxy: %s", np_obj.to_dict()) + self.push_api.push(context, [np_obj], rpc_events.CREATED) + return np_obj + + @db_base_plugin_common.convert_result_to_dict + def update_ndp_proxy(self, context, id, ndp_proxy): + ndp_proxy = ndp_proxy.get(np_apidef.RESOURCE_NAME) + with db_api.CONTEXT_WRITER.using(context): + obj = np.NDPProxy.get_object(context, id=id) + if not obj: + raise exc.NDPProxyNotFound(id=id) + obj.update_fields(ndp_proxy, reset_changes=True) + obj.update() + return obj + + @db_base_plugin_common.convert_result_to_dict + def get_ndp_proxy(self, context, id, fields=None): + obj = np.NDPProxy.get_object(context, id=id) + if not obj: + raise exc.NDPProxyNotFound(id=id) + return obj + + @db_base_plugin_common.convert_result_to_dict + def get_ndp_proxies(self, context, filters=None, + fields=None, sorts=None, limit=None, marker=None, + page_reverse=False): + pager = base_obj.Pager(sorts, limit, page_reverse, marker) + return np.NDPProxy.get_objects( + context, _pager=pager, **filters) + + def delete_ndp_proxy(self, context, id): + with db_api.CONTEXT_WRITER.using(context): + np_obj = np.NDPProxy.get_object(context, id=id) + if not np_obj: + raise exc.NDPProxyNotFound(id=id) + np_obj.delete() + + LOG.debug("Notify l3-agent to delete ndp proxy rules for " + "ndp proxy: %s", np_obj.to_dict()) + self.push_api.push(context, [np_obj], rpc_events.DELETED) diff --git a/neutron/tests/unit/extensions/test_l3_ndp_proxy.py b/neutron/tests/unit/extensions/test_l3_ndp_proxy.py new file mode 100644 index 00000000000..b86cee4df1b --- /dev/null +++ b/neutron/tests/unit/extensions/test_l3_ndp_proxy.py @@ -0,0 +1,518 @@ +# Copyright 2022 Troila +# 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 unittest import mock + +from neutron_lib.api.definitions import address_scope as scope_apidef +from neutron_lib.api.definitions import dns as dns_apidef +from neutron_lib.api.definitions import dvr as dvr_apidef +from neutron_lib.api.definitions import external_net as enet_apidef +from neutron_lib.api.definitions import l3 as l3_apidef +from neutron_lib.api.definitions import l3_ext_gw_mode +from neutron_lib import constants +from neutron_lib import context +from oslo_config import cfg +from oslo_utils import uuidutils +from webob import exc + +from neutron.db import address_scope_db +from neutron.extensions import address_scope as ext_address_scope +from neutron.extensions import l3 +from neutron.extensions import l3_ndp_proxy +from neutron.tests.unit.api import test_extensions +from neutron.tests.unit.extensions import test_address_scope +from neutron.tests.unit.extensions import test_l3 + +_uuid = uuidutils.generate_uuid + + +class TestL3NDPProxyIntPlugin(address_scope_db.AddressScopeDbMixin, + test_l3.TestL3NatServicePlugin, + test_l3.TestL3NatIntPlugin): + + supported_extension_aliases = [enet_apidef.ALIAS, l3_apidef.ALIAS, + dns_apidef.ALIAS, scope_apidef.ALIAS, + l3_ext_gw_mode.ALIAS, dvr_apidef.ALIAS] + + +class ExtendL3NDPPRroxyExtensionManager(object): + + def get_resources(self): + return (l3.L3.get_resources() + + l3_ndp_proxy.L3_ndp_proxy.get_resources() + + ext_address_scope.Address_scope.get_resources()) + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase, + test_l3.L3BaseForIntTests, + test_l3.L3NatTestCaseMixin): + fmt = 'json' + tenant_id = _uuid() + + def setUp(self): + mock.patch('neutron.api.rpc.handlers.resources_rpc.' + 'ResourcesPushRpcApi').start() + svc_plugins = ('neutron.services.ndp_proxy.plugin.NDPProxyPlugin',) + plugin = ('neutron.tests.unit.extensions.' + 'test_l3_ndp_proxy.TestL3NDPProxyIntPlugin') + ext_mgr = ExtendL3NDPPRroxyExtensionManager() + super(L3NDPProxyTestCase, self).setUp( + ext_mgr=ext_mgr, service_plugins=svc_plugins, plugin=plugin) + self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) + + self.ext_net = self._make_network(self.fmt, 'ext-net', True) + self.ext_net_id = self.ext_net['network']['id'] + self._set_net_external(self.ext_net_id) + self._ext_subnet_v4 = self._make_subnet( + self.fmt, self.ext_net, gateway="10.0.0.1", + cidr="10.0.0.0/24") + self._ext_subnet_v4_id = self._ext_subnet_v4['subnet']['id'] + self._ext_subnet_v6 = self._make_subnet( + self.fmt, self.ext_net, gateway="2001::1:1", + cidr="2001::1:0/112", + ip_version=constants.IP_VERSION_6, + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL) + self._ext_subnet_v6_id = self._ext_subnet_v6['subnet']['id'] + self.router1 = self._make_router(self.fmt, self.tenant_id) + self.router1_id = self.router1['router']['id'] + self.private_net = self._make_network(self.fmt, 'private-net', True) + self.private_subnet = self._make_subnet( + self.fmt, self.private_net, gateway="2001::2:1", + cidr="2001::2:0/112", + ip_version=constants.IP_VERSION_6, + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL) + self._update_router( + self.router1_id, + {'external_gateway_info': {'network_id': self.ext_net_id}, + 'enable_ndp_proxy': True}) + self._router_interface_action( + 'add', self.router1_id, + self.private_subnet['subnet']['id'], None) + + def _create_ndp_proxy(self, router_id, port_id, ip_address=None, + description=None, fmt=None, tenant_id=None, + expected_code=exc.HTTPCreated.code, + expected_message=None): + tenant_id = tenant_id or self.tenant_id + data = {'ndp_proxy': { + "port_id": port_id, + "router_id": router_id} + } + if ip_address: + data['ndp_proxy']['ip_address'] = ip_address + if description: + data['ndp_proxy']['description'] = description + + req_res = self._req( + 'POST', 'ndp-proxies', data, + fmt or self.fmt) + req_res.environ['neutron.context'] = context.Context( + '', tenant_id, is_admin=True) + + res = req_res.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + if expected_message: + self.assertEqual(expected_message, + res.json_body['NeutronError']['message']) + return self.deserialize(self.fmt, res) + + def _update_ndp_proxy(self, ndp_proxy_id, + tenant_id=None, fmt=None, + expected_code=exc.HTTPOk.code, + expected_message=None, **kwargs): + tenant_id = tenant_id or self.tenant_id + data = {} + for k, v in kwargs.items(): + data[k] = v + req_res = self._req( + 'PUT', 'ndp-proxies', {'ndp_proxy': data}, + fmt or self.fmt, id=ndp_proxy_id) + req_res.environ['neutron.context'] = context.Context( + '', tenant_id, is_admin=True) + res = req_res.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + if expected_message: + self.assertEqual(expected_message, + res.json_body['NeutronError']['message']) + return self.deserialize(self.fmt, res) + + def _get_ndp_proxy(self, ndp_proxy_id, tenant_id=None, + fmt=None, expected_code=exc.HTTPOk.code, + expected_message=None): + req_res = self._req('GET', 'ndp-proxies', id=ndp_proxy_id, + fmt=(fmt or self.fmt)) + res = req_res.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + if expected_message: + self.assertEqual(expected_message, + res.json_body['NeutronError']['message']) + return self.deserialize(self.fmt, res) + + def _list_ndp_proxy(self, tenant_id=None, fmt=None, + expected_code=exc.HTTPOk.code, + expected_message=None, **kwargs): + req_res = self._req('GET', 'ndp-proxies', params=kwargs, + fmt=(fmt or self.fmt)) + res = req_res.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + if expected_message: + self.assertEqual(expected_message, + res.json_body['NeutronError']['message']) + return self.deserialize(self.fmt, res) + + def _delete_ndp_proxy(self, ndp_proxy_id, tenant_id=None, + fmt=None, expected_code=exc.HTTPNoContent.code, + expected_message=None): + req_res = self._req('DELETE', 'ndp-proxies', id=ndp_proxy_id, + fmt=(fmt or self.fmt)) + res = req_res.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + if expected_message: + self.assertEqual(expected_message, + res.json_body['NeutronError']['message']) + if res.status_int != exc.HTTPNoContent.code: + return self.deserialize(self.fmt, res) + + def _update_router(self, router_id, update_date, tenant_id=None, + fmt=None, expected_code=exc.HTTPOk.code, + expected_message=None): + tenant_id = tenant_id or self.tenant_id + data = {'router': update_date} + router_req = self.new_update_request( + 'routers', id=router_id, data=data, + fmt=(fmt or self.fmt)) + router_req.environ['neutron.context'] = context.Context( + '', tenant_id, is_admin=True) + res = router_req.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + if expected_message: + self.assertEqual(expected_message, + res.json_body['NeutronError']['message']) + + def _get_router(self, router_id, tenant_id=None, fmt=None, + expected_code=exc.HTTPOk.code, + expected_message=None): + req_res = self._req('GET', 'routers', id=router_id, + fmt=(fmt or self.fmt)) + res = req_res.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + if expected_message: + self.assertEqual(expected_message, + res.json_body['NeutronError']['message']) + return self.deserialize(self.fmt, res) + + def test_create_and_update_ndp_proxy_without_exception(self): + with self.port(self.private_subnet) as port1, \ + self.port(self.private_subnet) as port2: + ipv6_address = port1['port']['fixed_ips'][0]['ip_address'] + ndp_proxy = self._create_ndp_proxy(self.router1_id, + port1['port']['id']) + ndp_proxy_id = ndp_proxy['ndp_proxy']['id'] + desc_str = "Test update description" + self._update_ndp_proxy( + ndp_proxy_id, **{'description': desc_str}) + new_ndp_proxy = self._get_ndp_proxy(ndp_proxy_id) + self.assertEqual( + desc_str, new_ndp_proxy['ndp_proxy']['description']) + + ipv6_address = port2['port']['fixed_ips'][0]['ip_address'] + self._create_ndp_proxy(self.router1_id, port2['port']['id'], + ipv6_address) + list_res = self._list_ndp_proxy() + self.assertEqual(len(list_res['ndp_proxies']), 2) + self._delete_ndp_proxy(ndp_proxy_id) + list_res = self._list_ndp_proxy() + self.assertEqual(len(list_res['ndp_proxies']), 1) + + def test_enable_ndp_proxy_without_external_gateway(self): + with self.router() as router: + router_id = router['router']['id'] + err_msg = ("Can not enable ndp proxy no router %s, The router has " + "no external gateway or the external gateway port has " + "no IPv6 address.") % router_id + self._update_router(router_id, {'enable_ndp_proxy': True}, + expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + + def test_delete_router_gateway_with_enable_ndp_proxy(self): + with self.router() as router: + router_id = router['router']['id'] + self._update_router( + router_id, + {'external_gateway_info': {'network_id': self.ext_net_id}}) + err_msg = ("Can not enable ndp proxy no router %s, The router's " + "external gateway will be unset.") % router_id + self._update_router( + router_id, + {'external_gateway_info': {}, 'enable_ndp_proxy': True}, + expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + + def test_unset_router_gateway_with_ndp_proxy(self): + with self.port(self.private_subnet) as port1: + self._create_ndp_proxy(self.router1_id, port1['port']['id']) + err_msg = ("Unable to unset external gateway of router %s, " + "There are one or more ndp proxies still in use " + "on the router.") % self.router1_id + self._update_router( + self.router1_id, {'external_gateway_info': {}}, + expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + + def test_create_ndp_proxy_with_invalid_port(self): + with self.subnet( + cidr='2001::8:0/112', + ip_version=constants.IP_VERSION_6, + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL) as sub1, \ + self.subnet( + self.private_net, + ip_version=constants.IP_VERSION_6, + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL, + cidr='2001::9:0/112') as sub2, \ + self.subnet(self.private_net) as sub3, \ + self.port(sub1) as port1, \ + self.port( + sub3, + **{'fixed_ips': [ + {'subnet_id': sub3['subnet']['id']}]}) as port2, \ + self.port( + sub2, + **{'fixed_ips': [ + {'subnet_id': sub2['subnet']['id'], + 'ip_address': '2001::9:12'}, + {'subnet_id': self.private_subnet['subnet']['id'], + 'ip_address': '2001::2:12'}, + {'subnet_id': sub3['subnet']['id']}]}) as port3: + err_msg = ("The port %s cannot reach the router %s by IPv6 " + "subnet.") % (port1['port']['id'], self.router1_id) + # Subnet not add to the router + self._create_ndp_proxy( + self.router1_id, port1['port']['id'], + expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + self._router_interface_action( + 'add', self.router1_id, + sub1['subnet']['id'], None) + # Invalid address: the adress not belong to the port + err_msg = ("The address 2001::10:22 is invaild, reason: " + "This address not belong to the " + "port %s.") % port1['port']['id'] + self._create_ndp_proxy( + self.router1_id, port1['port']['id'], + ip_address="2001::10:22", + expected_code=exc.HTTPBadRequest.code, + expected_message=err_msg) + # The subnet of specified address don't connect to router + err_msg = ("The address 2001::9:12 is invaild, reason: " + "This address cannot reach the " + "router %s.") % self.router1_id + self._create_ndp_proxy( + self.router1_id, port3['port']['id'], + ip_address='2001::9:12', + expected_code=exc.HTTPBadRequest.code, + expected_message=err_msg) + # Port only has IPv4 address + err_msg = ("Bad ndp_proxy request: Requested port %s must " + "allocate one IPv6 address at " + "least.") % port2['port']['id'] + self._create_ndp_proxy( + self.router1_id, port2['port']['id'], + expected_code=exc.HTTPBadRequest.code, + expected_message=err_msg) + # Auto select valid address + ndp_proxy = self._create_ndp_proxy( + self.router1_id, port3['port']['id']) + self.assertEqual('2001::2:12', + ndp_proxy['ndp_proxy']['ip_address']) + + def test_create_ndp_proxy_with_invalid_router(self): + with self.subnet( + cidr='2001::8:0/112', + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL, + ip_version=constants.IP_VERSION_6) as subnet, \ + self.router() as router, \ + self.port(subnet) as port: + router_id = router['router']['id'] + subnet_id = subnet['subnet']['id'] + port_id = port['port']['id'] + err_msg = ("The port %s cannot reach the router %s by " + "IPv6 subnet.") % (port_id, router_id) + self._create_ndp_proxy( + router_id, port_id, + expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + self._router_interface_action( + 'add', router_id, subnet_id, None) + err_msg = ("The enable_ndp_proxy parameter of router %s must be " + "set as True while create ndp proxy entry on " + "it.") % router_id + self._create_ndp_proxy( + router_id, port_id, + expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + + def test_update_gateway_without_ipv6_fixed_ip(self): + with self.router() as router: + router_id = router['router']['id'] + self._update_router( + router_id, + {'external_gateway_info': { + 'network_id': self.ext_net_id}, + 'enable_ndp_proxy': True}) + err_msg = ("Can't remove the IPv6 subnet from external gateway of " + "router %s, the IPv6 subnet in use by the router's " + "ndp proxy.") % router_id + ext_gw_data = { + 'external_gateway_info': { + 'network_id': self.ext_net_id, + 'external_fixed_ips': [ + {'subnet_id': self._ext_subnet_v4_id}]}} + self._update_router( + router_id, ext_gw_data, + expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + ext_gw_data = { + 'external_gateway_info': { + 'network_id': self.ext_net_id, + 'external_fixed_ips': [ + {'subnet_id': self._ext_subnet_v6_id}]}} + self._update_router(router_id, ext_gw_data) + + def test_remove_subnet(self): + with self.subnet(ip_version=constants.IP_VERSION_6, + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL, + cidr='2001::50:1:0/112') as subnet, \ + self.port(subnet) as port: + subnet_id = subnet['subnet']['id'] + port_id = port['port']['id'] + self._router_interface_action( + 'add', self.router1_id, subnet_id, None) + self._create_ndp_proxy( + self.router1_id, port_id) + err_msg = ("Unable to remove subnet %s from router %s, There " + "are one or more ndp proxies still in use on the " + "subnet.") % (subnet_id, self.router1_id) + expected_body = { + "NeutronError": { + "type": "RouterInterfaceInUseByNDPProxy", + "message": err_msg, "detail": ""}} + self._router_interface_action( + 'remove', self.router1_id, subnet_id, None, + expected_code=exc.HTTPConflict.code, + expected_body=expected_body) + + def test_create_ndp_proxy_with_different_address_scope(self): + with self.address_scope( + ip_version=constants.IP_VERSION_6, + tenant_id=self.tenant_id) as addr_scope, \ + self.subnetpool(['2001::100:0:0/100'], + **{'address_scope_id': addr_scope['address_scope']['id'], + 'default_prefixlen': 112, 'name': 'test1', + 'tenant_id': self.tenant_id}) as subnetpool, \ + self.subnet( + cidr='2001::100:1:0/112', + ip_version=constants.IP_VERSION_6, + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL, + subnetpool_id=subnetpool['subnetpool']['id'], + tenant_id=self.tenant_id) as subnet, \ + self.port(subnet) as port: + subnet_id = subnet['subnet']['id'] + port_id = port['port']['id'] + self._router_interface_action( + 'add', self.router1_id, subnet_id, None) + err_msg = ("The IPv6 address scope None of external network " + "conflict with internal network's IPv6 address " + "scope %s.") % addr_scope['address_scope']['id'] + self._create_ndp_proxy( + self.router1_id, port_id, + expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + + def test_create_router_with_external_gateway(self): + def _create_router(self, data, expected_code=exc.HTTPCreated.code, + expected_message=None): + router_req = self.new_create_request( + 'routers', data, self.fmt) + router_req.environ['neutron.context'] = context.Context( + '', self.tenant_id, is_admin=True) + res = router_req.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + if expected_message: + self.assertIn(expected_message, + res.json_body['NeutronError']['message']) + return self.deserialize(self.fmt, res) + + # Create router with enable_ndp_proxy is True but not external gateway + err_msg = ("The request body not contain external gateway " + "information.") + data = {'router': {'external_gateway_info': {}, + 'enable_ndp_proxy': True}} + _create_router(self, data, expected_code=exc.HTTPConflict.code, + expected_message=err_msg) + + data = {'router': { + 'external_gateway_info': {'network_id': self.ext_net_id}}} + res = _create_router(self, data) + self.assertFalse(res['router']['enable_ndp_proxy']) + + data = {'router': { + 'external_gateway_info': {'network_id': self.ext_net_id}, + 'enable_ndp_proxy': True}} + res = _create_router(self, data) + self.assertTrue(res['router']['enable_ndp_proxy']) + + # Set default enable_ndp_proxy as True + cfg.CONF.set_override("enable_ndp_proxy_by_default", True) + data = {'router': { + 'external_gateway_info': {'network_id': self.ext_net_id}}} + res = _create_router(self, data) + self.assertTrue(res['router']['enable_ndp_proxy']) + + def test_enable_ndp_proxy_by_default_conf_option(self): + cfg.CONF.set_override("enable_ndp_proxy_by_default", True) + with self.subnet( + cidr='2001::8:0/112', + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL, + ip_version=constants.IP_VERSION_6) as subnet, \ + self.port(subnet) as port, \ + self.router() as router: + router_id = router['router']['id'] + subnet_id = subnet['subnet']['id'] + port_id = port['port']['id'] + self._router_interface_action( + 'add', router_id, subnet_id, None) + router_dict = self._get_router(router_id) + self.assertFalse(router_dict['router']['enable_ndp_proxy']) + self._update_router( + router_id, + {'external_gateway_info': {'network_id': self.ext_net_id}}) + router_dict = self._get_router(router_id) + self.assertTrue(router_dict['router']['enable_ndp_proxy']) + self._create_ndp_proxy( + router_id, port_id) diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index c8888bc2b20..40ddb3525ce 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -542,6 +542,8 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = { obj_fields.IPAddressField: tools.get_random_ip_address, obj_fields.IPV4AddressField: lambda: tools.get_random_ip_address( version=constants.IP_VERSION_4), + obj_fields.IPV6AddressField: lambda: tools.get_random_ip_address( + version=constants.IP_VERSION_6), obj_fields.IntegerField: tools.get_random_integer, obj_fields.ListOfObjectsField: lambda: [], obj_fields.ListOfStringsField: tools.get_random_string_list, diff --git a/neutron/tests/unit/objects/test_ndp_proxy.py b/neutron/tests/unit/objects/test_ndp_proxy.py new file mode 100644 index 00000000000..f3aaeb8e959 --- /dev/null +++ b/neutron/tests/unit/objects/test_ndp_proxy.py @@ -0,0 +1,53 @@ +# Copyright 2022 Troila +# 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.objects import ndp_proxy +from neutron.tests.unit.objects import test_base +from neutron.tests.unit import testlib_api + + +class NDPProxyIfaceObjectTestCase(test_base.BaseObjectIfaceTestCase): + + _test_class = ndp_proxy.NDPProxy + + +class NDPProxyDbObjectTestCase(test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = ndp_proxy.NDPProxy + + def setUp(self): + super(NDPProxyDbObjectTestCase, self).setUp() + self.update_obj_fields( + {'router_id': lambda: self._create_test_router_id(), + 'port_id': lambda: self._create_test_port_id()}) + + +class RouterNDPProxyStateIfaceObjectTestCase( + test_base.BaseObjectIfaceTestCase): + + _test_class = ndp_proxy.RouterNDPProxyState + + +class RouterNDPProxyStateDbObjectTestCase(test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = ndp_proxy.RouterNDPProxyState + + def setUp(self): + super(RouterNDPProxyStateDbObjectTestCase, self).setUp() + self.update_obj_fields( + {'router_id': lambda: self._create_test_router_id()}) diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 361216ab5cc..48c5c31978b 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -64,6 +64,7 @@ object_data = { 'MeteringLabel': '1.0-cc4b620a3425222447cbe459f62de533', 'MeteringLabelRule': '2.0-0ad09894c62e1ce6e868f725158959ba', 'Log': '1.0-6391351c0f34ed34375a19202f361d24', + 'NDPProxy': '1.0-a6597d9caac3bb0d63f943f82e4dda8c', 'Network': '1.1-c3e9ecc0618ee934181d91b143a48901', 'NetworkDhcpAgentBinding': '1.1-d9443c88809ffa4c45a0a5a48134b54a', 'NetworkDNSDomain': '1.0-420db7910294608534c1e2e30d6d8319', @@ -106,6 +107,7 @@ object_data = { 'Router': '1.0-adb984d9b73aa11566d40abbeb790df1', 'RouterExtraAttributes': '1.0-ef8d61ae2864f0ec9af0ab7939cab318', 'RouterL3AgentBinding': '1.0-c5ba6c95e3a4c1236a55f490cd67da82', + 'RouterNDPProxyState': '1.0-4042e475bf173d1d8d17adb962eae1b2', 'RouterPort': '1.0-c8c8f499bcdd59186fcd83f323106908', 'RouterRoute': '1.0-07fc5337c801fb8c6ccfbcc5afb45907', 'SecurityGroup': '1.5-7eb8e44c327512e7bb1759ab41ede44b', diff --git a/setup.cfg b/setup.cfg index 27c05058990..331ba0d5f4a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -87,6 +87,7 @@ neutron.service_plugins = conntrack_helper = neutron.services.conntrack_helper.plugin:Plugin ovn-router = neutron.services.ovn_l3.plugin:OVNL3RouterPlugin local_ip = neutron.services.local_ip.local_ip_plugin:LocalIPPlugin + ndp_proxy = neutron.services.ndp_proxy.plugin:NDPProxyPlugin neutron.ml2.type_drivers = flat = neutron.plugins.ml2.drivers.type_flat:FlatTypeDriver local = neutron.plugins.ml2.drivers.type_local:LocalTypeDriver @@ -220,6 +221,7 @@ neutron.objects = L3HARouterVRIdAllocation = neutron.objects.l3_hamode:L3HARouterVRIdAllocation MeteringLabel = neutron.objects.metering:MeteringLabel MeteringLabelRule = neutron.objects.metering:MeteringLabelRule + NDPProxy = neutron.objects.ndp_proxy:NDPProxy Network = neutron.objects.network:Network NetworkDNSDomain = neutron.objects.network:NetworkDNSDomain NetworkDhcpAgentBinding = neutron.objects.network:NetworkDhcpAgentBinding @@ -258,6 +260,7 @@ neutron.objects = Router = neutron.objects.router:Router RouterExtraAttributes = neutron.objects.router:RouterExtraAttributes RouterL3AgentBinding = neutron.objects.l3agent:RouterL3AgentBinding + RouterNDPProxyState = neutron.objects.ndp_proxy:RouterNDPProxyState RouterPort = neutron.objects.router:RouterPort RouterRoute = neutron.objects.router:RouterRoute SecurityGroup = neutron.objects.securitygroup:SecurityGroup