[OVN][Placement] Add a SB Chassis event to track changes in BW config

Added a new OVN Client extension: OVNClientPlacementExtension. This
extension is in charge of handling the bandwidth information stored
in the OVN database, in the "Chassis" registers on the
"ovn-cms-options" dictionary.

Three new keys are created to store the resource provider information
needed to parameterize the network backend bandwidth information,
following the implementation done in OVS and SR-IOV:
- resource_provider_bandwidths
- resource_provider_inventory_defaults
- resource_provider_hypervisors

When the OVN Client is started, the Placement extension will check if
the "placement" extension is loaded. It will also create an event to
check any configuration change done in any "Chassis" register.

The Placement extension will read the initial configuration stored
in the OVN database and will populate it to Placement API, creating
the needed resource providers, traits and inventories.

NOTE: This patch belongs to a series of patches to implement
minimum bandwidth scheduling blueprint in OVN backend. The next
patch will make OVN backend scheduling aware using the information
stored in Placement API and the port information passed by Nova when
a VM is created.

NOTE: this patch improves [1], fixing the error [2] reported in [3].

[1]https://review.opendev.org/c/openstack/neutron/+/776701
[2]https://paste.opendev.org/show/807614/
[3]https://launchpad.net/bugs/1936983

Partial-Bug: #1578989
Change-Id: I63e81aebce2621226ff2cfe91f16c97913c137e8
This commit is contained in:
Rodolfo Alonso Hernandez 2021-02-19 17:27:39 +00:00
parent cee0653145
commit a6f975ac03
9 changed files with 764 additions and 13 deletions

View File

@ -270,6 +270,28 @@ neutron-openvswitch-agent. However look out for:
resource_provider_bandwidths = ens5:40000000:40000000,ens6:40000000:40000000,...
#resource_provider_inventory_defaults = step_size:1000,...
OVN chassis config
~~~~~~~~~~~~~~~~~~
Bandwidth config values are stored in each SB chassis register, in
"external_ids:ovn-cms-options". The configuration options are the same as in
SR-IOV and OVS agents. This is how the values are registered:
.. code-block:: bash
$ root@dev20:~# ovs-vsctl list Open_vSwitch
...
external_ids : {hostname=dev20.fistro.com, \
ovn-cms-options="resource_provider_bandwidths=br-ex:1001:2000;br-ex2:3000:4000, \
resource_provider_inventory_defaults=allocation_ratio:1.0;min_unit:10, \
resource_provider_hypervisors=br-ex:dev20.fistro.com;br-ex2:dev20.fistro.com", \
rundir="/var/run/openvswitch", \
system-id="029e7d3d-d2ab-4f2c-bc92-ec58c94a8fc1"}
...
Each configuration option defined in "external_ids:ovn-cms-options" is divided
by commas.
Propagation of resource information
-----------------------------------

View File

@ -11,6 +11,7 @@
# under the License.
import re
import uuid
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants as const
@ -298,3 +299,16 @@ CMS_OPT_AVAILABILITY_ZONES = 'availability-zones'
# OVN vlan transparency option
VLAN_PASSTHRU = 'vlan-passthru'
# OVN Placement API; used for minimum bandwidth scheduling allocation.
# NOTE(ralonsoh): rehome to neutron-lib
RP_HYPERVISORS = 'resource_provider_hypervisors'
# OVN mechanism driver constants.
OVN_RP_UUID = uuid.UUID('5533233b-800c-11eb-b1f4-000056b2f5b8')
OVN_SUPPORTED_VNIC_TYPES = [portbindings.VNIC_NORMAL,
portbindings.VNIC_DIRECT,
portbindings.VNIC_DIRECT_PHYSICAL,
portbindings.VNIC_MACVTAP,
portbindings.VNIC_VHOST_VDPA,
]

View File

