Files
neutron/neutron/api/rpc/handlers/dhcp_rpc.py
Kevin Benton b672c26cb4 Add provisioning blocks to status ACTIVE transition
Sometimes an object requires multiple disjoint actors to complete
a set of tasks before the status of the object should be transitioned
to ACTIVE. The main example of this is when a port is being created.
The L2 agent has to do its business to wire up the VIF, but at the same
time the DHCP agent has to setup the DHCP reservation. This led to
Nova booting the VM when the L2 agent was done even though the DHCP
agent may have been nowhere near ready.

This patch introduces a provisioning blocks mechansim that allows the
entities to be tracked that need to be involved to make a transition
to ACTIVE happen. See the devref in the dependent patch for a high-level
view of how this works.

The ML2 code is updated to use this new mechanism to prevent updating
the port status to ACTIVE without both the DHCP agent and L2 agent
reporting that the port is ready.

The DHCP RPC API required a version bump to allow the port ready
notification.

This also adds a devref doc for the provisioning_blocks
module with a high-level overview of how it works in addition
to a detailed description of how it is used specifically with
ML2, the L2 agents, and the DHCP agents.

Closes-Bug: #1453350
Change-Id: Id85ff6de1a14a550ab50baf4f79d3130af3680c8
2016-05-11 11:03:09 -07:00

237 lines
10 KiB
Python

