Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions neutron/cmd/ovn/neutron_ovn_db_sync_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ def post_fork_initialize(self, resource, event, trigger, **kwargs):
def ovn_client(self):
return self._ovn_client

def _set_hash_ring_nodes_offline(self):
"""Don't set hash ring nodes as offline.
def _remove_node_from_hash_ring(self):
"""Don't remove the node from the Hash Ring.

If this method was not overridden, cleanup would be performed when
calling the db sync and running neutron server would mark all the
nodes from the ring as offline.
calling the db sync and running neutron server would remove the
nodes from the Hash Ring.
"""

# Since we are not using the ovn mechanism driver while syncing,
Expand Down
16 changes: 16 additions & 0 deletions neutron/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@
from eventlet.green import subprocess
import netaddr
from neutron_lib.api.definitions import availability_zone as az_def
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import portbindings_extended
from neutron_lib import constants as n_const
from neutron_lib import context as n_context
from neutron_lib.db import api as db_api
from neutron_lib.plugins import utils as plugin_utils
from neutron_lib.services.trunk import constants as trunk_constants
from neutron_lib.utils import helpers
from oslo_config import cfg
Expand Down Expand Up @@ -1046,3 +1049,16 @@ def is_session_active(session):
if not (session.dirty or session.deleted or session.new):
return False
return True


# TODO(slaweq): this should be moved to neutron_lib.plugins.utils module
def is_port_bound(port, log_message=True):
active_binding = plugin_utils.get_port_binding_by_status_and_host(
port.get('port_bindings', []), n_const.ACTIVE)
if not active_binding:
if log_message:
LOG.warning('Binding for port %s was not found.', port)
return False
return active_binding[portbindings_extended.VIF_TYPE] not in (
portbindings.VIF_TYPE_UNBOUND,
portbindings.VIF_TYPE_BINDING_FAILED)
15 changes: 1 addition & 14 deletions neutron/db/l3_dvr_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from neutron_lib.api.definitions import external_net as extnet_apidef
from neutron_lib.api.definitions import l3 as l3_apidef
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import portbindings_extended
from neutron_lib.api.definitions import router_admin_state_down_before_update
from neutron_lib.api import validators
from neutron_lib.callbacks import events
Expand Down Expand Up @@ -70,18 +69,6 @@ def is_admin_state_down_necessary():
return _IS_ADMIN_STATE_DOWN_NECESSARY


# TODO(slaweq): this should be moved to neutron_lib.plugins.utils module
def is_port_bound(port):
active_binding = plugin_utils.get_port_binding_by_status_and_host(
port.get("port_bindings", []), const.ACTIVE)
if not active_binding:
LOG.warning("Binding for port %s was not found.", port)
return False
return active_binding[portbindings_extended.VIF_TYPE] not in [
portbindings.VIF_TYPE_UNBOUND,
portbindings.VIF_TYPE_BINDING_FAILED]


@registry.has_registry_receivers
class DVRResourceOperationHandler(object):
"""Contains callbacks for DVR operations.
Expand Down Expand Up @@ -1426,7 +1413,7 @@ def is_router_distributed(self, context, router_id):

def get_ports_under_dvr_connected_subnet(self, context, subnet_id):
query = dvr_mac_db.get_ports_query_by_subnet_and_ip(context, subnet_id)
ports = [p for p in query.all() if is_port_bound(p)]
ports = [p for p in query.all() if n_utils.is_port_bound(p)]
# TODO(slaweq): if there would be way to pass to neutron-lib only
# list of extensions which actually should be processed, than setting
# process_extensions=True below could avoid that second loop and
Expand Down
17 changes: 13 additions & 4 deletions neutron/db/ovn_hash_ring_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ def remove_nodes_from_host(context, group_name):
CONF.host, group_name)


def remove_node_by_uuid(context, node_uuid):
with db_api.CONTEXT_WRITER.using(context):
context.session.query(ovn_models.OVNHashRing).filter(
ovn_models.OVNHashRing.node_uuid == node_uuid).delete()
LOG.info('Node "%s" removed from the Hash Ring', node_uuid)


def _touch(context, updated_at=None, **filter_args):
if updated_at is None:
updated_at = timeutils.utcnow()
Expand Down Expand Up @@ -96,7 +103,9 @@ def count_offline_nodes(context, interval, group_name):
return query.count()


