neutron/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py

270 lines
10 KiB
Python

# 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