# Copyright (c) 2012 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.
import copy
import itertools
import operator
from neutron_lib import constants
from neutron_lib import exceptions
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log as logging
import oslo_messaging
from oslo_utils import excutils
from neutron._i18n import _, _LW
from neutron.callbacks import resources
from neutron.common import constants as n_const
from neutron.common import exceptions as n_exc
from neutron.common import utils
from neutron.db import api as db_api
from neutron.db import provisioning_blocks
from neutron.extensions import portbindings
from neutron import manager
from neutron.plugins.common import utils as p_utils
from neutron.quota import resource_registry
LOG = logging.getLogger(__name__)
class DhcpRpcCallback(object):
"""DHCP agent RPC callback in plugin implementations.
This class implements the server side of an rpc interface. The client
side of this interface can be found in
neutron.agent.dhcp.agent.DhcpPluginApi. For more information about
changing rpc interfaces, see doc/source/devref/rpc_api.rst.
"""
# API version history:
# 1.0 - Initial version.
# 1.1 - Added get_active_networks_info, create_dhcp_port,
# and update_dhcp_port methods.
# 1.2 - Removed get_dhcp_port. When removing a method (Making a
# backwards incompatible change) you would normally bump the
# major version. However, since the method was unused in the
# RPC client for many releases, it should be OK to bump the
# minor release instead and claim RPC compatibility with the
# last few client versions.
# 1.3 - Removed release_port_fixed_ip. It's not used by reference DHCP
# agent since Juno, so similar rationale for not bumping the
# major version as above applies here too.
# 1.4 - Removed update_lease_expiration. It's not used by reference
# DHCP agent since Juno, so similar rationale for not bumping the
# major version as above applies here too.
# 1.5 - Added dhcp_ready_on_ports.
target = oslo_messaging.Target(
namespace=n_const.RPC_NAMESPACE_DHCP_PLUGIN,
version='1.5')
def _get_active_networks(self, context, **kwargs):
"""Retrieve and return a list of the active networks."""
host = kwargs.get('host')
plugin = manager.NeutronManager.get_plugin()
if utils.is_extension_supported(
plugin, constants.DHCP_AGENT_SCHEDULER_EXT_ALIAS):
if cfg.CONF.network_auto_schedule:
plugin.auto_schedule_networks(context, host)
nets = plugin.list_active_networks_on_active_dhcp_agent(
context, host)
else:
filters = dict(admin_state_up=[True])
nets = plugin.get_networks(context, filters=filters)
return nets
def _port_action(self, plugin, context, port, action):
"""Perform port operations taking care of concurrency issues."""
try:
if action == 'create_port':
return p_utils.create_port(plugin, context, port)
elif action == 'update_port':
return plugin.update_port(context, port['id'], port)
else:
msg = _('Unrecognized action')
raise exceptions.Invalid(message=msg)
except (db_exc.DBError,
exceptions.NetworkNotFound,
exceptions.SubnetNotFound,
exceptions.IpAddressGenerationFailure) as e:
with excutils.save_and_reraise_exception(reraise=False) as ctxt:
if isinstance(e, exceptions.IpAddressGenerationFailure):
# Check if the subnet still exists and if it does not,
# this is the reason why the ip address generation failed.
# In any other unlikely event re-raise
try:
subnet_id = port['port']['fixed_ips'][0]['subnet_id']
plugin.get_subnet(context, subnet_id)
except exceptions.SubnetNotFound:
pass
else:
ctxt.reraise = True
net_id = port['port']['network_id']
LOG.warning(_LW("Action %(action)s for network %(net_id)s "
"could not complete successfully: %(reason)s"),
{"action": action, "net_id": net_id, 'reason': e})
def get_active_networks(self, context, **kwargs):
"""Retrieve and return a list of the active network ids."""
# NOTE(arosen): This method is no longer used by the DHCP agent but is
# left so that neutron-dhcp-agents will still continue to work if
# neutron-server is upgraded and not the agent.
host = kwargs.get('host')
LOG.debug('get_active_networks requested from %s', host)
nets = self._get_active_networks(context, **kwargs)
return [net['id'] for net in nets]
def _group_by_network_id(self, res):
grouped = {}
keyfunc = operator.itemgetter('network_id')
for net_id, values in itertools.groupby(sorted(res, key=keyfunc),
keyfunc):
grouped[net_id] = list(values)
return grouped
def get_active_networks_info(self, context, **kwargs):
"""Returns all the networks/subnets/ports in system."""
host = kwargs.get('host')
LOG.debug('get_active_networks_info from %s', host)
networks = self._get_active_networks(context, **kwargs)
plugin = manager.NeutronManager.get_plugin()
filters = {'network_id': [network['id'] for network in networks]}
ports = plugin.get_ports(context, filters=filters)
filters['enable_dhcp'] = [True]
subnets = plugin.get_subnets(context, filters=filters)
grouped_subnets = self._group_by_network_id(subnets)
grouped_ports = self._group_by_network_id(ports)
for network in networks:
network['subnets'] = grouped_subnets.get(network['id'], [])
network['ports'] = grouped_ports.get(network['id'], [])
return networks
def get_network_info(self, context, **kwargs):
"""Retrieve and return extended information about a network."""
network_id = kwargs.get('network_id')
host = kwargs.get('host')
LOG.debug('Network %(network_id)s requested from '
'%(host)s', {'network_id': network_id,
'host': host})
plugin = manager.NeutronManager.get_plugin()
try:
network = plugin.get_network(context, network_id)
except exceptions.NetworkNotFound:
LOG.debug("Network %s could not be found, it might have "
"been deleted concurrently.", network_id)
return
filters = dict(network_id=[network_id])
network['subnets'] = plugin.get_subnets(context, filters=filters)
network['ports'] = plugin.get_ports(context, filters=filters)
return network
@db_api.retry_db_errors
def release_dhcp_port(self, context, **kwargs):
"""Release the port currently being used by a DHCP agent."""
host = kwargs.get('host')
network_id = kwargs.get('network_id')
device_id = kwargs.get('device_id')
LOG.debug('DHCP port deletion for %(network_id)s request from '
'%(host)s',
{'network_id': network_id, 'host': host})
plugin = manager.NeutronManager.get_plugin()
plugin.delete_ports_by_device_id(context, device_id, network_id)
@db_api.retry_db_errors
@resource_registry.mark_resources_dirty
def create_dhcp_port(self, context, **kwargs):
"""Create and return dhcp port information.
If an expected failure occurs, a None port is returned.
"""
host = kwargs.get('host')
# Note(pbondar): Create deep copy of port to prevent operating
# on changed dict if RetryRequest is raised
port = copy.deepcopy(kwargs.get('port'))
LOG.debug('Create dhcp port %(port)s '
'from %(host)s.',
{'port': port,
'host': host})
port['port']['device_owner'] = constants.DEVICE_OWNER_DHCP
port['port'][portbindings.HOST_ID] = host
if 'mac_address' not in port['port']:
port['port']['mac_address'] = constants.ATTR_NOT_SPECIFIED
plugin = manager.NeutronManager.get_plugin()
return self._port_action(plugin, context, port, 'create_port')
@db_api.retry_db_errors
def update_dhcp_port(self, context, **kwargs):
"""Update the dhcp port."""
host = kwargs.get('host')
port = kwargs.get('port')
port['id'] = kwargs.get('port_id')
port['port'][portbindings.HOST_ID] = host
plugin = manager.NeutronManager.get_plugin()
old_port = plugin.get_port(context, port['id'])
if (old_port['device_id'] != n_const.DEVICE_ID_RESERVED_DHCP_PORT
and old_port['device_id'] !=
utils.get_dhcp_agent_device_id(port['port']['network_id'], host)):
raise n_exc.DhcpPortInUse(port_id=port['id'])
LOG.debug('Update dhcp port %(port)s '
'from %(host)s.',
{'port': port,
'host': host})
return self._port_action(plugin, context, port, 'update_port')
def dhcp_ready_on_ports(self, context, port_ids):
for port_id in port_ids:
provisioning_blocks.provisioning_complete(
context, port_id, resources.PORT,
provisioning_blocks.DHCP_ENTITY)