def set_nodes_from_host_as_offline(context, group_name):
timestamp = datetime.datetime(day=26, month=10, year=1985, hour=9)
_touch(context, updated_at=timestamp, hostname=CONF.host,
group_name=group_name)
@db_api.CONTEXT_READER
def count_nodes_from_host(context, group_name):
query = context.session.query(ovn_models.OVNHashRing).filter(
ovn_models.OVNHashRing.group_name == group_name,
ovn_models.OVNHashRing.hostname == CONF.host)
return query.count()
18 changes: 11 additions & 7 deletions neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,17 +270,17 @@ def subscribe(self):
resources.SECURITY_GROUP_RULE,
events.BEFORE_DELETE)

def _set_hash_ring_nodes_offline(self, *args, **kwargs):
def _remove_node_from_hash_ring(self, *args, **kwargs):
# The node_uuid attribute will be empty for worker types
# that are not added to the Hash Ring and can be skipped
if self.node_uuid is None:
return
admin_context = n_context.get_admin_context()
ovn_hash_ring_db.set_nodes_from_host_as_offline(
admin_context, self.hash_ring_group)
LOG.info('Hash Ring nodes from host "%s" marked as offline',
cfg.CONF.host)
ovn_hash_ring_db.remove_node_by_uuid(
admin_context, self.node_uuid)

def pre_fork_initialize(self, resource, event, trigger, payload=None):
"""Pre-initialize the ML2/OVN driver."""
atexit.register(self._set_hash_ring_nodes_offline)
signal.signal(signal.SIGTERM, self._set_hash_ring_nodes_offline)
ovn_utils.create_neutron_pg_drop()

