[OVN][Placement] Placement init config with Chassis create event

Now the initial Placement configuration per Chassis is build when
a Chassis create event is received. That will happen when:
* The Neutron API is restarted.
* When a new Chassis register is created.

Closes-Bug: #2102090
Change-Id: I9aa8accdcead079fe2e713fe569f8bff7403be92
This commit is contained in:
Rodolfo Alonso Hernandez
2025-03-19 12:24:17 +00:00
parent 89053ab55f
commit 5841cacecb
5 changed files with 65 additions and 20 deletions

View File

@@ -325,13 +325,11 @@ by commas.
This information is retrieved from the OVN SB database during the Neutron This information is retrieved from the OVN SB database during the Neutron
server initialization and when the "Chassis" registers are updated. server initialization and when the "Chassis" registers are updated.
During the Neutron server initialization, a ``MaintenanceWorker`` thread will The initial Placement configuration is retrieved when the Neutron API receives
call ``OvnSbSynchronizer.do_sync``, that will call a "Chassis" create event, that happens when the IDL is connected to the
``OVNClientPlacementExtension.read_initial_chassis_config``. This method lists database server. When a creation event is received, the Neutron API reads the
all chassis and builds the resource provider information needed by Placement. configuration, builds a ``PlacementState`` instance and sends it to the
This information is stored in the "Chassis" registers, in Placement API.
"external_ids:ovn-cms-options", with the same format as retrieved from the
local "Open_vSwitch" registers from each chassis.
The second method to update the Placement information is when a "Chassis" The second method to update the Placement information is when a "Chassis"
registers is updated. The ``OVNClientPlacementExtension`` extension registers registers is updated. The ``OVNClientPlacementExtension`` extension registers

View File

@@ -125,16 +125,19 @@ class ChassisBandwidthConfigEvent(row_event.RowEvent):
if self._driver._post_fork_event.is_set(): if self._driver._post_fork_event.is_set():
return self._driver._ovn_client.placement_extension return self._driver._ovn_client.placement_extension
@property
def placement_extension_enabled(self):
return self.placement_extension and self.placement_extension.enabled
def match_fn(self, event, row, old=None): def match_fn(self, event, row, old=None):
# If the OVNMechanismDriver OVNClient has not been instantiated, the
# event is skipped. All chassis configurations are read during the
# OVN placement extension initialization.
if (not self.placement_extension or
not self.placement_extension.enabled):
return False
if event == self.ROW_CREATE: if event == self.ROW_CREATE:
return True return True
if event == self.ROW_UPDATE and old and hasattr(old, 'other_config'):
# If the OVNMechanismDriver OVNClient has not been instantiated, the
# update event is skipped.
if not self.placement_extension_enabled:
return False
if old and hasattr(old, 'other_config'):
row_bw = _parse_ovn_cms_options(row) row_bw = _parse_ovn_cms_options(row)
old_bw = _parse_ovn_cms_options(old) old_bw = _parse_ovn_cms_options(old)
if row_bw != old_bw: if row_bw != old_bw:
@@ -142,6 +145,14 @@ class ChassisBandwidthConfigEvent(row_event.RowEvent):
return False return False
def run(self, event, row, old): def run(self, event, row, old):
if event == self.ROW_CREATE:
# It is possible that a Chassis create event is received before
# the OVNMechanismDriver OVNClient has been instantiated. Wait for
# it and check the Placement extension.
self._driver._post_fork_event.wait()
if not self.placement_extension_enabled:
return
name2uuid = self.placement_extension.name2uuid() name2uuid = self.placement_extension.name2uuid()
state = self.placement_extension.build_placement_state(row, name2uuid) state = self.placement_extension.build_placement_state(row, name2uuid)
if not state: if not state:
@@ -149,8 +160,8 @@ class ChassisBandwidthConfigEvent(row_event.RowEvent):
_send_deferred_batch(state) _send_deferred_batch(state)
ch_config = dict_chassis_config(state) ch_config = dict_chassis_config(state)
LOG.debug('OVN chassis %(chassis)s Placement configuration modified: ' LOG.info('OVN chassis %(chassis)s Placement configuration modified: '
'%(config)s', {'chassis': row.name, 'config': ch_config}) '%(config)s', {'chassis': row.name, 'config': ch_config})
@common_utils.SingletonDecorator @common_utils.SingletonDecorator
@@ -228,7 +239,7 @@ class OVNClientPlacementExtension:
msg = ', '.join(['Chassis {}: {}'.format( msg = ', '.join(['Chassis {}: {}'.format(
name, dict_chassis_config(state)) name, dict_chassis_config(state))
for (name, state) in chassis.items()]) or '(no info)' for (name, state) in chassis.items()]) or '(no info)'
LOG.debug('OVN chassis Placement initial configuration: %s', msg) LOG.info('OVN chassis Placement initial configuration: %s', msg)
return chassis return chassis
def name2uuid(self, name=None): def name2uuid(self, name=None):

