388 lines
17 KiB
Python
388 lines
17 KiB
Python
# 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.get(
|
|
l3_ext_ndp_proxy.ENABLE_NDP_PROXY, lib_consts.ATTR_NOT_SPECIFIED)
|
|
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
|
|
# address 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)
|