diff --git a/doc/source/admin/config-qos-min-bw.rst b/doc/source/admin/config-qos-min-bw.rst index 9f77bdd3a27..451dc3806bf 100644 --- a/doc/source/admin/config-qos-min-bw.rst +++ b/doc/source/admin/config-qos-min-bw.rst @@ -301,6 +301,25 @@ SR-IOV and OVS agents. This is how the values are registered: Each configuration option defined in "external_ids:ovn-cms-options" is divided by commas. +This information is retrieved from the OVN SB database during the Neutron +server initialization and when the "Chassis" registers are updated. + +During the Neutron server initialization, a ``MaintenanceWorker`` thread will +call ``OvnSbSynchronizer.do_sync``, that will call +``OVNClientPlacementExtension.read_initial_chassis_config``. This method lists +all chassis and builds the resource provider information needed by Placement. +This information is stored in the "Chassis" registers, in +"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" +registers is updated. The ``OVNClientPlacementExtension`` extension registers +an event handler that attends the OVN SB "Chassis" bandwidth configuration +changes. This event handler builds a ``PlacementState`` instance and sends it +to the Placement API. If a new chassis is added or an existing one changes its +resource provider configuration, this event updates it in the Placement +database. + Propagation of resource information ----------------------------------- diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py index 8a055876a0f..eab14543263 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py @@ -100,7 +100,7 @@ def _send_deferred_batch(state): LOG.exception('Placement client call failed: %s', str(deferred)) -def _dict_chassis_config(state): +def dict_chassis_config(state): if state: return {n_const.RP_BANDWIDTHS: state._rp_bandwidths, n_const.RP_INVENTORY_DEFAULTS: state._rp_inventory_defaults, @@ -122,10 +122,11 @@ class ChassisBandwidthConfigEvent(row_event.RowEvent): def run(self, event, row, old): name2uuid = self._placement_extension.name2uuid() state = self._placement_extension.build_placement_state(row, name2uuid) + if not state: + return + _send_deferred_batch(state) - self._placement_extension.add_chassis_config( - row.name, _dict_chassis_config(state)) - ch_config = self._placement_extension.get_chassis_config(row.name) + ch_config = dict_chassis_config(state) LOG.debug('OVN chassis %(chassis)s Placement configuration modified: ' '%(config)s', {'chassis': row.name, 'config': ch_config}) @@ -152,9 +153,7 @@ class OVNClientPlacementExtension(object): self._plugin = None self.uuid_ns = ovn_const.OVN_RP_UUID self.supported_vnic_types = ovn_const.OVN_SUPPORTED_VNIC_TYPES - self._enabled = bool(self.placement_plugin) - self._chassis = {} # Initial config read could take some time. - if not self._enabled: + if not self.enabled: return if not self._config_event: @@ -163,12 +162,9 @@ class OVNClientPlacementExtension(object): self._driver._sb_idl.idl.notify_handler.watch_events( [self._config_event]) except AttributeError: - self._enabled = False - - if not self._enabled: - return - - self._chassis = self._read_initial_chassis_config() + # "sb_idl.idl.notify_handler" is not present in the + # MaintenanceWorker. + pass @property def placement_plugin(self): @@ -177,6 +173,10 @@ class OVNClientPlacementExtension(object): plugins_constants.PLACEMENT_REPORT) return self._placement_plugin + @property + def enabled(self): + return bool(self.placement_plugin) + @property def plugin(self): if self._plugin is None: @@ -190,24 +190,33 @@ class OVNClientPlacementExtension(object): self.plugin.mechanism_manager.mech_drivers['ovn'].obj) return self._ovn_mech_driver - @property - def chassis(self): - return self._chassis - - def _read_initial_chassis_config(self): + def get_chassis_config(self): + """Read all Chassis BW config and returns the Placement states""" chassis = {} name2uuid = self.name2uuid() for ch in self._driver._sb_idl.chassis_list().execute( check_error=True): state = self.build_placement_state(ch, name2uuid) - _send_deferred_batch(state) - config = _dict_chassis_config(state) - if config: - chassis[ch.name] = config + if state: + chassis[ch.name] = state - msg = '\n'.join(['Chassis %s: %s' % (name, config) - for (name, config) in chassis.items()]) - LOG.debug('OVN chassis Placement initial configuration:\n%s', msg) + return chassis + + def read_initial_chassis_config(self): + """Read the Chassis BW configuration and update the Placement API + + This method is called once from the MaintenanceWorker when the Neutron + server starts. + """ + if not self.enabled: + return + + chassis = self.get_chassis_config() + for state in chassis.values(): + _send_deferred_batch(state) + msg = ', '.join(['Chassis %s: %s' % (name, dict_chassis_config(state)) + for (name, state) in chassis.items()]) or '(no info)' + LOG.debug('OVN chassis Placement initial configuration: %s', msg) return chassis def name2uuid(self, name=None): @@ -248,7 +257,9 @@ class OVNClientPlacementExtension(object): device in hypervisor_rps and device in bridges} # NOTE(ralonsoh): OVN only reports min BW RPs; packet processing RPs - # will be added in a future implementation. + # will be added in a future implementation. If no RP_BANDWIDTHS values + # are present (that means there is no BW information for any interface + # in this host), no "PlacementState" is returned. return placement_report.PlacementState( rp_bandwidths=cms_options[n_const.RP_BANDWIDTHS], rp_inventory_defaults=cms_options[n_const.RP_INVENTORY_DEFAULTS], @@ -260,10 +271,3 @@ class OVNClientPlacementExtension(object): device_mappings=bridge_mappings, supported_vnic_types=self.supported_vnic_types, client=self.placement_plugin._placement_client) - - def get_chassis_config(self, chassis_name): - return self._chassis.get(chassis_name) - - def add_chassis_config(self, chassis_name, config): - if config: - self._chassis[chassis_name] = config diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py index 815e342a142..c73cd3df3b3 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py @@ -80,7 +80,7 @@ class OVNClient(object): # TODO(ralonsoh): handle the OVN client extensions with an ext. manager self._qos_driver = qos_extension.OVNClientQosExtension(driver=self) - self._placement_extension = ( + self.placement_extension = ( placement_extension.OVNClientPlacementExtension(self)) self._ovn_scheduler = l3_ovn_scheduler.get_scheduler() diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py index adcf0c1fd16..f8edb14ef5a 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py @@ -1273,6 +1273,9 @@ class OvnSbSynchronizer(OvnDbSynchronizer): self.sync_hostname_and_physical_networks(ctx) if utils.is_ovn_l3(self.l3_plugin): 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() def sync_hostname_and_physical_networks(self, ctx): LOG.debug('OVN-SB Sync hostname and physical networks started') diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py index 0add9093e17..5e808fe7eaf 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py @@ -66,7 +66,7 @@ class TestOVNClientQosExtension(base.TestOVNFunctionalBase): super().setUp(maintenance_worker=maintenance_worker, service_plugins=service_plugins) self.ovn_client = self.mech_driver._ovn_client - self.placement_ext = self.ovn_client._placement_extension + self.placement_ext = self.ovn_client.placement_extension self.mock_name2uuid = mock.patch.object( self.placement_ext, 'name2uuid').start() self.mock_send_batch = mock.patch.object( @@ -98,12 +98,15 @@ class TestOVNClientQosExtension(base.TestOVNFunctionalBase): 'Chassis', name, ('external_ids', external_ids) ).execute(check_error=True) - def _check_placement_cache(self, expected_chassis): + def _check_placement_config(self, expected_chassis): current_chassis = None def check_chassis(): nonlocal current_chassis - current_chassis = self.placement_ext.chassis + current_chassis = self.placement_ext.get_chassis_config() + current_chassis = { + chassis_name: placement_extension.dict_chassis_config(state) + for chassis_name, state in current_chassis.items()} return current_chassis == expected_chassis try: @@ -126,37 +129,37 @@ class TestOVNClientQosExtension(base.TestOVNFunctionalBase): bandwidths='br-provider0:3000:4000', inventory_defaults='allocation_ratio:3.0;min_unit:1', hypervisors='br-provider0:host2') - self._check_placement_cache({**self.CHASSIS1, **self.CHASSIS2}) + self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2}) self._update_chassis( 'chassis2', bandwidths='br-provider0:5000:6000', inventory_defaults='allocation_ratio:1.1;min_unit:1', hypervisors='br-provider0:host2') - self._check_placement_cache({**self.CHASSIS1, **self.CHASSIS2_B}) + self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2_B}) def test_read_initial_empty_config_and_update(self): self.mock_name2uuid.return_value = {'host1': 'uuid1', 'host2': 'uuid2'} self._create_chassis('host1', 'chassis1', physical_nets=['phys1']) self._create_chassis('host2', 'chassis2', physical_nets=['phys2']) - self._check_placement_cache({**{'chassis1': self.EMPTY_CHASSIS}, - **{'chassis2': self.EMPTY_CHASSIS}}) + self._check_placement_config({**{'chassis1': self.EMPTY_CHASSIS}, + **{'chassis2': self.EMPTY_CHASSIS}}) self._update_chassis( 'chassis1', bandwidths='br-provider0:1000:2000', inventory_defaults='allocation_ratio:1.0;min_unit:2', hypervisors='br-provider0:host1') - self._check_placement_cache({**self.CHASSIS1, - **{'chassis2': self.EMPTY_CHASSIS}}) + self._check_placement_config({**self.CHASSIS1, + **{'chassis2': self.EMPTY_CHASSIS}}) self._update_chassis( 'chassis2', bandwidths='br-provider0:3000:4000', inventory_defaults='allocation_ratio:3.0;min_unit:1', hypervisors='br-provider0:host2') - self._check_placement_cache({**self.CHASSIS1, **self.CHASSIS2}) + self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2}) def test_update_twice(self): self.mock_name2uuid.return_value = {'host1': 'uuid1', @@ -167,19 +170,19 @@ class TestOVNClientQosExtension(base.TestOVNFunctionalBase): inventory_defaults='allocation_ratio:1.0;min_unit:2', hypervisors='br-provider0:host1') self._create_chassis('host2', 'chassis2', physical_nets=['phys2']) - self._check_placement_cache({**self.CHASSIS1, - **{'chassis2': self.EMPTY_CHASSIS}}) + self._check_placement_config({**self.CHASSIS1, + **{'chassis2': self.EMPTY_CHASSIS}}) self._update_chassis( 'chassis2', bandwidths='br-provider0:3000:4000', inventory_defaults='allocation_ratio:3.0;min_unit:1', hypervisors='br-provider0:host2') - self._check_placement_cache({**self.CHASSIS1, **self.CHASSIS2}) + self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2}) self._update_chassis( 'chassis2', bandwidths='br-provider0:5000:6000', inventory_defaults='allocation_ratio:1.1;min_unit:1', hypervisors='br-provider0:host2') - self._check_placement_cache({**self.CHASSIS1, **self.CHASSIS2_B}) + self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2_B}) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py index 430ccbab484..c7e27b3c56f 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py @@ -48,9 +48,14 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): {'name': 'compute2', 'uuid': 'uuid2'}] } - def test__read_initial_chassis_config(self): + def test_read_initial_chassis_config(self): # Add two public networks, a RP per bridge and the correlation between # the hypervisors and the bridges. + def _check_expected_config(init_conf, expected): + res = {chassis_name: p_extension.dict_chassis_config(state) for + chassis_name, state in init_conf.items()} + self.assertEqual(expected, res) + chassis = fakes.FakeChassis.create( bridge_mappings=['public1:br-ext1', 'public2:br-ext2'], rp_bandwidths=['br-ext1:1000:2000', 'br-ext2:3000:4000'], @@ -58,7 +63,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors=['br-ext1:compute1', 'br-ext2:compute2']) self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: { 'br-ext1': {'egress': 1000, 'ingress': 2000}, @@ -69,7 +74,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): 'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'}, 'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected) # Add an extra bridge mapping that is discarded because it is not in # the hypervisors list (wrong configuration). @@ -81,7 +86,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors=['br-ext1:compute1', 'br-ext2:compute2']) self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: { 'br-ext1': {'egress': 1000, 'ingress': 2000}, @@ -92,7 +97,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): 'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'}, 'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected) # Add an unknown bridge, not present in the bridge mappings, that is # discarded (wrong configuration). @@ -103,7 +108,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors=['br-ext1:compute1', 'br-ext2:compute2']) self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: { 'br-ext1': {'egress': 1000, 'ingress': 2000}}, @@ -113,7 +118,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): 'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'}, 'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected) # Add an unknown hypervisor, that is not present in the Placement list # of resource providers. This hypervisor is discarded (wrong @@ -126,7 +131,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors=['br-ext1:compute1', 'br-ext2:compute3']) self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: { 'br-ext1': {'egress': 1000, 'ingress': 2000}}, @@ -135,7 +140,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): ovn_const.RP_HYPERVISORS: { 'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'}} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected) # Missing bridge mapping for br-ext2, the RP for this bridge will be # discarded. @@ -146,7 +151,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors=['br-ext1:compute1', 'br-ext2:compute2']) self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: { 'br-ext1': {'egress': 1000, 'ingress': 2000}}, @@ -156,7 +161,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): 'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'}, 'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected) # No bridge mappings, no RP BW inventories. chassis = fakes.FakeChassis.create( @@ -166,7 +171,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors=['br-ext1:compute1', 'br-ext2:compute2']) self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: {}, n_const.RP_INVENTORY_DEFAULTS: { @@ -175,7 +180,7 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): 'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'}, 'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected) # No bridge mappings nor hypervisors, no RP BW inventories. chassis = fakes.FakeChassis.create( @@ -185,14 +190,14 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors=None) self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: {}, n_const.RP_INVENTORY_DEFAULTS: { 'allocation_ratio': 1.0, 'min_unit': 5}, ovn_const.RP_HYPERVISORS: {} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected) # If no RP BW information (any deployment not using it), OVN Placement # extension won't break anything (sorry for LP#1936983, that was me). @@ -203,13 +208,13 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors=None) self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: {}, n_const.RP_INVENTORY_DEFAULTS: {}, ovn_const.RP_HYPERVISORS: {} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected) # Test wrongly defined parameters. E.g.: # external_ids: {ovn-cms-options={resource_provider_bandwidths=, ...}} @@ -220,10 +225,10 @@ class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase): rp_hypervisors='') self.plugin_driver._sb_idl.chassis_list.return_value.execute.\ return_value = [chassis] - init_conf = self.placement_driver._read_initial_chassis_config() + init_conf = self.placement_driver.read_initial_chassis_config() expected = {chassis.name: { n_const.RP_BANDWIDTHS: {}, n_const.RP_INVENTORY_DEFAULTS: {}, ovn_const.RP_HYPERVISORS: {} }} - self.assertEqual(expected, init_conf) + _check_expected_config(init_conf, expected)