389 lines
17 KiB
Python
389 lines
17 KiB
Python
# Copyright 2015-2016 Hewlett Packard Enterprise Development Company, LP
|
|
#
|
|
# 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 constants as api_const
|
|
from neutron_lib.api.definitions import l3 as l3_apidef
|
|
from neutron_lib.api.definitions import network as net_def
|
|
from neutron_lib.callbacks import events
|
|
from neutron_lib.callbacks import registry
|
|
from neutron_lib.callbacks import resources
|
|
from neutron_lib.db import api as db_api
|
|
from neutron_lib.db import resource_extend
|
|
from neutron_lib.db import utils as db_utils
|
|
from neutron_lib import exceptions as n_exc
|
|
from neutron_lib.objects import exceptions as obj_exc
|
|
from neutron_lib.plugins import constants
|
|
from neutron_lib.plugins import directory
|
|
from neutron_lib.plugins import utils as p_utils
|
|
from oslo_log import log as logging
|
|
|
|
from neutron._i18n import _
|
|
from neutron.objects import auto_allocate as auto_allocate_obj
|
|
from neutron.objects import base as base_obj
|
|
from neutron.objects import network as net_obj
|
|
from neutron.services.auto_allocate import exceptions
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CHECK_REQUIREMENTS = 'dry-run'
|
|
|
|
|
|
def _ensure_external_network_default_value_callback(
|
|
resource, event, trigger, payload=None):
|
|
"""Ensure the is_default db field matches the create/update request."""
|
|
|
|
_request = payload.request_body
|
|
_context = payload.context
|
|
_network = payload.desired_state or payload.latest_state
|
|
_orig = payload.states[0]
|
|
|
|
@db_api.retry_if_session_inactive()
|
|
def _do_ensure_external_network_default_value_callback(
|
|
context, request, orig, network):
|
|
is_default = request.get(api_const.IS_DEFAULT)
|
|
if is_default is None:
|
|
return
|
|
if is_default:
|
|
# ensure only one default external network at any given time
|
|
pager = base_obj.Pager(limit=1)
|
|
objs = net_obj.ExternalNetwork.get_objects(context, _pager=pager,
|
|
is_default=True)
|
|
if objs:
|
|
if objs[0] and network['id'] != objs[0].network_id:
|
|
raise exceptions.DefaultExternalNetworkExists(
|
|
net_id=objs[0].network_id)
|
|
|
|
if orig and orig.get(api_const.IS_DEFAULT) == is_default:
|
|
return
|
|
network[api_const.IS_DEFAULT] = is_default
|
|
# Reflect the status of the is_default on the create/update request
|
|
obj = net_obj.ExternalNetwork.get_object(context,
|
|
network_id=network['id'])
|
|
if obj:
|
|
obj.is_default = is_default
|
|
obj.update()
|
|
|
|
_do_ensure_external_network_default_value_callback(
|
|
_context, _request, _orig, _network)
|
|
|
|
|
|
@resource_extend.has_resource_extenders
|
|
class AutoAllocatedTopologyMixin(object):
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
# NOTE(kevinbenton): we subscribe on object construction because
|
|
# the tests blow away the callback manager for each run
|
|
new = super(AutoAllocatedTopologyMixin, cls).__new__(cls, *args,
|
|
**kwargs)
|
|
registry.subscribe(_ensure_external_network_default_value_callback,
|
|
resources.NETWORK, events.PRECOMMIT_UPDATE)
|
|
registry.subscribe(_ensure_external_network_default_value_callback,
|
|
resources.NETWORK, events.PRECOMMIT_CREATE)
|
|
return new
|
|
|
|
# TODO(armax): if a tenant modifies auto allocated resources under
|
|
# the hood the behavior of the get_auto_allocated_topology API is
|
|
# undetermined. Consider adding callbacks to deal with the following
|
|
# situations:
|
|
# - insert subnet -> plug router interface
|
|
# - delete router -> remove the entire topology
|
|
# - update subnet -> prevent operation
|
|
# - update router gateway -> prevent operation
|
|
# - ...
|
|
|
|
@property
|
|
def core_plugin(self):
|
|
if not getattr(self, '_core_plugin', None):
|
|
self._core_plugin = directory.get_plugin()
|
|
return self._core_plugin
|
|
|
|
@property
|
|
def l3_plugin(self):
|
|
if not getattr(self, '_l3_plugin', None):
|
|
self._l3_plugin = directory.get_plugin(constants.L3)
|
|
return self._l3_plugin
|
|
|
|
@staticmethod
|
|
@resource_extend.extends([net_def.COLLECTION_NAME])
|
|
def _extend_external_network_default(net_res, net_db):
|
|
"""Add is_default field to 'show' response."""
|
|
if net_db.external is not None:
|
|
net_res[api_const.IS_DEFAULT] = net_db.external.is_default
|
|
return net_res
|
|
|
|
def get_auto_allocated_topology(self, context, tenant_id, fields=None):
|
|
"""Return tenant's network associated to auto-allocated topology.
|
|
|
|
The topology will be provisioned upon return, if network is missing.
|
|
"""
|
|
fields = fields or []
|
|
tenant_id = self._validate(context, tenant_id)
|
|
if CHECK_REQUIREMENTS in fields:
|
|
# for dry-run requests, simply validates that subsequent
|
|
# requests can be fulfilled based on a set of requirements
|
|
# such as existence of default networks, pools, etc.
|
|
return self._check_requirements(context, tenant_id)
|
|
elif fields:
|
|
raise n_exc.BadRequest(resource='auto_allocate',
|
|
msg=_("Unrecognized field"))
|
|
|
|
# Check for an existent topology
|
|
network_id = self._get_auto_allocated_network(context, tenant_id)
|
|
if network_id:
|
|
return self._response(network_id, tenant_id, fields=fields)
|
|
# See if we indeed have an external network to connect to, otherwise
|
|
# we will fail fast
|
|
default_external_network = self._get_default_external_network(
|
|
context)
|
|
|
|
# If we reach this point, then we got some work to do!
|
|
network_id = self._build_topology(
|
|
context, tenant_id, default_external_network)
|
|
return self._response(network_id, tenant_id, fields=fields)
|
|
|
|
def delete_auto_allocated_topology(self, context, tenant_id):
|
|
tenant_id = self._validate(context, tenant_id)
|
|
topology = self._get_auto_allocated_topology(context, tenant_id)
|
|
if topology:
|
|
subnets = self.core_plugin.get_subnets(
|
|
context,
|
|
filters={'network_id': [topology['network_id']]})
|
|
self._cleanup(
|
|
context, network_id=topology['network_id'],
|
|
router_id=topology['router_id'], subnets=subnets)
|
|
|
|
def _build_topology(self, context, tenant_id, default_external_network):
|
|
"""Build the network topology and returns its network UUID."""
|
|
try:
|
|
subnets = self._provision_tenant_private_network(
|
|
context, tenant_id)
|
|
network_id = subnets[0]['network_id']
|
|
router = self._provision_external_connectivity(
|
|
context, default_external_network, subnets, tenant_id)
|
|
network_id = self._save(
|
|
context, tenant_id, network_id, router['id'], subnets)
|
|
return network_id
|
|
except exceptions.UnknownProvisioningError as e:
|
|
# Clean partially provisioned topologies, and reraise the
|
|
# error. If it can be retried, so be it.
|
|
LOG.error("Unknown error while provisioning topology for "
|
|
"tenant %(tenant_id)s. Reason: %(reason)s",
|
|
{'tenant_id': tenant_id, 'reason': e})
|
|
self._cleanup(
|
|
context, network_id=e.network_id,
|
|
router_id=e.router_id, subnets=e.subnets)
|
|
raise e.error
|
|
|
|
def _check_requirements(self, context, tenant_id):
|
|
"""Raise if requirements are not met."""
|
|
self._get_default_external_network(context)
|
|
try:
|
|
self._get_supported_subnetpools(context)
|
|
except n_exc.NotFound:
|
|
raise exceptions.AutoAllocationFailure(
|
|
reason=_("No default subnetpools defined"))
|
|
return {'id': 'dry-run=pass', 'tenant_id': tenant_id}
|
|
|
|
def _validate(self, context, tenant_id):
|
|
"""Validate and return the tenant to be associated to the topology."""
|
|
if tenant_id == 'None':
|
|
# NOTE(HenryG): the client might be sending us astray by
|
|
# passing no tenant; this is really meant to be the tenant
|
|
# issuing the request, therefore let's get it from the context
|
|
tenant_id = context.tenant_id
|
|
|
|
if not context.is_admin and tenant_id != context.tenant_id:
|
|
raise n_exc.NotAuthorized()
|
|
|
|
return tenant_id
|
|
|
|
def _get_auto_allocated_topology(self, context, tenant_id):
|
|
"""Return the auto allocated topology record if present or None."""
|
|
return auto_allocate_obj.AutoAllocatedTopology.get_object(
|
|
context, project_id=tenant_id)
|
|
|
|
def _get_auto_allocated_network(self, context, tenant_id):
|
|
"""Get the auto allocated network for the tenant."""
|
|
network = self._get_auto_allocated_topology(context, tenant_id)
|
|
if network:
|
|
return network['network_id']
|
|
|
|
@staticmethod
|
|
def _response(network_id, tenant_id, fields=None):
|
|
"""Build response for auto-allocated network."""
|
|
res = {
|
|
'id': network_id,
|
|
'tenant_id': tenant_id
|
|
}
|
|
return db_utils.resource_fields(res, fields)
|
|
|
|
def _get_default_external_network(self, context):
|
|
"""Get the default external network for the deployment."""
|
|
|
|
default_external_networks = net_obj.ExternalNetwork.get_objects(
|
|
context, is_default=True)
|
|
|
|
if not default_external_networks:
|
|
LOG.error("Unable to find default external network "
|
|
"for deployment, please create/assign one to "
|
|
"allow auto-allocation to work correctly.")
|
|
raise exceptions.AutoAllocationFailure(
|
|
reason=_("No default router:external network"))
|
|
if len(default_external_networks) > 1:
|
|
LOG.error("Multiple external default networks detected. "
|
|
"Network %s is true 'default'.",
|
|
default_external_networks[0]['network_id'])
|
|
return default_external_networks[0].network_id
|
|
|
|
def _get_supported_subnetpools(self, context):
|
|
"""Return the default subnet pools available."""
|
|
default_subnet_pools = [
|
|
self.core_plugin.get_default_subnetpool(
|
|
context, ver) for ver in (4, 6)
|
|
]
|
|
available_pools = [
|
|
s for s in default_subnet_pools if s
|
|
]
|
|
if not available_pools:
|
|
LOG.error("No default pools available")
|
|
raise n_exc.NotFound()
|
|
|
|
return available_pools
|
|
|
|
def _provision_tenant_private_network(self, context, tenant_id):
|
|
"""Create a tenant private network/subnets."""
|
|
network = None
|
|
try:
|
|
network_args = {
|
|
'name': 'auto_allocated_network',
|
|
'admin_state_up': False,
|
|
'tenant_id': tenant_id,
|
|
'shared': False
|
|
}
|
|
network = p_utils.create_network(
|
|
self.core_plugin, context, {'network': network_args})
|
|
subnets = []
|
|
for pool in self._get_supported_subnetpools(context):
|
|
subnet_args = {
|
|
'name': 'auto_allocated_subnet_v%s' % pool['ip_version'],
|
|
'network_id': network['id'],
|
|
'tenant_id': tenant_id,
|
|
'ip_version': pool['ip_version'],
|
|
'subnetpool_id': pool['id'],
|
|
}
|
|
subnets.append(p_utils.create_subnet(
|
|
self.core_plugin, context, {'subnet': subnet_args}))
|
|
return subnets
|
|
except (n_exc.SubnetAllocationError, ValueError,
|
|
n_exc.BadRequest, n_exc.NotFound) as e:
|
|
LOG.error("Unable to auto allocate topology for tenant "
|
|
"%(tenant_id)s due to missing or unmet "
|
|
"requirements. Reason: %(reason)s",
|
|
{'tenant_id': tenant_id, 'reason': e})
|
|
if network:
|
|
self._cleanup(context, network['id'])
|
|
raise exceptions.AutoAllocationFailure(
|
|
reason=_("Unable to provide tenant private network"))
|
|
except Exception as e:
|
|
network_id = network['id'] if network else None
|
|
raise exceptions.UnknownProvisioningError(e, network_id=network_id)
|
|
|
|
def _provision_external_connectivity(self, context,
|
|
default_external_network, subnets,
|
|
tenant_id):
|
|
"""Uplink tenant subnet(s) to external network."""
|
|
router_args = {
|
|
'name': 'auto_allocated_router',
|
|
l3_apidef.EXTERNAL_GW_INFO: {
|
|
'network_id': default_external_network},
|
|
'tenant_id': tenant_id,
|
|
'admin_state_up': True
|
|
}
|
|
router = None
|
|
attached_subnets = []
|
|
try:
|
|
router = self.l3_plugin.create_router(
|
|
context, {'router': router_args})
|
|
for subnet in subnets:
|
|
self.l3_plugin.add_router_interface(
|
|
context, router['id'], {'subnet_id': subnet['id']})
|
|
attached_subnets.append(subnet)
|
|
return router
|
|
except n_exc.BadRequest as e:
|
|
LOG.error("Unable to auto allocate topology for tenant "
|
|
"%(tenant_id)s because of router errors. "
|
|
"Reason: %(reason)s",
|
|
{'tenant_id': tenant_id, 'reason': e})
|
|
router_id = router['id'] if router else None
|
|
self._cleanup(context,
|
|
network_id=subnets[0]['network_id'],
|
|
router_id=router_id, subnets=attached_subnets)
|
|
raise exceptions.AutoAllocationFailure(
|
|
reason=_("Unable to provide external connectivity"))
|
|
except Exception as e:
|
|
router_id = router['id'] if router else None
|
|
raise exceptions.UnknownProvisioningError(
|
|
e, network_id=subnets[0]['network_id'],
|
|
router_id=router_id, subnets=subnets)
|
|
|
|
def _save(self, context, tenant_id, network_id, router_id, subnets):
|
|
"""Save auto-allocated topology, or revert in case of DB errors."""
|
|
try:
|
|
auto_allocate_obj.AutoAllocatedTopology(
|
|
context, project_id=tenant_id, network_id=network_id,
|
|
router_id=router_id).create()
|
|
self.core_plugin.update_network(
|
|
context, network_id,
|
|
{'network': {'admin_state_up': True}})
|
|
except obj_exc.NeutronDbObjectDuplicateEntry:
|
|
LOG.debug("Multiple auto-allocated networks detected for "
|
|
"tenant %s. Attempting clean up for network %s "
|
|
"and router %s.",
|
|
tenant_id, network_id, router_id)
|
|
self._cleanup(
|
|
context, network_id=network_id,
|
|
router_id=router_id, subnets=subnets)
|
|
network_id = self._get_auto_allocated_network(context, tenant_id)
|
|
except Exception as e:
|
|
raise exceptions.UnknownProvisioningError(
|
|
e, network_id=network_id,
|
|
router_id=router_id, subnets=subnets)
|
|
return network_id
|
|
|
|
def _cleanup(self, context, network_id=None, router_id=None, subnets=None):
|
|
"""Clean up auto allocated resources."""
|
|
# Concurrent attempts to delete the topology may interleave and
|
|
# cause some operations to fail with NotFound exceptions. Rather
|
|
# than fail partially, the exceptions should be ignored and the
|
|
# cleanup should proceed uninterrupted.
|
|
if router_id:
|
|
for subnet in subnets or []:
|
|
ignore_notfound(
|
|
self.l3_plugin.remove_router_interface,
|
|
context, router_id, {'subnet_id': subnet['id']})
|
|
ignore_notfound(self.l3_plugin.delete_router, context, router_id)
|
|
|
|
if network_id:
|
|
ignore_notfound(
|
|
self.core_plugin.delete_network, context, network_id)
|
|
|
|
|
|
def ignore_notfound(func, *args, **kwargs):
|
|
"""Call the given function and pass if a `NotFound` exception is raised."""
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except n_exc.NotFound:
|
|
pass
|