@staticmethod
Expand All @@ -298,6 +298,10 @@ def _setup_hash_ring(self):
thread for this host. Subsequently workers just need to register
themselves to the hash ring.
"""
# Attempt to remove the node from the ring when the worker stops
atexit.register(self._remove_node_from_hash_ring)
signal.signal(signal.SIGTERM, self._remove_node_from_hash_ring)

admin_context = n_context.get_admin_context()
if not self._hash_ring_probe_event.is_set():
# Clear existing entries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from neutron.db import segments_db
from neutron.objects import router as router_obj
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_db_sync
from neutron import service
from neutron.services.logapi.drivers.ovn import driver as log_driver


Expand Down Expand Up @@ -1067,3 +1068,20 @@ def touch_hash_ring_nodes(self):
# here because we want the maintenance tasks from each instance to
# execute this task.
hash_ring_db.touch_nodes_from_host(self.ctx, self._group)

# Check the number of the nodes in the ring and log a message in
# case they are out of sync. See LP #2024205 for more information
# on this issue.
api_workers = service._get_api_workers()
num_nodes = hash_ring_db.count_nodes_from_host(self.ctx, self._group)

if num_nodes > api_workers:
LOG.critical(
'The number of nodes in the Hash Ring (%d) is higher than '
'the number of API workers (%d) for host "%s". Something is '
'not right and OVSDB events could be missed because of this. '
'Please check the status of the Neutron processes, this can '
'happen when the API workers are killed and restarted. '
'Restarting the service should fix the issue, see LP '
'#2024205 for more information.',
num_nodes, api_workers, cfg.CONF.host)
19 changes: 18 additions & 1 deletion neutron/services/trunk/drivers/ovn/trunk_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
from oslo_log import log

from neutron.common.ovn import constants as ovn_const
from neutron.common import utils as n_utils
from neutron.db import db_base_plugin_common
from neutron.db import ovn_revision_numbers_db as db_rev
from neutron.objects import ports as port_obj
from neutron.services.trunk.drivers import base as trunk_base
from neutron.services.trunk import exceptions as trunk_exc


SUPPORTED_INTERFACES = (
Expand Down Expand Up @@ -162,6 +164,10 @@ def _unset_binding_profile(self, context, subport, ovn_txn):
LOG.debug("Done unsetting parent for subport %s", subport.port_id)
return db_port

@staticmethod
def _is_port_bound(port):
return n_utils.is_port_bound(port, log_message=False)

def trunk_updated(self, trunk):
# Check if parent port is handled by OVN.
if not self.plugin_driver.nb_ovn.lookup('Logical_Switch_Port',
Expand Down Expand Up @@ -208,6 +214,16 @@ def trunk_event(self, resource, event, trunk_plugin, payload):
self.trunk_updated(payload.states[0])
elif event == events.AFTER_DELETE:
self.trunk_deleted(payload.states[0])
elif event == events.PRECOMMIT_CREATE:
trunk = payload.desired_state
parent_port = trunk.db_obj.port
if self._is_port_bound(parent_port):
raise trunk_exc.ParentPortInUse(port_id=parent_port.id)
elif event == events.PRECOMMIT_DELETE:
trunk = payload.states[0]
parent_port = payload.states[1]
if self._is_port_bound(parent_port):
raise trunk_exc.TrunkInUse(trunk_id=trunk.id)

def subport_event(self, resource, event, trunk_plugin, payload):
if event == events.AFTER_CREATE:
Expand All @@ -233,7 +249,8 @@ def register(self, resource, event, trigger, payload=None):
resource, event, trigger, payload=payload)
self._handler = OVNTrunkHandler(self.plugin_driver)
for _event in (events.AFTER_CREATE, events.AFTER_UPDATE,
events.AFTER_DELETE):
events.AFTER_DELETE, events.PRECOMMIT_CREATE,
events.PRECOMMIT_DELETE):
registry.subscribe(self._handler.trunk_event,
resources.TRUNK,
_event)
Expand Down
5 changes: 3 additions & 2 deletions neutron/services/trunk/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ def delete_trunk(self, context, trunk_id):
trunk = self._get_trunk(context, trunk_id)
rules.trunk_can_be_managed(context, trunk)
trunk_port_validator = rules.TrunkPortValidator(trunk.port_id)
parent_port = trunk.db_obj.port
if trunk_port_validator.can_be_trunked_or_untrunked(context):
# NOTE(status_police): when a trunk is deleted, the logical
# object disappears from the datastore, therefore there is no
Expand All @@ -307,7 +308,7 @@ def delete_trunk(self, context, trunk_id):
'deleting trunk port %s: %s', trunk_id,
str(e))
payload = events.DBEventPayload(context, resource_id=trunk_id,
states=(trunk,))
states=(trunk, parent_port))
registry.publish(resources.TRUNK, events.PRECOMMIT_DELETE,
self, payload=payload)
else:
Expand All @@ -317,7 +318,7 @@ def delete_trunk(self, context, trunk_id):
registry.publish(resources.TRUNK, events.AFTER_DELETE, self,
payload=events.DBEventPayload(
context, resource_id=trunk_id,
states=(trunk,)))
states=(trunk, parent_port)))

@db_base_plugin_common.convert_result_to_dict
def add_subports(self, context, trunk_id, subports):
Expand Down
2 changes: 1 addition & 1 deletion neutron/tests/functional/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ def trigger(self):
# NOTE(ralonsoh): do not access to the DB at exit when the SQL
# connection is already closed, to avoid useless exception messages.
mock.patch.object(
self.mech_driver, '_set_hash_ring_nodes_offline').start()
self.mech_driver, '_remove_node_from_hash_ring').start()
self.mech_driver.pre_fork_initialize(
mock.ANY, mock.ANY, trigger_cls.trigger)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import contextlib

from neutron_lib.api.definitions import portbindings
from neutron_lib.callbacks import exceptions as n_exc
from neutron_lib import constants as n_consts
from neutron_lib.db import api as db_api
from neutron_lib.plugins import utils
Expand Down Expand Up @@ -113,6 +114,25 @@ def test_trunk_create_with_subports(self):
with self.trunk([subport]) as trunk:
self._verify_trunk_info(trunk, has_items=True)

def test_trunk_create_parent_port_bound(self):
with self.network() as network:
with self.subnet(network=network) as subnet:
with self.port(subnet=subnet) as parent_port:
pb = port_obj.PortBinding.get_objects(
self.context, port_id=parent_port['port']['id'])
port_obj.PortBinding.update_object(
self.context, {'vif_type': portbindings.VIF_TYPE_OVS},
port_id=pb[0].port_id, host=pb[0].host)
tenant_id = uuidutils.generate_uuid()
trunk = {'trunk': {
'port_id': parent_port['port']['id'],
'tenant_id': tenant_id, 'project_id': tenant_id,
'admin_state_up': True,
'name': 'trunk', 'sub_ports': []}}
self.assertRaises(n_exc.CallbackFailure,
self.trunk_plugin.create_trunk,
self.context, trunk)

def test_subport_add(self):
with self.subport() as subport:
with self.trunk() as trunk:
Expand Down Expand Up @@ -147,3 +167,14 @@ def test_trunk_delete(self):
with self.trunk() as trunk:
self.trunk_plugin.delete_trunk(self.context, trunk['id'])
self._verify_trunk_info({}, has_items=False)

def test_trunk_delete_parent_port_bound(self):
with self.trunk() as trunk:
bp = port_obj.PortBinding.get_objects(
self.context, port_id=trunk['port_id'])
port_obj.PortBinding.update_object(
self.context, {'vif_type': portbindings.VIF_TYPE_OVS},
port_id=bp[0].port_id, host=bp[0].host)
self.assertRaises(n_exc.CallbackFailure,
self.trunk_plugin.delete_trunk,
self.context, trunk['id'])
3 changes: 2 additions & 1 deletion neutron/tests/unit/db/test_l3_dvr_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from neutron_lib.plugins import utils as plugin_utils
from oslo_utils import uuidutils

from neutron.common import utils as n_utils
from neutron.db import agents_db
from neutron.db import l3_dvr_db
from neutron.db import l3_dvrscheduler_db
Expand Down Expand Up @@ -1521,7 +1522,7 @@ def test_is_router_distributed(self):
self.assertTrue(
self.mixin.is_router_distributed(self.ctx, router_id))

@mock.patch.object(l3_dvr_db, "is_port_bound")
@mock.patch.object(n_utils, 'is_port_bound')
def test_get_ports_under_dvr_connected_subnet(self, is_port_bound_mock):
router_dict = {'name': 'test_router', 'admin_state_up': True,
'distributed': True}
Expand Down
10 changes: 6 additions & 4 deletions neutron/tests/unit/db/test_ovn_hash_ring_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,16 +270,18 @@ def test_count_offline_nodes(self):
self.assertEqual(0, ovn_hash_ring_db.count_offline_nodes(
self.admin_ctx, interval=60, group_name=HASH_RING_TEST_GROUP))

def test_set_nodes_from_host_as_offline(self):
def test_remove_node_by_uuid(self):
self._add_nodes_and_assert_exists(count=3)

active_nodes = ovn_hash_ring_db.get_active_nodes(
self.admin_ctx, interval=60, group_name=HASH_RING_TEST_GROUP)
self.assertEqual(3, len(active_nodes))

ovn_hash_ring_db.set_nodes_from_host_as_offline(
self.admin_ctx, HASH_RING_TEST_GROUP)
node_to_remove = active_nodes[0].node_uuid
ovn_hash_ring_db.remove_node_by_uuid(
self.admin_ctx, node_to_remove)

active_nodes = ovn_hash_ring_db.get_active_nodes(
self.admin_ctx, interval=60, group_name=HASH_RING_TEST_GROUP)
self.assertEqual(0, len(active_nodes))
self.assertEqual(2, len(active_nodes))
self.assertNotIn(node_to_remove, [n.node_uuid for n in active_nodes])
3 changes: 2 additions & 1 deletion neutron/tests/unit/services/trunk/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ def _test_trunk_delete_notify(self, event):
resources.TRUNK, event, self.trunk_plugin, payload=mock.ANY)
payload = callback.mock_calls[0][2]['payload']
self.assertEqual(self.context, payload.context)
self.assertEqual(trunk_obj, payload.latest_state)
self.assertEqual(trunk_obj, payload.states[0])
self.assertEqual(parent_port['port']['id'], payload.states[1].id)
self.assertEqual(trunk['id'], payload.resource_id)

def test_delete_trunk_notify_after_delete(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fixes:
- |
Now the ML2/OVN trunk driver prevents a trunk creation if the parent port
is already bound. In the same way, if a parent port being used in a trunk
is bound, the trunk cannot be deleted.