270 lines
10 KiB
Python
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
|