You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
14 KiB

# 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
# 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.callbacks import events
from neutron_lib.callbacks import priority_group
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib import constants as lib_const
from neutron_lib.db import api as db_api
from neutron_lib import exceptions as lib_exc
from neutron_lib.plugins import constants as plugin_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.db import servicetype_db as st_db
from import provider_configuration
from import service_base
LOG = logging.getLogger(__name__)
class DriverController(object):
"""Driver controller for the L3 service plugin.
This component is responsible for dispatching router requests to L3
service providers and for performing the bookkeeping about which
driver is associated with a given router.
This is not intended to be accessed by the drivers or the l3 plugin.
All of the methods are marked as private to reflect this.
def __init__(self, l3_plugin):
self.l3_plugin = l3_plugin
self._stm = st_db.ServiceTypeManager.get_instance()
plugin_constants.L3, _LegacyPlusProviderConfiguration())
def _load_drivers(self):
self.drivers, self.default_provider = (
service_base.load_drivers(plugin_constants.L3, self.l3_plugin))
# store the provider name on each driver to make finding inverse easy
for provider_name, driver in self.drivers.items():
setattr(driver, 'name', provider_name)
def _flavor_plugin(self):
if not hasattr(self, '_flavor_plugin_ref'):
self._flavor_plugin_ref = directory.get_plugin(
return self._flavor_plugin_ref
@registry.receives(resources.ROUTER, [events.BEFORE_CREATE],
def _check_router_request(self, resource, event, trigger, context,
router, **kwargs):
"""Validates that API request is sane (flags compat with flavor)."""
drv = self._get_provider_for_create(context, router)
_ensure_driver_supports_request(drv, router)
@registry.receives(resources.ROUTER, [events.PRECOMMIT_CREATE],
def _set_router_provider(self, resource, event, trigger, context, router,
router_db, **kwargs):
"""Associates a router with a service provider.
Association is done by flavor_id if it's specified, otherwise it will
fallback to determining which loaded driver supports the ha/distributed
attributes associated with the router.
if _flavor_specified(router):
router_db.flavor_id = router['flavor_id']
drv = self._get_provider_for_create(context, router)
self._stm.add_resource_association(context, plugin_constants.L3,, router['id'])
trigger, payload=events.DBEventPayload(
context, request_body=router, states=(router_db,),
metadata={'old_driver': None, 'new_driver': drv},
@registry.receives(resources.ROUTER, [events.PRECOMMIT_DELETE],
def _clear_router_provider(self, resource, event, trigger, context,
router_id, **kwargs):
"""Remove the association between a router and a service provider."""
drv = self.get_provider_for_router(context, router_id)
trigger, payload=events.DBEventPayload(
metadata={'old_driver': drv, 'new_driver': None},
self._stm.del_resource_associations(context, [router_id])
@registry.receives(resources.ROUTER, [events.PRECOMMIT_UPDATE],
def _update_router_provider(self, resource, event, trigger, payload=None):
"""Handle transition between providers.
The provider can currently be changed only by the caller updating
'ha' and/or 'distributed' attributes. If we allow updates of flavor_id
directly in the future those requests will also land here.
drv = self.get_provider_for_router(payload.context,
new_drv = None
if _flavor_specified(payload.request_body):
if (payload.request_body['flavor_id'] !=
# TODO(kevinbenton): this is currently disallowed by the API
# so we shouldn't hit it but this is a placeholder to add
# support later.
raise NotImplementedError()
# the following is to support updating the 'ha' and 'distributed'
# attributes via the API.
_ensure_driver_supports_request(drv, payload.request_body)
except lib_exc.InvalidInput:
# the current driver does not support this request, we need to
# migrate to a new provider. populate the distributed and ha
# flags from the previous state if not in the update so we can
# determine the target provider appropriately.
# NOTE(kevinbenton): if the router is associated with a flavor
# we bail because changing the provider without changing
# the flavor will make things inconsistent. We can probably
# update the flavor automatically in the future.
if payload.states[0]['flavor_id']:
raise lib_exc.InvalidInput(error_message=_(
"Changing the 'ha' and 'distributed' attributes on a "
"router associated with a flavor is not supported"))
if 'distributed' not in payload.request_body:
payload.request_body['distributed'] = (payload.states[0]
if 'ha' not in payload.request_body:
payload.request_body['ha'] = payload.states[0]['ha']
LOG.debug("Get a provider driver handle based on the ha flag: "
"%(ha_flag)s and distributed flag: %(distributed_flag)s",
{'ha_flag': payload.request_body['ha'],
new_drv = self._attrs_to_driver(payload.request_body)
if new_drv:
LOG.debug("Router %(id)s migrating from %(old)s provider to "
"%(new)s provider.", {'id': payload.resource_id,
'old': drv,
'new': new_drv})
_ensure_driver_supports_request(new_drv, payload.request_body)
# TODO(kevinbenton): notify old driver explicitly of driver change
with db_api.CONTEXT_WRITER.using(payload.context):
trigger, payload=payload)
payload.context, [payload.resource_id])
payload.context, plugin_constants.L3,, payload.resource_id, expire_session=False)
trigger, payload=payload)
def get_provider_for_router(self, context, router_id):
"""Return the provider driver handle for a router id."""
driver_name = self._stm.get_provider_names_by_resource_ids(
context, [router_id]).get(router_id)
if not driver_name:
# this is an old router that hasn't been mapped to a provider
# yet so we do this now
router = self.l3_plugin.get_router(context, router_id)
driver = self._attrs_to_driver(router)
driver_name =
with context.session.begin(subtransactions=True):
context, plugin_constants.L3,
driver_name, router_id)
self, payload=events.DBEventPayload(
context, states=(router,),
metadata={'old_driver': None, 'new_driver': driver},
return self.drivers[driver_name]
def _get_provider_for_create(self, context, router):
"""Get provider based on flavor or ha/distributed flags."""
if not _flavor_specified(router):
return self._attrs_to_driver(router)
return self._get_l3_driver_by_flavor(context, router['flavor_id'])
def _get_l3_driver_by_flavor(self, context, flavor_id):
"""Get a provider driver handle for a given flavor_id."""
flavor = self._flavor_plugin.get_flavor(context, flavor_id)
provider = self._flavor_plugin.get_flavor_next_provider(
context, flavor['id'])[0]
# TODO(kevinbenton): the callback framework suppresses the nice errors
# these generate when they fail to lookup. carry them through
driver = self.drivers[provider['provider']]
return driver
def _attrs_to_driver(self, router):
"""Get a provider driver handle based on the ha/distributed flags."""
distributed = _is_distributed(
router.get('distributed', lib_const.ATTR_NOT_SPECIFIED))
ha = _is_ha(router.get('ha', lib_const.ATTR_NOT_SPECIFIED))
drivers = self.drivers.values()
# make sure default is tried before the rest if defined
if self.default_provider:
drivers.insert(0, self.drivers[self.default_provider])
for driver in drivers:
if _is_driver_compatible(distributed, ha, driver):
return driver
raise NotImplementedError(
_("Could not find a service provider that supports "
"distributed=%(d)s and ha=%(h)s") % {'d': distributed, 'h': ha}
def uses_scheduler(self, context, router_id):
"""Returns True if the integrated L3 scheduler should be used."""
return (self.get_provider_for_router(context, router_id).
class _LegacyPlusProviderConfiguration(
def __init__(self):
# loads up ha, dvr, and single_node service providers automatically.
# If an operator has setup explicit values that conflict with these,
# the operator defined values will take priority.
super(_LegacyPlusProviderConfiguration, self).__init__(
for name, driver in (('dvrha', 'dvrha.DvrHaDriver'),
('dvr', 'dvr.DvrDriver'), ('ha', 'ha.HaDriver'),
('single_node', 'single_node.SingleNodeDriver')):
path = '' % driver
self.add_provider({'service_type': plugin_constants.L3,
'name': name, 'driver': path,
'default': False})
except lib_exc.Invalid:
LOG.debug("Could not add L3 provider '%s', it may have "
"already been explicitly defined.", name)
def _is_driver_compatible(distributed, ha, driver):
if not driver.distributed_support.is_compatible(distributed):
return False
if not driver.ha_support.is_compatible(ha):
return False
return True
def _is_distributed(distributed_attr):
if distributed_attr is False:
return False
if distributed_attr == lib_const.ATTR_NOT_SPECIFIED:
return cfg.CONF.router_distributed
return True
def _is_ha(ha_attr):
if ha_attr is False:
return False
if ha_attr == lib_const.ATTR_NOT_SPECIFIED:
return cfg.CONF.l3_ha
return True
def _flavor_specified(router):
return ('flavor_id' in router and
router['flavor_id'] != lib_const.ATTR_NOT_SPECIFIED)
def _ensure_driver_supports_request(drv, router_body):
r = router_body
for key, attr in (('distributed', 'distributed_support'),
('ha', 'ha_support')):
flag = r.get(key)
if flag not in [True, False]:
continue # not specified in body
if not getattr(drv, attr).is_compatible(flag):
raise lib_exc.InvalidInput(error_message=(
_("Provider %(name)s does not support %(key)s=%(flag)s")
% dict(, key=key, flag=flag)))