[Server Side] L3 router support ndp proxy
Change-Id: I9b92702af8a235443a2fa1aea3997f3d40a03fc3 Partial-Bug: #1877301
This commit is contained in:
parent
f94226c514
commit
a0a25cb15c
27
neutron/conf/db/l3_ndpproxy_db.py
Normal file
27
neutron/conf/db/l3_ndpproxy_db.py
Normal 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)
|
@ -1 +1 @@
|
||||
cd9ef14ccf87
|
||||
34cf8b009713
|
||||
|
@ -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')
|
||||
)
|
62
neutron/db/models/ndp_proxy.py
Normal file
62
neutron/db/models/ndp_proxy.py
Normal 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')
|
||||
)
|
21
neutron/extensions/l3_ext_ndp_proxy.py
Normal file
21
neutron/extensions/l3_ext_ndp_proxy.py
Normal 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
|
74
neutron/extensions/l3_ndp_proxy.py
Normal file
74
neutron/extensions/l3_ndp_proxy.py
Normal 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
|
80
neutron/objects/ndp_proxy.py
Normal file
80
neutron/objects/ndp_proxy.py
Normal 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),
|
||||
}
|
0
neutron/services/ndp_proxy/__init__.py
Normal file
0
neutron/services/ndp_proxy/__init__.py
Normal file
65
neutron/services/ndp_proxy/exceptions.py
Normal file
65
neutron/services/ndp_proxy/exceptions.py
Normal 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.")
|
386
neutron/services/ndp_proxy/plugin.py
Normal file
386
neutron/services/ndp_proxy/plugin.py
Normal 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)
|
518
neutron/tests/unit/extensions/test_l3_ndp_proxy.py
Normal file
518
neutron/tests/unit/extensions/test_l3_ndp_proxy.py
Normal 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)
|
@ -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,
|
||||
|
53
neutron/tests/unit/objects/test_ndp_proxy.py
Normal file
53
neutron/tests/unit/objects/test_ndp_proxy.py
Normal 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()})
|
@ -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',
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user