f8a22c7d4a
The goal of this patch is to avoid the connection disruption during the live-migration using OVS. Since [1], when a port is migrated, both the source and the destination hosts are added to the profile binding information. Initially, the source host binding is activated and the destination is deactivated. When the port is created in the destination host (created by Nova), the port was not configured because the binding was not activated. The binding (that means, all the OpenFlow rules) was done when Nova sent the port activation. That happend when the VM was already running in the destination host. If the OVS agent was loaded, the port was bound seconds later to the port activation. Instead, this patch enables the OpenFlow rule creation in the destination host when the port is created. Another problem are the "neutron-vif-plugged" events sent by Neutron to Nova to inform about the port binding. Nova is expecting one single event informing about the destination port binding. At this moment, Nova considers the port is bound and ready to transmit data. Several triggers were firing expectedly this event: - When the port binding was updated, the port is set to down and then up again, forcing this event. - When the port binding was updated, first the binding is deleted and then updated with the new information. That triggers in the source host to set the port down and the up again, sending the event. This patch removes those events, sending the "neutron-vif-plugged" event only when the port is bound to the destination host (and as commented before, this is happening now regardless of the binding activation status). This feature depends on [2]. If this Nova patch is not in place, Nova will never plug the port in the destination host and Neutron won't be able to send the vif-plugged event to Nova to finish the live-migration process. Because from Neutron cannot query Nova to know if this patch is in place, a new temporary configuration option has been created to enable this feature. The default value will be "False"; that means Neutron will behave as before. [1]https://bugs.launchpad.net/neutron/+bug/1580880 [2]https://review.opendev.org/c/openstack/nova/+/767368 Closes-Bug: #1901707 Change-Id: Iee323943ac66e566e5a5e92de1861832e86fc7fc
178 lines
8.0 KiB
Python
178 lines
8.0 KiB
Python
# Copyright 2016 Mirantis, Inc. 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.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 oslo_log import log as logging
|
|
|
|
from neutron._i18n import _
|
|
from neutron.db import models_v2
|
|
from neutron.objects import provisioning_blocks as pb_obj
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
PROVISIONING_COMPLETE = 'provisioning_complete'
|
|
# identifiers for the various entities that participate in provisioning
|
|
DHCP_ENTITY = 'DHCP'
|
|
L2_AGENT_ENTITY = 'L2'
|
|
|
|
# TODO(sshank): Change to object later on when complete integration of Port
|
|
# OVO is complete. Currently 'extend_port_dict' in ext_test fails when changed
|
|
# to OVO here.
|
|
_RESOURCE_TO_MODEL_MAP = {resources.PORT: models_v2.Port}
|
|
|
|
|
|
def add_model_for_resource(resource, model):
|
|
"""Adds a mapping between a callback resource and a DB model."""
|
|
_RESOURCE_TO_MODEL_MAP[resource] = model
|
|
|
|
|
|
@db_api.retry_if_session_inactive()
|
|
def add_provisioning_component(context, object_id, object_type, entity):
|
|
"""Adds a provisioning block by an entity to a given object.
|
|
|
|
Adds a provisioning block to the DB for object_id with an identifier
|
|
of the entity that is doing the provisioning. While an object has these
|
|
provisioning blocks present, this module will not emit any callback events
|
|
indicating that provisioning has completed. Any logic that depends on
|
|
multiple disjoint components may use these blocks and subscribe to the
|
|
PROVISIONING_COMPLETE event to know when all components have completed.
|
|
|
|
:param context: neutron api request context
|
|
:param object_id: ID of object that has been provisioned
|
|
:param object_type: callback resource type of the object
|
|
:param entity: The entity that has provisioned the object
|
|
"""
|
|
log_dict = {'entity': entity, 'oid': object_id, 'otype': object_type}
|
|
# we get an object's ID, so we need to convert that into a standard attr id
|
|
standard_attr_id = _get_standard_attr_id(context, object_id, object_type)
|
|
if not standard_attr_id:
|
|
return
|
|
if pb_obj.ProvisioningBlock.objects_exist(
|
|
context, standard_attr_id=standard_attr_id, entity=entity):
|
|
# an entry could be leftover from a previous transition that hasn't
|
|
# yet been provisioned. (e.g. multiple updates in a short period)
|
|
LOG.debug("Ignored duplicate provisioning block setup for %(otype)s "
|
|
"%(oid)s by entity %(entity)s.", log_dict)
|
|
return
|
|
pb_obj.ProvisioningBlock(
|
|
context, standard_attr_id=standard_attr_id, entity=entity).create()
|
|
LOG.debug("Transition to ACTIVE for %(otype)s object %(oid)s "
|
|
"will not be triggered until provisioned by entity %(entity)s.",
|
|
log_dict)
|
|
|
|
|
|
@db_api.retry_if_session_inactive()
|
|
def remove_provisioning_component(context, object_id, object_type, entity,
|
|
standard_attr_id=None):
|
|
"""Remove a provisioning block for an object without triggering a callback.
|
|
|
|
Removes a provisioning block without triggering a callback. A user of this
|
|
module should call this when a block is no longer correct. If the block has
|
|
been satisfied, the 'provisioning_complete' method should be called.
|
|
|
|
:param context: neutron api request context
|
|
:param object_id: ID of object that has been provisioned
|
|
:param object_type: callback resource type of the object
|
|
:param entity: The entity that has provisioned the object
|
|
:param standard_attr_id: Optional ID to pass to the function to avoid the
|
|
extra DB lookup to translate the object_id into
|
|
the standard_attr_id.
|
|
:return: boolean indicating whether or not a record was deleted
|
|
"""
|
|
standard_attr_id = standard_attr_id or _get_standard_attr_id(
|
|
context, object_id, object_type)
|
|
if not standard_attr_id:
|
|
return False
|
|
if pb_obj.ProvisioningBlock.delete_objects(
|
|
context, standard_attr_id=standard_attr_id, entity=entity):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
@db_api.retry_if_session_inactive()
|
|
def provisioning_complete(context, object_id, object_type, entity):
|
|
"""Mark that the provisioning for object_id has been completed by entity.
|
|
|
|
Marks that an entity has finished provisioning an object. If there are
|
|
no remaining provisioning components, a callback will be triggered
|
|
indicating that provisioning has been completed for the object. Subscribers
|
|
to this callback must be idempotent because it may be called multiple
|
|
times in high availability deployments.
|
|
|
|
:param context: neutron api request context
|
|
:param object_id: ID of object that has been provisioned
|
|
:param object_type: callback resource type of the object
|
|
:param entity: The entity that has provisioned the object
|
|
"""
|
|
log_dict = {'oid': object_id, 'entity': entity, 'otype': object_type}
|
|
# this can't be called in a transaction to avoid REPEATABLE READ
|
|
# tricking us into thinking there are remaining provisioning components
|
|
if context.session.is_active:
|
|
raise RuntimeError(_("Must not be called in a transaction"))
|
|
standard_attr_id = _get_standard_attr_id(context, object_id,
|
|
object_type)
|
|
if not standard_attr_id:
|
|
return
|
|
if remove_provisioning_component(context, object_id, object_type, entity,
|
|
standard_attr_id):
|
|
LOG.debug("Provisioning for %(otype)s %(oid)s completed by entity "
|
|
"%(entity)s.", log_dict)
|
|
# now with that committed, check if any records are left. if None, emit
|
|
# an event that provisioning is complete.
|
|
if not pb_obj.ProvisioningBlock.objects_exist(
|
|
context, standard_attr_id=standard_attr_id):
|
|
LOG.debug("Provisioning complete for %(otype)s %(oid)s triggered by "
|
|
"entity %(entity)s.", log_dict)
|
|
registry.publish(object_type, PROVISIONING_COMPLETE, entity,
|
|
payload=events.DBEventPayload(
|
|
context, resource_id=object_id))
|
|
|
|
|
|
@db_api.retry_if_session_inactive()
|
|
def is_object_blocked(context, object_id, object_type):
|
|
"""Return boolean indicating if object has a provisioning block.
|
|
|
|
:param context: neutron api request context
|
|
:param object_id: ID of object that has been provisioned
|
|
:param object_type: callback resource type of the object
|
|
"""
|
|
standard_attr_id = _get_standard_attr_id(context, object_id,
|
|
object_type)
|
|
if not standard_attr_id:
|
|
# object doesn't exist so it has no blocks
|
|
return False
|
|
return pb_obj.ProvisioningBlock.objects_exist(
|
|
context, standard_attr_id=standard_attr_id)
|
|
|
|
|
|
def _get_standard_attr_id(context, object_id, object_type):
|
|
model = _RESOURCE_TO_MODEL_MAP.get(object_type)
|
|
if not model:
|
|
raise RuntimeError(_("Could not find model for %s. If you are "
|
|
"adding provisioning blocks for a new resource "
|
|
"you must call add_model_for_resource during "
|
|
"initialization for your type.") % object_type)
|
|
obj = (context.session.query(model.standard_attr_id).
|
|
enable_eagerloads(False).
|
|
filter_by(id=object_id).first())
|
|
if not obj:
|
|
# concurrent delete
|
|
LOG.debug("Could not find standard attr ID for object %s.", object_id)
|
|
return
|
|
return obj.standard_attr_id
|