/
placement.py
296 lines (245 loc) · 11.4 KB
/
placement.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# 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_config import cfg
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):
other_config = ovn_utils.get_ovn_chassis_other_config(chassis)
bridge_mappings = other_config.get('ovn-bridge-mappings', '')
bridge_mappings = helpers.parse_mappings(bridge_mappings.split(','))
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, driver):
self._driver = driver
# 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'
@property
def placement_extension(self):
if self._driver._post_fork_event.is_set():
return self._driver._ovn_client.placement_extension
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
elif event == self.ROW_CREATE:
return True
elif event == self.ROW_UPDATE and old and hasattr(old, 'other_config'):
row_bw = _parse_ovn_cms_options(row)
old_bw = _parse_ovn_cms_options(old)
if row_bw != old_bw:
return True
return False
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)
ch_config = dict_chassis_config(state)
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._rp_tun_name = cfg.CONF.ml2.tunnelled_network_rp_name
@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 enabled(self):
return bool(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
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)
if state:
chassis[ch.name] = state
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):
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 = {}
# ML2/OVN can also track tunnelled networks bandwidth. The key
# RP_TUNNELLED must be defined in "resource_provider_bandwidths" and
# "resource_provider_hypervisors". E.g.:
# ovn-cms-options =
# resource_provider_bandwidths=br-ex:100:200;rp_tunnelled:300:400
# resource_provider_hypervisors=br-ex:host1,rp_tunnelled:host1
for device, hyperv in cms_options[ovn_const.RP_HYPERVISORS].items():
try:
hypervisor_rps[device] = {'name': hyperv,
'uuid': name2uuid[hyperv]}
except (KeyError, AttributeError):
continue
rp_devices = set(itertools.chain(*bridge_mappings.values()))
# If "ml2.tunnelled_network_rp_name" is present in configured resource
# providers, that means this ML2/OVN host will track the tunnelled
# networks available bandwidth.
if self._rp_tun_name in hypervisor_rps:
rp_devices.add(self._rp_tun_name)
# 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] = {
rp_device: bw for rp_device, bw in rp_bw.items() if
rp_device in hypervisor_rps and rp_device in rp_devices}
# NOTE(ralonsoh): OVN only reports min BW RPs; packet processing RPs
# 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],
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)