View File

@@ -1351,9 +1351,6 @@ class OvnSbSynchronizer(OvnDbSynchronizer):
self.sync_hostname_and_physical_networks(ctx) self.sync_hostname_and_physical_networks(ctx)
if utils.is_ovn_l3(self.l3_plugin): if utils.is_ovn_l3(self.l3_plugin):
self.l3_plugin.schedule_unhosted_gateways() self.l3_plugin.schedule_unhosted_gateways()
# NOTE(ralonsoh): this could be called using a resource event.
self.ovn_driver._ovn_client.placement_extension.\
read_initial_chassis_config()
LOG.debug("OVN-Southbound DB sync process completed @ %s", LOG.debug("OVN-Southbound DB sync process completed @ %s",
str(datetime.now())) str(datetime.now()))

View File

@@ -223,3 +223,34 @@ class TestOVNClientPlacementExtension(base.TestOVNFunctionalBase):
common_utils.wait_until_true, common_utils.wait_until_true,
lambda: mock_send_placement.called, lambda: mock_send_placement.called,
timeout=2) timeout=2)
@mock.patch.object(placement_extension, '_send_deferred_batch')
def test_chassis_bandwidth_initial_config_event(self, mock_send_placement):
ch_name = uuidutils.generate_uuid()
rp_uuid = uuidutils.generate_uuid()
ch_event = test_ovsdb_monitor.WaitForChassisPrivateCreateEvent(ch_name)
self.mech_driver.sb_ovn.idl.notify_handler.watch_event(ch_event)
self.mock_name2uuid.return_value = {'host1': rp_uuid}
self._create_chassis(
'host1', ch_name, physical_nets=['phys1'],
bandwidths='br-provider0:1000:2000',
inventory_defaults='allocation_ratio:1.0;min_unit:2',
hypervisors='br-provider0:host1')
self.assertTrue(ch_event.wait())
common_utils.wait_until_true(lambda: mock_send_placement.called,
timeout=2)
mock_send_placement.assert_called_once()
placement_state = mock_send_placement.call_args[0][0]
device_mappings = {'phys1': ['br-provider0']}
self.assertEqual(placement_state._device_mappings, device_mappings)
hypervisor_rps = {'br-provider0': {'name': 'host1', 'uuid': rp_uuid}}
self.assertEqual(placement_state._hypervisor_rps, hypervisor_rps)
rp_bandwidths = {'br-provider0': {'egress': 1000, 'ingress': 2000}}
self.assertEqual(placement_state._rp_bandwidths, rp_bandwidths)
rp_inventory_defaults = {'allocation_ratio': 1.0, 'min_unit': 2}
self.assertEqual(placement_state._rp_inventory_defaults,
rp_inventory_defaults)

View File

@@ -0,0 +1,8 @@
---
issues:
- |
The ML2/OVN Placement initial configuration is executed now in the Neutron
API process and removed from the maintenance worker; since the migration
to WSGI, now the API and the maintenance worker are different processes.
When an OVN ``Chassis`` creation event is received, the configuration is
read, a ``PlacementState`` object created and sent to the Placement API.