@ -20,7 +20,6 @@ import operator
import signal
import threading
import types
import uuid
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net
@ -95,8 +94,7 @@ class OVNMechanismDriver(api.MechanismDriver):
update network/port case, all data validation must be done within
methods that are part of the database transaction.
"""
resource_provider_uuid5_namespace = uuid.UUID(
'5533233b-800c-11eb-b1f4-000056b2f5b8')
resource_provider_uuid5_namespace = ovn_const.OVN_RP_UUID
def initialize(self):
"""Perform driver initialization.
@ -190,12 +188,7 @@ class OVNMechanismDriver(api.MechanismDriver):
in vlan_transparency_network_types)
def _setup_vif_port_bindings(self):
self.supported_vnic_types = [portbindings.VNIC_NORMAL,
portbindings.VNIC_DIRECT,
portbindings.VNIC_DIRECT_PHYSICAL,
portbindings.VNIC_MACVTAP,
portbindings.VNIC_VHOST_VDPA,
]
self.supported_vnic_types = ovn_const.OVN_SUPPORTED_VNIC_TYPES
self.vif_details = {
portbindings.VIF_TYPE_OVS: {
portbindings.CAP_PORT_FILTER: self.sg_enabled

View File

@ -0,0 +1,269 @@
# Copyright 2021 Red Hat, Inc.
#
# 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 itertools
from keystoneauth1 import exceptions as ks_exc
from neutron_lib import constants as n_const
from neutron_lib.placement import constants as placement_constants
from neutron_lib.placement import utils as placement_utils
from neutron_lib.plugins import constants as plugins_constants
from neutron_lib.plugins import directory
from neutron_lib.utils import helpers
from oslo_log import log as logging
from ovsdbapp.backend.ovs_idl import event as row_event
from neutron.agent.common import placement_report
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import utils as ovn_utils
from neutron.common import utils as common_utils
LOG = logging.getLogger(__name__)
def _parse_ovn_cms_options(chassis):
cms_options = ovn_utils.get_ovn_cms_options(chassis)
return {n_const.RP_BANDWIDTHS: _parse_bandwidths(cms_options),
n_const.RP_INVENTORY_DEFAULTS: _parse_inventory_defaults(
cms_options),
ovn_const.RP_HYPERVISORS: _parse_hypervisors(cms_options)}
def _parse_bridge_mappings(chassis):
bridge_mappings = chassis.external_ids.get('ovn-bridge-mappings', '')
bridge_mappings = helpers.parse_mappings(bridge_mappings.split(','),
unique_values=False)
return {k: [v] for k, v in bridge_mappings.items()}
def _parse_placement_option(option_name, cms_options):
for cms_option in (cms_option for cms_option in cms_options if
option_name in cms_option):
try:
return cms_option.split('=')[1]
except IndexError:
break
def _parse_bandwidths(cms_options):
bw_values = _parse_placement_option(n_const.RP_BANDWIDTHS, cms_options)
if not bw_values:
return {}
return placement_utils.parse_rp_bandwidths(bw_values.split(';'))
def _parse_inventory_defaults(cms_options):
inv_defaults = _parse_placement_option(n_const.RP_INVENTORY_DEFAULTS,
cms_options)
if not inv_defaults:
return {}
inventory = {}
for inv_default in inv_defaults.split(';'):
for key in placement_constants.INVENTORY_OPTIONS:
if key in inv_default:
inventory[key] = inv_default.split(':')[1]
return placement_utils.parse_rp_inventory_defaults(inventory)
def _parse_hypervisors(cms_options):
hyperv = _parse_placement_option(ovn_const.RP_HYPERVISORS, cms_options)
if not hyperv:
return {}
return helpers.parse_mappings(hyperv.split(';'), unique_values=False)
def _send_deferred_batch(state):
if not state:
return
deferred_batch = state.deferred_sync()
for deferred in deferred_batch:
try:
LOG.debug('Placement client: %s', str(deferred))
deferred.execute()
except Exception:
LOG.exception('Placement client call failed: %s', str(deferred))
def _dict_chassis_config(state):
if state:
return {n_const.RP_BANDWIDTHS: state._rp_bandwidths,
n_const.RP_INVENTORY_DEFAULTS: state._rp_inventory_defaults,
ovn_const.RP_HYPERVISORS: state._hypervisor_rps}
class ChassisBandwidthConfigEvent(row_event.RowEvent):
"""Chassis create update event to track the bandwidth config changes."""
def __init__(self, placement_extension):
self._placement_extension = placement_extension
# NOTE(ralonsoh): BW resource provider information is stored in
# "Chassis", not "Chassis_Private".
table = 'Chassis'
events = (self.ROW_CREATE, self.ROW_UPDATE)
super().__init__(events, table, None)
self.event_name = 'ChassisBandwidthConfigEvent'
def run(self, event, row, old):
name2uuid = self._placement_extension.name2uuid()
state = self._placement_extension.build_placement_state(row, name2uuid)
_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)
LOG.debug('OVN chassis %(chassis)s Placement configuration modified: '
'%(config)s', {'chassis': row.name, 'config': ch_config})
@common_utils.SingletonDecorator
class OVNClientPlacementExtension(object):
"""OVN client Placement API extension"""
def __init__(self, driver):
LOG.info('Starting OVNClientPlacementExtension')
super().__init__()
self._config_event = None
self._reset(driver)
def _reset(self, driver):
"""Reset the interval members values
This class is a singleton. Once initialized, any other new instance
will return the same object reference with the same member values.
This method is used to reset all of them as when the class is initially
instantiated if needed.
"""
self._driver = driver
self._placement_plugin = None
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:
return
if not self._config_event:
self._config_event = ChassisBandwidthConfigEvent(self)
try:
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()
@property
def placement_plugin(self):
if self._placement_plugin is None:
self._placement_plugin = directory.get_plugin(
plugins_constants.PLACEMENT_REPORT)
return self._placement_plugin
@property
def plugin(self):
if self._plugin is None:
self._plugin = self._driver._plugin
return self._plugin
@property
def ovn_mech_driver(self):
if self._ovn_mech_driver is None:
self._ovn_mech_driver = (
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):
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
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 name2uuid(self, name=None):
try:
rps = self.placement_plugin._placement_client.\
list_resource_providers(name=name)['resource_providers']
except (ks_exc.HttpError, ks_exc.ClientException):
LOG.warning('Error connecting to Placement API.')
return {}
_name2uuid = {rp['name']: rp['uuid'] for rp in rps}
LOG.info('Placement information about resource providers '
'(name:uuid):%s ', _name2uuid)
return _name2uuid
def build_placement_state(self, chassis, name2uuid):
bridge_mappings = _parse_bridge_mappings(chassis)
cms_options = _parse_ovn_cms_options(chassis)
LOG.debug('Building placement options for chassis %s: %s',
chassis.name, cms_options)
hypervisor_rps = {}
for device, hyperv in cms_options[ovn_const.RP_HYPERVISORS].items():
try:
hypervisor_rps[device] = {'name': hyperv,
'uuid': name2uuid[hyperv]}
except (KeyError, AttributeError):
continue
bridges = set(itertools.chain(*bridge_mappings.values()))
# Remove "cms_options[RP_BANDWIDTHS]" not present in "hypervisor_rps"
# and "bridge_mappings". If we don't have a way to match the RP bridge
# with a host ("hypervisor_rps") or a way to match the RP bridge with
# an external network ("bridge_mappings"), this value is irrelevant.
rp_bw = cms_options[n_const.RP_BANDWIDTHS]
if rp_bw:
cms_options[n_const.RP_BANDWIDTHS] = {
device: bw for device, bw in rp_bw.items() if
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.
return placement_report.PlacementState(
rp_bandwidths=cms_options[n_const.RP_BANDWIDTHS],
rp_inventory_defaults=cms_options[n_const.RP_INVENTORY_DEFAULTS],
rp_pkt_processing={},
rp_pkt_processing_inventory_defaults=None,
driver_uuid_namespace=self.uuid_ns,
agent_type=ovn_const.OVN_CONTROLLER_AGENT,
hypervisor_rps=hypervisor_rps,
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

View File

@ -44,6 +44,8 @@ from neutron.common import utils as common_utils
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
from neutron.db import ovn_revision_numbers_db as db_rev
from neutron.db import segments_db
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
import placement as placement_extension
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
import qos as qos_extension
from neutron.scheduler import l3_ovn_scheduler
@ -74,6 +76,8 @@ class OVNClient(object):
# TODO(ralonsoh): handle the OVN client extensions with an ext. manager
self._qos_driver = qos_extension.OVNClientQosExtension(self)
self._placement_extension = (
placement_extension.OVNClientPlacementExtension(self))
self._ovn_scheduler = l3_ovn_scheduler.get_scheduler()
@property

View File

@ -42,6 +42,8 @@ from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
from neutron.db import models # noqa
from neutron import manager
from neutron.plugins.ml2.drivers.ovn.agent import neutron_agent
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions import \
placement as ovn_client_placement
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import worker
from neutron.plugins.ml2.drivers import type_geneve # noqa
from neutron import service # noqa
@ -167,7 +169,7 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase,
self.temp_dir = self.useFixture(fixtures.TempDir()).path
self._start_ovsdb_server()
def setUp(self, maintenance_worker=False):
def setUp(self, maintenance_worker=False, service_plugins=None):
ml2_config.cfg.CONF.set_override('extension_drivers',
self._extension_drivers,
group='ml2')
@ -187,6 +189,7 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase,
self.addCleanup(exts.PluginAwareExtensionManager.clear_instance)
self.ovsdb_server_mgr = None
self._service_plugins = service_plugins
super(TestOVNFunctionalBase, self).setUp()
self.test_log_dir = os.path.join(DEFAULT_LOG_DIR, self.id())
base.setup_test_logging(
@ -220,10 +223,14 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase,
self._start_idls()
self._start_ovn_northd()
self.addCleanup(self._reset_agent_cache_singleton)
self.addCleanup(self._reset_ovn_client_placement_extension)
def _reset_agent_cache_singleton(self):
neutron_agent.AgentCache._instance = None
def _reset_ovn_client_placement_extension(self):
ovn_client_placement.OVNClientPlacementExtension._instance = None
def _get_install_share_path(self):
lookup_paths = set()
for installation in ['local', '']:
@ -253,8 +260,10 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase,
def get_additional_service_plugins(self):
p = super(TestOVNFunctionalBase, self).get_additional_service_plugins()
p.update({'revision_plugin_name': 'revisions'})
p.update({'segments': 'neutron.services.segments.plugin.Plugin'})
p.update({'revision_plugin_name': 'revisions',
'segments': 'neutron.services.segments.plugin.Plugin'})
if self._service_plugins:
p.update(self._service_plugins)
return p
@property

View File

@ -0,0 +1,185 @@
# Copyright 2021 Red Hat, Inc.
#
# 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 unittest import mock
from neutron_lib import constants as n_const
from neutron_lib.plugins import constants as plugins_constants
from neutron.common.ovn import constants as ovn_const
from neutron.common import utils as common_utils
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
import placement as placement_extension
from neutron.tests.functional import base
class TestOVNClientQosExtension(base.TestOVNFunctionalBase):
EMPTY_CHASSIS = {n_const.RP_BANDWIDTHS: {},
n_const.RP_INVENTORY_DEFAULTS: {},
ovn_const.RP_HYPERVISORS: {}}
RP_BANDWIDTHS_1 = {'br-provider0': {'egress': 1000, 'ingress': 2000}}
RP_INVENTORY_DEFAULTS_1 = {'allocation_ratio': 1.0, 'min_unit': 2}
RP_HYPERVISORS_1 = {'br-provider0': {'name': 'host1', 'uuid': 'uuid1'}}
CHASSIS1 = {
'chassis1': {
n_const.RP_BANDWIDTHS: RP_BANDWIDTHS_1,
n_const.RP_INVENTORY_DEFAULTS: RP_INVENTORY_DEFAULTS_1,
ovn_const.RP_HYPERVISORS: RP_HYPERVISORS_1
}
}
RP_BANDWIDTHS_2 = {'br-provider0': {'egress': 3000, 'ingress': 4000}}
RP_INVENTORY_DEFAULTS_2 = {'allocation_ratio': 3.0, 'min_unit': 1}
RP_HYPERVISORS_2 = {'br-provider0': {'name': 'host2', 'uuid': 'uuid2'}}
CHASSIS2 = {
'chassis2': {
n_const.RP_BANDWIDTHS: RP_BANDWIDTHS_2,
n_const.RP_INVENTORY_DEFAULTS: RP_INVENTORY_DEFAULTS_2,
ovn_const.RP_HYPERVISORS: RP_HYPERVISORS_2
}
}
RP_BANDWIDTHS_3 = {'br-provider0': {'egress': 5000, 'ingress': 6000}}
RP_INVENTORY_DEFAULTS_3 = {'allocation_ratio': 1.1, 'min_unit': 1}
CHASSIS2_B = {
'chassis2': {
n_const.RP_BANDWIDTHS: RP_BANDWIDTHS_3,
n_const.RP_INVENTORY_DEFAULTS: RP_INVENTORY_DEFAULTS_3,
ovn_const.RP_HYPERVISORS: RP_HYPERVISORS_2
}
}
def setUp(self, maintenance_worker=False, service_plugins=None):
service_plugins = {plugins_constants.PLACEMENT_REPORT: 'placement'}
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.mock_name2uuid = mock.patch.object(
self.placement_ext, 'name2uuid').start()
self.mock_send_batch = mock.patch.object(
placement_extension, '_send_deferred_batch').start()
def _build_external_ids(self, bandwidths, inventory_defaults, hypervisors):
options = []
if bandwidths:
options.append(n_const.RP_BANDWIDTHS + '=' + bandwidths)
if inventory_defaults:
options.append(n_const.RP_INVENTORY_DEFAULTS + '=' +
inventory_defaults)
if hypervisors:
options.append(ovn_const.RP_HYPERVISORS + '=' + hypervisors)
return {'ovn-cms-options': ','.join(options)}
def _create_chassis(self, host, name, physical_nets=None, bandwidths=None,
inventory_defaults=None, hypervisors=None):
external_ids = self._build_external_ids(bandwidths, inventory_defaults,
hypervisors)
self.add_fake_chassis(host, physical_nets=physical_nets,
external_ids=external_ids, name=name)
def _update_chassis(self, name, bandwidths=None, inventory_defaults=None,
hypervisors=None):
external_ids = self._build_external_ids(bandwidths, inventory_defaults,
hypervisors)
self.sb_api.db_set(
'Chassis', name, ('external_ids', external_ids)
).execute(check_error=True)
def _check_placement_cache(self, expected_chassis):
current_chassis = None
def check_chassis():
nonlocal current_chassis
current_chassis = self.placement_ext.chassis
return current_chassis == expected_chassis
try:
common_utils.wait_until_true(check_chassis, timeout=5)
except common_utils.WaitTimeout:
self.fail('OVN client Placement extension cache does not have '
'the expected chassis information.\nExpected: %s.\n'
'Actual: %s' % (expected_chassis, current_chassis))
def test_read_initial_config_and_update(self):
self.mock_name2uuid.return_value = {'host1': 'uuid1',
'host2': 'uuid2'}
self._create_chassis(
'host1', 'chassis1', physical_nets=['phys1'],
bandwidths='br-provider0:1000:2000',
inventory_defaults='allocation_ratio:1.0;min_unit:2',
hypervisors='br-provider0:host1')
self._create_chassis(
'host2', 'chassis2', physical_nets=['phys2'],
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._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})
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._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._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})
def test_update_twice(self):
self.mock_name2uuid.return_value = {'host1': 'uuid1',
'host2': 'uuid2'}
self._create_chassis(
'host1', 'chassis1', physical_nets=['phys1'],
bandwidths='br-provider0:1000:2000',
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._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._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})

