[Server Side] L3 router support ndp proxy

Change-Id: I9b92702af8a235443a2fa1aea3997f3d40a03fc3
Partial-Bug: #1877301
This commit is contained in:
Yang JianFeng 2020-11-14 08:45:19 +00:00 committed by yangjianfeng
parent f94226c514
commit a0a25cb15c
15 changed files with 1365 additions and 1 deletions

View File

@ -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)

View File

@ -1 +1 @@
cd9ef14ccf87
34cf8b009713

View File

@ -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')
)

View File

@ -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')
)

View File

@ -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

View File

@ -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

View File

@ -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),
}

View File

View File

@ -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.")

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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()})

View File

@ -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',

View File

@ -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