View File

@ -17,6 +17,7 @@ import copy
from unittest import mock
from neutron_lib.api.definitions import l3
from neutron_lib import constants as n_const
from neutron_lib.utils import net
from oslo_utils import uuidutils
@ -819,7 +820,9 @@ class FakeOVNRouter(object):
class FakeChassis(object):
@staticmethod
def create(attrs=None, az_list=None, chassis_as_gw=False):
def create(attrs=None, az_list=None, chassis_as_gw=False,
bridge_mappings=None, rp_bandwidths=None,
rp_inventory_defaults=None, rp_hypervisors=None):
cms_opts = []
if az_list:
cms_opts.append("%s=%s" % (ovn_const.CMS_OPT_AVAILABILITY_ZONES,
@ -827,10 +830,33 @@ class FakeChassis(object):
if chassis_as_gw:
cms_opts.append(ovn_const.CMS_OPT_CHASSIS_AS_GW)
if rp_bandwidths:
cms_opts.append('%s=%s' % (n_const.RP_BANDWIDTHS,
';'.join(rp_bandwidths)))
elif rp_bandwidths == '': # Test wrongly defined parameter
cms_opts.append('%s=' % n_const.RP_BANDWIDTHS)
if rp_inventory_defaults:
inv_defaults = ';'.join('%s:%s' % (key, value) for key, value in
rp_inventory_defaults.items())
cms_opts.append('%s=%s' % (n_const.RP_INVENTORY_DEFAULTS,
inv_defaults))
elif rp_inventory_defaults == '': # Test wrongly defined parameter
cms_opts.append('%s=' % n_const.RP_INVENTORY_DEFAULTS)
if rp_hypervisors:
cms_opts.append('%s=%s' % (ovn_const.RP_HYPERVISORS,
';'.join(rp_hypervisors)))
elif rp_hypervisors == '': # Test wrongly defined parameter
cms_opts.append('%s=' % ovn_const.RP_HYPERVISORS)
external_ids = {}
if cms_opts:
external_ids[ovn_const.OVN_CMS_OPTIONS] = ','.join(cms_opts)
if bridge_mappings:
external_ids['ovn-bridge-mappings'] = ','.join(bridge_mappings)
attrs = {
'encaps': [],
'external_ids': external_ids,

View File

@ -0,0 +1,229 @@
# Copyright 2021 Red Hat, Inc.
#
# 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 unittest import mock
from neutron_lib import constants as n_const
from neutron.common.ovn import constants as ovn_const
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
import placement as p_extension
from neutron.tests.unit import fake_resources as fakes
from neutron.tests.unit.plugins.ml2 import test_plugin
class TestOVNClientPlacementExtension(test_plugin.Ml2PluginV2TestCase):
CORE_PLUGIN_CLASS = 'neutron.plugins.ml2.plugin.Ml2Plugin'
def setUp(self):
super(TestOVNClientPlacementExtension, self).setUp()
self.setup_coreplugin(self.CORE_PLUGIN_CLASS, load_plugins=True)
self.plugin_driver = mock.Mock()
self.placement_driver = p_extension.OVNClientPlacementExtension(
self.plugin_driver)
self.placement_client = mock.Mock(
update_trait=mock.Mock(__name__='update_trait'),
ensure_resource_provider=mock.Mock(__name__='ensure_rp'),
update_resource_provider_traits=mock.Mock(
__name__='update_rp_traits'),
update_resource_provider_inventories=mock.Mock(
__name__='update_rp_inventories'))
self.placement_plugin = mock.Mock(
_placement_client=self.placement_client)
self.placement_driver._placement_plugin = self.placement_plugin
self.placement_client.list_resource_providers.return_value = {
'resource_providers': [{'name': 'compute1', 'uuid': 'uuid1'},
{'name': 'compute2', 'uuid': 'uuid2'}]
}
def test__read_initial_chassis_config(self):
# Add two public networks, a RP per bridge and the correlation between
# the hypervisors and the bridges.
chassis = fakes.FakeChassis.create(
bridge_mappings=['public1:br-ext1', 'public2:br-ext2'],
rp_bandwidths=['br-ext1:1000:2000', 'br-ext2:3000:4000'],
rp_inventory_defaults={'allocation_ratio': 1.0, 'min_unit': 5},
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()
expected = {chassis.name: {
n_const.RP_BANDWIDTHS: {
'br-ext1': {'egress': 1000, 'ingress': 2000},
'br-ext2': {'egress': 3000, 'ingress': 4000}},
n_const.RP_INVENTORY_DEFAULTS: {
'allocation_ratio': 1.0, 'min_unit': 5},
ovn_const.RP_HYPERVISORS: {
'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'},
'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}}
}}
self.assertEqual(expected, init_conf)
# Add an extra bridge mapping that is discarded because it is not in
# the hypervisors list (wrong configuration).
chassis = fakes.FakeChassis.create(
bridge_mappings=['public1:br-ext1', 'public2:br-ext2',
'public3:br-ext3'],
rp_bandwidths=['br-ext1:1000:2000', 'br-ext2:3000:4000'],
rp_inventory_defaults={'allocation_ratio': 1.0, 'min_unit': 5},
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()
expected = {chassis.name: {
n_const.RP_BANDWIDTHS: {
'br-ext1': {'egress': 1000, 'ingress': 2000},
'br-ext2': {'egress': 3000, 'ingress': 4000}},
n_const.RP_INVENTORY_DEFAULTS: {
'allocation_ratio': 1.0, 'min_unit': 5},
ovn_const.RP_HYPERVISORS: {
'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'},
'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}}
}}
self.assertEqual(expected, init_conf)
# Add an unknown bridge, not present in the bridge mappings, that is
# discarded (wrong configuration).
chassis = fakes.FakeChassis.create(
bridge_mappings=['public1:br-ext1', 'public2:br-ext2'],
rp_bandwidths=['br-ext1:1000:2000', 'br-ext3:3000:4000'],
rp_inventory_defaults={'allocation_ratio': 1.0, 'min_unit': 5},
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()
expected = {chassis.name: {
n_const.RP_BANDWIDTHS: {
'br-ext1': {'egress': 1000, 'ingress': 2000}},
n_const.RP_INVENTORY_DEFAULTS: {
'allocation_ratio': 1.0, 'min_unit': 5},
ovn_const.RP_HYPERVISORS: {
'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'},
'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}}
}}
self.assertEqual(expected, init_conf)
# Add an unknown hypervisor, that is not present in the Placement list
# of resource providers. This hypervisor is discarded (wrong
# configuration). Because "br-ext2" has no match with an existing
# hypervisor, is discarded too.
chassis = fakes.FakeChassis.create(
bridge_mappings=['public1:br-ext1', 'public2:br-ext2'],
rp_bandwidths=['br-ext1:1000:2000', 'br-ext2:3000:4000'],
rp_inventory_defaults={'allocation_ratio': 1.0, 'min_unit': 5},
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()
expected = {chassis.name: {
n_const.RP_BANDWIDTHS: {
'br-ext1': {'egress': 1000, 'ingress': 2000}},
n_const.RP_INVENTORY_DEFAULTS: {
'allocation_ratio': 1.0, 'min_unit': 5},
ovn_const.RP_HYPERVISORS: {
'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'}}
}}
self.assertEqual(expected, init_conf)
# Missing bridge mapping for br-ext2, the RP for this bridge will be
# discarded.
chassis = fakes.FakeChassis.create(
bridge_mappings=['public1:br-ext1'],
rp_bandwidths=['br-ext1:1000:2000', 'br-ext2:3000:4000'],
rp_inventory_defaults={'allocation_ratio': 1.0, 'min_unit': 5},
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()
expected = {chassis.name: {
n_const.RP_BANDWIDTHS: {
'br-ext1': {'egress': 1000, 'ingress': 2000}},
n_const.RP_INVENTORY_DEFAULTS: {
'allocation_ratio': 1.0, 'min_unit': 5},
ovn_const.RP_HYPERVISORS: {
'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'},
'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}}
}}
self.assertEqual(expected, init_conf)
# No bridge mappings, no RP BW inventories.
chassis = fakes.FakeChassis.create(
bridge_mappings=None,
rp_bandwidths=['br-ext1:1000:2000', 'br-ext2:3000:4000'],
rp_inventory_defaults={'allocation_ratio': 1.0, 'min_unit': 5},
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()
expected = {chassis.name: {
n_const.RP_BANDWIDTHS: {},
n_const.RP_INVENTORY_DEFAULTS: {
'allocation_ratio': 1.0, 'min_unit': 5},
ovn_const.RP_HYPERVISORS: {
'br-ext1': {'name': 'compute1', 'uuid': 'uuid1'},
'br-ext2': {'name': 'compute2', 'uuid': 'uuid2'}}
}}
self.assertEqual(expected, init_conf)
# No bridge mappings nor hypervisors, no RP BW inventories.
chassis = fakes.FakeChassis.create(
bridge_mappings=None,
rp_bandwidths=['br-ext1:1000:2000', 'br-ext2:3000:4000'],
rp_inventory_defaults={'allocation_ratio': 1.0, 'min_unit': 5},
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()
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)
# If no RP BW information (any deployment not using it), OVN Placement
# extension won't break anything (sorry for LP#1936983, that was me).
chassis = fakes.FakeChassis.create(
bridge_mappings=['public1:br-ext1'],
rp_bandwidths=None,
rp_inventory_defaults=None,
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()
expected = {chassis.name: {
n_const.RP_BANDWIDTHS: {},
n_const.RP_INVENTORY_DEFAULTS: {},
ovn_const.RP_HYPERVISORS: {}
}}
self.assertEqual(expected, init_conf)
# Test wrongly defined parameters. E.g.:
# external_ids: {ovn-cms-options={resource_provider_bandwidths=, ...}}
chassis = fakes.FakeChassis.create(
bridge_mappings=['public1:br-ext1'],
rp_bandwidths='',
rp_inventory_defaults='',
rp_hypervisors='')
self.plugin_driver._sb_idl.chassis_list.return_value.execute.\
return_value = [chassis]
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)