| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | ## |
| 3 | # Copyright 2018 University of Bristol - High Performance Networks Research |
| 4 | # Group |
| 5 | # All Rights Reserved. |
| 6 | # |
| 7 | # Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique |
| 8 | # Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou |
| 9 | # |
| 10 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 11 | # not use this file except in compliance with the License. You may obtain |
| 12 | # a copy of the License at |
| 13 | # |
| 14 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 15 | # |
| 16 | # Unless required by applicable law or agreed to in writing, software |
| 17 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 18 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 19 | # License for the specific language governing permissions and limitations |
| 20 | # under the License. |
| 21 | # |
| 22 | # For those usages not covered by the Apache License, Version 2.0 please |
| 23 | # contact with: <highperformance-networks@bristol.ac.uk> |
| 24 | # |
| 25 | # Neither the name of the University of Bristol nor the names of its |
| 26 | # contributors may be used to endorse or promote products derived from |
| 27 | # this software without specific prior written permission. |
| 28 | # |
| 29 | # This work has been performed in the context of DCMS UK 5G Testbeds |
| 30 | # & Trials Programme and in the framework of the Metro-Haul project - |
| 31 | # funded by the European Commission under Grant number 761727 through the |
| 32 | # Horizon 2020 and 5G-PPP programmes. |
| 33 | ## |
| 34 | |
| 35 | """This module contains only logic related to managing records in a database |
| 36 | which includes data format normalization, data format validation and etc. |
| 37 | (It works as an extension to `nfvo_db.py` for the WIM feature) |
| 38 | |
| 39 | No domain logic/architectural concern should be present in this file. |
| 40 | """ |
| 41 | import json |
| 42 | import logging |
| 43 | from contextlib import contextmanager |
| 44 | from hashlib import sha1 |
| 45 | from itertools import groupby |
| 46 | from operator import itemgetter |
| 47 | from sys import exc_info |
| 48 | from threading import Lock |
| 49 | from time import time |
| 50 | from uuid import uuid1 as generate_uuid |
| 51 | |
| 52 | from six import reraise |
| 53 | |
| 54 | import yaml |
| 55 | |
| 56 | from ..utils import ( |
| 57 | check_valid_uuid, |
| 58 | convert_float_timestamp2str, |
| 59 | expand_joined_fields, |
| 60 | filter_dict_keys, |
| 61 | filter_out_dict_keys, |
| 62 | merge_dicts, |
| 63 | remove_none_items |
| 64 | ) |
| 65 | from .errors import ( |
| 66 | DbBaseException, |
| 67 | InvalidParameters, |
| 68 | MultipleRecordsFound, |
| 69 | NoRecordFound, |
| 70 | UndefinedUuidOrName, |
| 71 | UndefinedWanMappingType, |
| 72 | UnexpectedDatabaseError, |
| 73 | WimAccountOverwrite, |
| 74 | WimAndTenantAlreadyAttached |
| 75 | ) |
| 76 | |
| 77 | _WIM = 'wims AS wim ' |
| 78 | |
| 79 | _WIM_JOIN = ( |
| 80 | _WIM + |
| 81 | ' JOIN wim_nfvo_tenants AS association ' |
| 82 | ' ON association.wim_id=wim.uuid ' |
| 83 | ' JOIN nfvo_tenants AS nfvo_tenant ' |
| 84 | ' ON association.nfvo_tenant_id=nfvo_tenant.uuid ' |
| 85 | ' JOIN wim_accounts AS wim_account ' |
| 86 | ' ON association.wim_account_id=wim_account.uuid ' |
| 87 | ) |
| 88 | |
| 89 | _WIM_ACCOUNT_JOIN = ( |
| 90 | 'wim_accounts AS wim_account ' |
| 91 | ' JOIN wim_nfvo_tenants AS association ' |
| 92 | ' ON association.wim_account_id=wim_account.uuid ' |
| 93 | ' JOIN wims AS wim ' |
| 94 | ' ON association.wim_id=wim.uuid ' |
| 95 | ' JOIN nfvo_tenants AS nfvo_tenant ' |
| 96 | ' ON association.nfvo_tenant_id=nfvo_tenant.uuid ' |
| 97 | ) |
| 98 | |
| 99 | _DATACENTER_JOIN = ( |
| 100 | 'datacenters AS datacenter ' |
| 101 | ' JOIN tenants_datacenters AS association ' |
| 102 | ' ON association.datacenter_id=datacenter.uuid ' |
| 103 | ' JOIN datacenter_tenants as datacenter_account ' |
| 104 | ' ON association.datacenter_tenant_id=datacenter_account.uuid ' |
| 105 | ' JOIN nfvo_tenants AS nfvo_tenant ' |
| 106 | ' ON association.nfvo_tenant_id=nfvo_tenant.uuid ' |
| 107 | ) |
| 108 | |
| 109 | _PORT_MAPPING = 'wim_port_mappings as wim_port_mapping ' |
| 110 | |
| 111 | _PORT_MAPPING_JOIN_WIM = ( |
| 112 | ' JOIN wims as wim ' |
| 113 | ' ON wim_port_mapping.wim_id=wim.uuid ' |
| 114 | ) |
| 115 | |
| 116 | _PORT_MAPPING_JOIN_DATACENTER = ( |
| 117 | ' JOIN datacenters as datacenter ' |
| 118 | ' ON wim_port_mapping.datacenter_id=datacenter.uuid ' |
| 119 | ) |
| 120 | |
| 121 | _WIM_SELECT = [ |
| 122 | 'wim.{0} as {0}'.format(_field) |
| 123 | for _field in 'uuid name description wim_url type config ' |
| 124 | 'created_at modified_at'.split() |
| 125 | ] |
| 126 | |
| 127 | _WIM_ACCOUNT_SELECT = 'uuid name user password config'.split() |
| 128 | |
| 129 | _PORT_MAPPING_SELECT = ('wim_port_mapping.*', ) |
| 130 | |
| 131 | _CONFIDENTIAL_FIELDS = ('password', 'passwd') |
| 132 | |
| 133 | _SERIALIZED_FIELDS = ('config', 'vim_info', 'wim_info', 'conn_info', 'extra', |
| 134 | 'wan_service_mapping_info') |
| 135 | |
| 136 | UNIQUE_PORT_MAPPING_INFO_FIELDS = { |
| 137 | 'dpid-port': ('wan_switch_dpid', 'wan_switch_port') |
| 138 | } |
| 139 | """Fields that should be unique for each port mapping that relies on |
| 140 | wan_service_mapping_info. |
| 141 | |
| 142 | For example, for port mappings of type 'dpid-port', each combination of |
| 143 | wan_switch_dpid and wan_switch_port should be unique (the same switch cannot |
| 144 | be connected to two different places using the same port) |
| 145 | """ |
| 146 | |
| 147 | |
| 148 | class WimPersistence(object): |
| 149 | """High level interactions with the WIM tables in the database""" |
| 150 | |
| 151 | def __init__(self, db, logger=None, lock=None): |
| 152 | self.db = db |
| 153 | self.logger = logger or logging.getLogger('openmano.wim.persistence') |
| 154 | self.lock = lock or Lock() |
| 155 | |
| 156 | def query(self, |
| 157 | FROM=None, |
| 158 | SELECT=None, |
| 159 | WHERE=None, |
| 160 | ORDER_BY=None, |
| 161 | LIMIT=None, |
| 162 | OFFSET=None, |
| 163 | error_if_none=True, |
| 164 | error_if_multiple=False, |
| 165 | postprocess=None, |
| 166 | hide=_CONFIDENTIAL_FIELDS, |
| 167 | **kwargs): |
| 168 | """Retrieve records from the database. |
| 169 | |
| 170 | Keyword Arguments: |
| 171 | SELECT, FROM, WHERE, LIMIT, ORDER_BY: used to compose the SQL |
| 172 | query. See ``nfvo_db.get_rows``. |
| 173 | OFFSET: only valid when used togheter with LIMIT. |
| 174 | Ignore the OFFSET first results of the query. |
| 175 | error_if_none: by default an error is raised if no record is |
| 176 | found. With this option it is possible to disable this error. |
| 177 | error_if_multiple: by default no error is raised if more then one |
| 178 | record is found. |
| 179 | With this option it is possible to enable this error. |
| 180 | postprocess: function applied to every retrieved record. |
| 181 | This function receives a dict as input and must return it |
| 182 | after modifications. Moreover this function should accept a |
| 183 | second optional parameter ``hide`` indicating |
| 184 | the confidential fiels to be obfuscated. |
| 185 | By default a minimal postprocessing function is applied, |
| 186 | obfuscating confidential fields and converting timestamps. |
| 187 | hide: option proxied to postprocess |
| 188 | |
| 189 | All the remaining keyword arguments will be assumed to be ``name``s or |
| 190 | ``uuid``s to compose the WHERE statement, according to their format. |
| 191 | If the value corresponds to an array, the first element will determine |
| 192 | if it is an name or UUID. |
| 193 | |
| 194 | For example: |
| 195 | - ``wim="abcdef"``` will be turned into ``wim.name="abcdef"``, |
| 196 | - ``datacenter="5286a274-8a1b-4b8d-a667-9c94261ad855"`` |
| 197 | will be turned into |
| 198 | ``datacenter.uuid="5286a274-8a1b-4b8d-a667-9c94261ad855"``. |
| 199 | - ``wim=["5286a274-8a1b-4b8d-a667-9c94261ad855", ...]`` |
| 200 | will be turned into |
| 201 | ``wim.uuid=["5286a274-8a1b-4b8d-a667-9c94261ad855", ...]`` |
| 202 | |
| 203 | Raises: |
| 204 | NoRecordFound: if the query result set is empty |
| 205 | DbBaseException: errors occuring during the execution of the query. |
| 206 | """ |
| 207 | # Defaults: |
| 208 | postprocess = postprocess or _postprocess_record |
| 209 | WHERE = WHERE or {} |
| 210 | |
| 211 | # Find remaining keywords by name or uuid |
| 212 | WHERE.update(_compose_where_from_uuids_or_names(**kwargs)) |
| 213 | WHERE = WHERE or None |
| 214 | # ^ If the where statement is empty, it is better to leave it as None, |
| 215 | # so it can be filtered out at a later stage |
| 216 | LIMIT = ('{:d},{:d}'.format(OFFSET, LIMIT) |
| 217 | if LIMIT and OFFSET else LIMIT) |
| 218 | |
| 219 | query = remove_none_items({ |
| 220 | 'SELECT': SELECT, 'FROM': FROM, 'WHERE': WHERE, |
| 221 | 'LIMIT': LIMIT, 'ORDER_BY': ORDER_BY}) |
| 222 | |
| 223 | with self.lock: |
| 224 | records = self.db.get_rows(**query) |
| 225 | |
| 226 | table = FROM.split()[0] |
| 227 | if error_if_none and not records: |
| 228 | raise NoRecordFound(WHERE, table) |
| 229 | |
| 230 | if error_if_multiple and len(records) > 1: |
| 231 | self.logger.error('Multiple records ' |
| 232 | 'FROM %s WHERE %s:\n\n%s\n\n', |
| 233 | FROM, WHERE, json.dumps(records, indent=4)) |
| 234 | raise MultipleRecordsFound(WHERE, table) |
| 235 | |
| 236 | return [ |
| 237 | expand_joined_fields(postprocess(record, hide)) |
| 238 | for record in records |
| 239 | ] |
| 240 | |
| 241 | def query_one(self, *args, **kwargs): |
| 242 | """Similar to ``query``, but ensuring just one result. |
| 243 | ``error_if_multiple`` is enabled by default. |
| 244 | """ |
| 245 | kwargs.setdefault('error_if_multiple', True) |
| 246 | records = self.query(*args, **kwargs) |
| 247 | return records[0] if records else None |
| 248 | |
| 249 | def get_by_uuid(self, table, uuid, **kwargs): |
| 250 | """Retrieve one record from the database based on its uuid |
| 251 | |
| 252 | Arguments: |
| 253 | table (str): table name (to be used in SQL's FROM statement). |
| 254 | uuid (str): unique identifier for record. |
| 255 | |
| 256 | For additional keyword arguments and exceptions see :obj:`~.query` |
| 257 | (``error_if_multiple`` is enabled by default). |
| 258 | """ |
| 259 | if uuid is None: |
| 260 | raise UndefinedUuidOrName(table) |
| 261 | return self.query_one(table, WHERE={'uuid': uuid}, **kwargs) |
| 262 | |
| 263 | def get_by_name_or_uuid(self, table, uuid_or_name, **kwargs): |
| 264 | """Retrieve a record from the database based on a value that can be its |
| 265 | uuid or name. |
| 266 | |
| 267 | Arguments: |
| 268 | table (str): table name (to be used in SQL's FROM statement). |
| 269 | uuid_or_name (str): this value can correspond to either uuid or |
| 270 | name |
| 271 | For additional keyword arguments and exceptions see :obj:`~.query` |
| 272 | (``error_if_multiple`` is enabled by default). |
| 273 | """ |
| 274 | if uuid_or_name is None: |
| 275 | raise UndefinedUuidOrName(table) |
| 276 | |
| 277 | key = 'uuid' if check_valid_uuid(uuid_or_name) else 'name' |
| 278 | return self.query_one(table, WHERE={key: uuid_or_name}, **kwargs) |
| 279 | |
| 280 | def get_wims(self, uuid_or_name=None, tenant=None, **kwargs): |
| 281 | """Retrieve information about one or more WIMs stored in the database |
| 282 | |
| 283 | Arguments: |
| 284 | uuid_or_name (str): uuid or name for WIM |
| 285 | tenant (str): [optional] uuid or name for NFVO tenant |
| 286 | |
| 287 | See :obj:`~.query` for additional keyword arguments. |
| 288 | """ |
| 289 | kwargs.update(wim=uuid_or_name, tenant=tenant) |
| 290 | from_ = _WIM_JOIN if tenant else _WIM |
| 291 | select_ = _WIM_SELECT[:] + (['wim_account.*'] if tenant else []) |
| 292 | |
| 293 | kwargs.setdefault('SELECT', select_) |
| 294 | return self.query(from_, **kwargs) |
| 295 | |
| 296 | def get_wim(self, wim, tenant=None, **kwargs): |
| 297 | """Similar to ``get_wims`` but ensure only one result is returned""" |
| 298 | kwargs.setdefault('error_if_multiple', True) |
| 299 | return self.get_wims(wim, tenant)[0] |
| 300 | |
| 301 | def create_wim(self, wim_descriptor): |
| 302 | """Create a new wim record inside the database and returns its uuid |
| 303 | |
| 304 | Arguments: |
| 305 | wim_descriptor (dict): properties of the record |
| 306 | (usually each field corresponds to a database column, but extra |
| 307 | information can be offloaded to another table or serialized as |
| 308 | JSON/YAML) |
| 309 | Returns: |
| 310 | str: UUID of the created WIM |
| 311 | """ |
| 312 | if "config" in wim_descriptor: |
| 313 | wim_descriptor["config"] = _serialize(wim_descriptor["config"]) |
| 314 | |
| 315 | with self.lock: |
| 316 | return self.db.new_row( |
| 317 | "wims", wim_descriptor, add_uuid=True, confidential_data=True) |
| 318 | |
| 319 | def update_wim(self, uuid_or_name, wim_descriptor): |
| 320 | """Change an existing WIM record on the database""" |
| 321 | # obtain data, check that only one exist |
| 322 | wim = self.get_by_name_or_uuid('wims', uuid_or_name) |
| 323 | |
| 324 | # edit data |
| 325 | wim_id = wim['uuid'] |
| 326 | where = {'uuid': wim['uuid']} |
| 327 | |
| 328 | # unserialize config, edit and serialize it again |
| Anderson Bravalheri | fed47b0 | 2018-12-16 20:44:08 +0000 | [diff] [blame] | 329 | new_config_dict = wim_descriptor.get('config', {}) or {} |
| 330 | config_dict = remove_none_items(merge_dicts( |
| 331 | wim.get('config', {}) or {}, new_config_dict)) |
| 332 | wim_descriptor['config'] = ( |
| 333 | _serialize(config_dict) if config_dict else None) |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 334 | |
| 335 | with self.lock: |
| 336 | self.db.update_rows('wims', wim_descriptor, where) |
| 337 | |
| 338 | return wim_id |
| 339 | |
| 340 | def delete_wim(self, wim): |
| 341 | # get nfvo_tenant info |
| 342 | wim = self.get_by_name_or_uuid('wims', wim) |
| 343 | |
| 344 | with self.lock: |
| 345 | self.db.delete_row_by_id('wims', wim['uuid']) |
| 346 | |
| 347 | return wim['uuid'] + ' ' + wim['name'] |
| 348 | |
| 349 | def get_wim_accounts_by(self, wim=None, tenant=None, uuid=None, **kwargs): |
| 350 | """Retrieve WIM account information from the database together |
| 351 | with the related records (wim, nfvo_tenant and wim_nfvo_tenant) |
| 352 | |
| 353 | Arguments: |
| 354 | wim (str): uuid or name for WIM |
| 355 | tenant (str): [optional] uuid or name for NFVO tenant |
| 356 | |
| 357 | See :obj:`~.query` for additional keyword arguments. |
| 358 | """ |
| 359 | kwargs.update(wim=wim, tenant=tenant) |
| 360 | kwargs.setdefault('postprocess', _postprocess_wim_account) |
| 361 | if uuid: |
| 362 | kwargs.setdefault('WHERE', {'wim_account.uuid': uuid}) |
| 363 | return self.query(FROM=_WIM_ACCOUNT_JOIN, **kwargs) |
| 364 | |
| Anderson Bravalheri | e2c09f3 | 2018-11-30 09:55:29 +0000 | [diff] [blame] | 365 | def get_wim_account_by(self, wim=None, tenant=None, uuid=None, **kwargs): |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 366 | """Similar to ``get_wim_accounts_by``, but ensuring just one result""" |
| 367 | kwargs.setdefault('error_if_multiple', True) |
| Anderson Bravalheri | e2c09f3 | 2018-11-30 09:55:29 +0000 | [diff] [blame] | 368 | return self.get_wim_accounts_by(wim, tenant, uuid, **kwargs)[0] |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 369 | |
| 370 | def get_wim_accounts(self, **kwargs): |
| 371 | """Retrieve all the accounts from the database""" |
| 372 | kwargs.setdefault('postprocess', _postprocess_wim_account) |
| 373 | return self.query(FROM=_WIM_ACCOUNT_JOIN, **kwargs) |
| 374 | |
| 375 | def get_wim_account(self, uuid_or_name, **kwargs): |
| 376 | """Retrieve WIM Account record by UUID or name, |
| 377 | See :obj:`get_by_name_or_uuid` for keyword arguments. |
| 378 | """ |
| 379 | kwargs.setdefault('postprocess', _postprocess_wim_account) |
| 380 | kwargs.setdefault('SELECT', _WIM_ACCOUNT_SELECT) |
| 381 | return self.get_by_name_or_uuid('wim_accounts', uuid_or_name, **kwargs) |
| 382 | |
| 383 | @contextmanager |
| 384 | def _associate(self, wim_id, nfvo_tenant_id): |
| 385 | """Auxiliary method for ``create_wim_account`` |
| 386 | |
| 387 | This method just create a row in the association table |
| 388 | ``wim_nfvo_tenants`` |
| 389 | """ |
| 390 | try: |
| 391 | with self.lock: |
| 392 | yield |
| 393 | except DbBaseException as db_exception: |
| 394 | error_msg = str(db_exception) |
| 395 | if all([msg in error_msg |
| 396 | for msg in ("already in use", "'wim_nfvo_tenant'")]): |
| 397 | ex = WimAndTenantAlreadyAttached(wim_id, nfvo_tenant_id) |
| 398 | reraise(ex.__class__, ex, exc_info()[2]) |
| 399 | |
| 400 | raise |
| 401 | |
| 402 | def create_wim_account(self, wim, tenant, properties): |
| 403 | """Associate a wim to a tenant using the ``wim_nfvo_tenants`` table |
| 404 | and create a ``wim_account`` to store credentials and configurations. |
| 405 | |
| 406 | For the sake of simplification, we assume that each NFVO tenant can be |
| 407 | attached to a WIM using only one WIM account. This is automatically |
| 408 | guaranteed via database constraints. |
| 409 | For corner cases, the same WIM can be registered twice using another |
| 410 | name. |
| 411 | |
| 412 | Arguments: |
| 413 | wim (str): name or uuid of the WIM related to the account being |
| 414 | created |
| 415 | tenant (str): name or uuid of the nfvo tenant to which the account |
| 416 | will be created |
| 417 | properties (dict): properties of the account |
| 418 | (eg. user, password, ...) |
| 419 | """ |
| 420 | wim_id = self.get_by_name_or_uuid('wims', wim, SELECT=['uuid'])['uuid'] |
| 421 | tenant = self.get_by_name_or_uuid('nfvo_tenants', tenant, |
| 422 | SELECT=['uuid', 'name']) |
| 423 | account = properties.setdefault('name', tenant['name']) |
| 424 | |
| 425 | wim_account = self.query_one('wim_accounts', |
| 426 | WHERE={'wim_id': wim_id, 'name': account}, |
| 427 | error_if_none=False) |
| 428 | |
| 429 | transaction = [] |
| 430 | used_uuids = [] |
| 431 | |
| 432 | if wim_account is None: |
| 433 | # If a row for the wim account doesn't exist yet, we need to |
| 434 | # create one, otherwise we can just re-use it. |
| 435 | account_id = str(generate_uuid()) |
| 436 | used_uuids.append(account_id) |
| 437 | row = merge_dicts(properties, wim_id=wim_id, uuid=account_id) |
| 438 | transaction.append({'wim_accounts': _preprocess_wim_account(row)}) |
| 439 | else: |
| 440 | account_id = wim_account['uuid'] |
| 441 | properties.pop('config', None) # Config is too complex to compare |
| 442 | diff = {k: v for k, v in properties.items() if v != wim_account[k]} |
| 443 | if diff: |
| 444 | tip = 'Edit the account first, and then attach it to a tenant' |
| 445 | raise WimAccountOverwrite(wim_account, diff, tip) |
| 446 | |
| 447 | transaction.append({ |
| 448 | 'wim_nfvo_tenants': {'nfvo_tenant_id': tenant['uuid'], |
| 449 | 'wim_id': wim_id, |
| 450 | 'wim_account_id': account_id}}) |
| 451 | |
| 452 | with self._associate(wim_id, tenant['uuid']): |
| 453 | self.db.new_rows(transaction, used_uuids, confidential_data=True) |
| 454 | |
| 455 | return account_id |
| 456 | |
| 457 | def update_wim_account(self, uuid, properties, hide=_CONFIDENTIAL_FIELDS): |
| 458 | """Update WIM account record by overwriting fields with new values |
| 459 | |
| 460 | Specially for the field ``config`` this means that a new dict will be |
| 461 | merged to the existing one. |
| 462 | |
| 463 | Attributes: |
| 464 | uuid (str): UUID for the WIM account |
| 465 | properties (dict): fields that should be overwritten |
| 466 | |
| 467 | Returns: |
| 468 | Updated wim_account |
| 469 | """ |
| 470 | wim_account = self.get_by_uuid('wim_accounts', uuid) |
| 471 | safe_fields = 'user password name created'.split() |
| 472 | updates = _preprocess_wim_account( |
| 473 | merge_dicts(wim_account, filter_dict_keys(properties, safe_fields)) |
| 474 | ) |
| 475 | |
| 476 | if properties.get('config'): |
| 477 | old_config = wim_account.get('config') or {} |
| 478 | new_config = merge_dicts(old_config, properties['config']) |
| 479 | updates['config'] = _serialize(new_config) |
| 480 | |
| 481 | with self.lock: |
| 482 | num_changes = self.db.update_rows( |
| 483 | 'wim_accounts', UPDATE=updates, |
| 484 | WHERE={'uuid': wim_account['uuid']}) |
| 485 | |
| 486 | if num_changes is None: |
| 487 | raise UnexpectedDatabaseError('Impossible to update wim_account ' |
| 488 | '{name}:{uuid}'.format(*wim_account)) |
| 489 | |
| 490 | return self.get_wim_account(wim_account['uuid'], hide=hide) |
| 491 | |
| 492 | def delete_wim_account(self, uuid): |
| 493 | """Remove WIM account record from the database""" |
| 494 | # Since we have foreign keys configured with ON CASCADE, we can rely |
| 495 | # on the database engine to guarantee consistency, deleting the |
| 496 | # dependant records |
| 497 | with self.lock: |
| 498 | return self.db.delete_row_by_id('wim_accounts', uuid) |
| 499 | |
| 500 | def get_datacenters_by(self, datacenter=None, tenant=None, **kwargs): |
| 501 | """Retrieve datacenter information from the database together |
| 502 | with the related records (nfvo_tenant) |
| 503 | |
| 504 | Arguments: |
| 505 | datacenter (str): uuid or name for datacenter |
| 506 | tenant (str): [optional] uuid or name for NFVO tenant |
| 507 | |
| 508 | See :obj:`~.query` for additional keyword arguments. |
| 509 | """ |
| Anderson Bravalheri | fed47b0 | 2018-12-16 20:44:08 +0000 | [diff] [blame] | 510 | if tenant: |
| 511 | kwargs.update(datacenter=datacenter, tenant=tenant) |
| 512 | return self.query(_DATACENTER_JOIN, **kwargs) |
| 513 | else: |
| 514 | return [self.get_by_name_or_uuid('datacenters', |
| 515 | datacenter, **kwargs)] |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 516 | |
| 517 | def get_datacenter_by(self, datacenter=None, tenant=None, **kwargs): |
| 518 | """Similar to ``get_datacenters_by``, but ensuring just one result""" |
| 519 | kwargs.setdefault('error_if_multiple', True) |
| 520 | return self.get_datacenters_by(datacenter, tenant, **kwargs)[0] |
| 521 | |
| 522 | def _create_single_port_mapping(self, properties): |
| 523 | info = properties.setdefault('wan_service_mapping_info', {}) |
| 524 | endpoint_id = properties.get('wan_service_endpoint_id') |
| 525 | |
| 526 | if info.get('mapping_type') and not endpoint_id: |
| 527 | properties['wan_service_endpoint_id'] = ( |
| 528 | self._generate_port_mapping_id(info)) |
| 529 | |
| 530 | properties['wan_service_mapping_info'] = _serialize(info) |
| 531 | |
| 532 | try: |
| 533 | with self.lock: |
| 534 | self.db.new_row('wim_port_mappings', properties, |
| 535 | add_uuid=False, confidential_data=True) |
| 536 | except DbBaseException as old_exception: |
| 537 | self.logger.exception(old_exception) |
| 538 | ex = InvalidParameters( |
| 539 | "The mapping must contain the " |
| 540 | "'pop_switch_dpid', 'pop_switch_port', and " |
| 541 | "wan_service_mapping_info: " |
| 542 | "('wan_switch_dpid' and 'wan_switch_port') or " |
| 543 | "'wan_service_endpoint_id}'") |
| 544 | reraise(ex.__class__, ex, exc_info()[2]) |
| 545 | |
| 546 | return properties |
| 547 | |
| 548 | def create_wim_port_mappings(self, wim, port_mappings, tenant=None): |
| 549 | if not isinstance(wim, dict): |
| 550 | wim = self.get_by_name_or_uuid('wims', wim) |
| 551 | |
| 552 | for port_mapping in port_mappings: |
| 553 | port_mapping['wim_name'] = wim['name'] |
| 554 | datacenter = self.get_datacenter_by( |
| 555 | port_mapping['datacenter_name'], tenant) |
| 556 | for pop_wan_port_mapping in port_mapping['pop_wan_mappings']: |
| 557 | element = merge_dicts(pop_wan_port_mapping, { |
| 558 | 'wim_id': wim['uuid'], |
| 559 | 'datacenter_id': datacenter['uuid']}) |
| 560 | self._create_single_port_mapping(element) |
| 561 | |
| 562 | return port_mappings |
| 563 | |
| 564 | def _filter_port_mappings_by_tenant(self, mappings, tenant): |
| 565 | """Make sure all the datacenters and wims listed in the port mapping |
| 566 | belong to an specific tenant |
| 567 | """ |
| 568 | |
| 569 | # NOTE: Theoretically this could be done at SQL level, but given the |
| 570 | # number of tables involved (wim_port_mappings, wim_accounts, |
| 571 | # wims, wim_nfvo_tenants, datacenters, datacenter_tenants, |
| 572 | # tenants_datacents and nfvo_tenants), it would result in a |
| 573 | # extremely complex query. Moreover, the predicate can vary: |
| 574 | # for `get_wim_port_mappings` we can have any combination of |
| 575 | # (wim, datacenter, tenant), not all of them having the 3 values |
| 576 | # so we have combinatorial trouble to write the 'FROM' statement. |
| 577 | |
| 578 | kwargs = {'tenant': tenant, 'error_if_none': False} |
| 579 | # Cache results to speedup things |
| 580 | datacenters = {} |
| 581 | wims = {} |
| 582 | |
| 583 | def _get_datacenter(uuid): |
| 584 | return ( |
| 585 | datacenters.get(uuid) or |
| 586 | datacenters.setdefault( |
| 587 | uuid, self.get_datacenters_by(uuid, **kwargs))) |
| 588 | |
| 589 | def _get_wims(uuid): |
| 590 | return (wims.get(uuid) or |
| 591 | wims.setdefault(uuid, self.get_wims(uuid, **kwargs))) |
| 592 | |
| 593 | return [ |
| 594 | mapping |
| 595 | for mapping in mappings |
| 596 | if (_get_datacenter(mapping['datacenter_id']) and |
| 597 | _get_wims(mapping['wim_id'])) |
| 598 | ] |
| 599 | |
| 600 | def get_wim_port_mappings(self, wim=None, datacenter=None, tenant=None, |
| 601 | **kwargs): |
| 602 | """List all the port mappings, optionally filtering by wim, datacenter |
| 603 | AND/OR tenant |
| 604 | """ |
| 605 | from_ = [_PORT_MAPPING, |
| 606 | _PORT_MAPPING_JOIN_WIM if wim else '', |
| 607 | _PORT_MAPPING_JOIN_DATACENTER if datacenter else ''] |
| 608 | |
| 609 | criteria = ('wim_id', 'datacenter_id') |
| 610 | kwargs.setdefault('error_if_none', False) |
| 611 | mappings = self.query( |
| 612 | ' '.join(from_), |
| 613 | SELECT=_PORT_MAPPING_SELECT, |
| 614 | ORDER_BY=['wim_port_mapping.{}'.format(c) for c in criteria], |
| 615 | wim=wim, datacenter=datacenter, |
| 616 | postprocess=_postprocess_wim_port_mapping, |
| 617 | **kwargs) |
| 618 | |
| 619 | if tenant: |
| 620 | mappings = self._filter_port_mappings_by_tenant(mappings, tenant) |
| 621 | |
| 622 | # We don't have to sort, since we have used 'ORDER_BY' |
| 623 | grouped_mappings = groupby(mappings, key=itemgetter(*criteria)) |
| 624 | |
| 625 | return [ |
| 626 | {'wim_id': key[0], |
| 627 | 'datacenter_id': key[1], |
| Anderson Bravalheri | fed47b0 | 2018-12-16 20:44:08 +0000 | [diff] [blame] | 628 | 'pop_wan_mappings': [ |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 629 | filter_out_dict_keys(mapping, ( |
| 630 | 'id', 'wim_id', 'datacenter_id', |
| 631 | 'created_at', 'modified_at')) |
| 632 | for mapping in group]} |
| 633 | for key, group in grouped_mappings |
| 634 | ] |
| 635 | |
| 636 | def delete_wim_port_mappings(self, wim_id): |
| 637 | with self.lock: |
| 638 | self.db.delete_row(FROM='wim_port_mappings', |
| 639 | WHERE={"wim_id": wim_id}) |
| 640 | return "port mapping for wim {} deleted.".format(wim_id) |
| 641 | |
| 642 | def update_wim_port_mapping(self, id, properties): |
| 643 | original = self.query_one('wim_port_mappings', WHERE={'id': id}) |
| 644 | |
| 645 | mapping_info = remove_none_items(merge_dicts( |
| 646 | original.get('wan_service_mapping_info') or {}, |
| 647 | properties.get('wan_service_mapping_info') or {})) |
| 648 | |
| 649 | updates = preprocess_record( |
| 650 | merge_dicts(original, remove_none_items(properties), |
| 651 | wan_service_mapping_info=mapping_info)) |
| 652 | |
| 653 | with self.lock: |
| 654 | num_changes = self.db.update_rows( |
| 655 | 'wim_port_mappings', UPDATE=updates, WHERE={'id': id}) |
| 656 | |
| 657 | if num_changes is None: |
| 658 | raise UnexpectedDatabaseError( |
| 659 | 'Impossible to update wim_port_mappings %s:\n%s\n', |
| 660 | id, _serialize(properties)) |
| 661 | |
| 662 | return num_changes |
| 663 | |
| 664 | def get_actions_in_groups(self, wim_account_id, |
| 665 | item_types=('instance_wim_nets',), |
| 666 | group_offset=0, group_limit=150): |
| 667 | """Retrieve actions from the database in groups. |
| 668 | Each group contains all the actions that have the same ``item`` type |
| 669 | and ``item_id``. |
| 670 | |
| 671 | Arguments: |
| 672 | wim_account_id: restrict the search to actions to be performed |
| 673 | using the same account |
| 674 | item_types (list): [optional] filter the actions to the given |
| 675 | item types |
| 676 | group_limit (int): maximum number of groups returned by the |
| 677 | function |
| 678 | group_offset (int): skip the N first groups. Used together with |
| 679 | group_limit for pagination purposes. |
| 680 | |
| 681 | Returns: |
| 682 | List of groups, where each group is a tuple ``(key, actions)``. |
| 683 | In turn, ``key`` is a tuple containing the values of |
| 684 | ``(item, item_id)`` used to create the group and ``actions`` is a |
| 685 | list of ``vim_wim_actions`` records (dicts). |
| 686 | """ |
| 687 | |
| 688 | type_options = set( |
| 689 | '"{}"'.format(self.db.escape_string(t)) for t in item_types) |
| 690 | |
| 691 | items = ('SELECT DISTINCT a.item, a.item_id, a.wim_account_id ' |
| 692 | 'FROM vim_wim_actions AS a ' |
| 693 | 'WHERE a.wim_account_id="{}" AND a.item IN ({}) ' |
| 694 | 'ORDER BY a.item, a.item_id ' |
| 695 | 'LIMIT {:d},{:d}').format( |
| 696 | self.safe_str(wim_account_id), |
| 697 | ','.join(type_options), |
| 698 | group_offset, group_limit |
| 699 | ) |
| 700 | |
| 701 | join = 'vim_wim_actions NATURAL JOIN ({}) AS items'.format(items) |
| 702 | with self.lock: |
| 703 | db_results = self.db.get_rows( |
| 704 | FROM=join, ORDER_BY=('item', 'item_id', 'created_at')) |
| 705 | |
| 706 | results = (_postprocess_action(r) for r in db_results) |
| 707 | criteria = itemgetter('item', 'item_id') |
| 708 | return [(k, list(g)) for k, g in groupby(results, key=criteria)] |
| 709 | |
| 710 | def update_action(self, instance_action_id, task_index, properties): |
| 711 | condition = {'instance_action_id': instance_action_id, |
| 712 | 'task_index': task_index} |
| 713 | action = self.query_one('vim_wim_actions', WHERE=condition) |
| 714 | |
| 715 | extra = remove_none_items(merge_dicts( |
| 716 | action.get('extra') or {}, |
| 717 | properties.get('extra') or {})) |
| 718 | |
| 719 | updates = preprocess_record( |
| 720 | merge_dicts(action, properties, extra=extra)) |
| 721 | |
| 722 | with self.lock: |
| 723 | num_changes = self.db.update_rows('vim_wim_actions', |
| 724 | UPDATE=updates, WHERE=condition) |
| 725 | |
| 726 | if num_changes is None: |
| 727 | raise UnexpectedDatabaseError( |
| 728 | 'Impossible to update vim_wim_actions ' |
| 729 | '{instance_action_id}[{task_index}]'.format(*action)) |
| 730 | |
| 731 | return num_changes |
| 732 | |
| 733 | def get_wan_links(self, uuid=None, **kwargs): |
| 734 | """Retrieve WAN link records from the database |
| 735 | |
| 736 | Keyword Arguments: |
| 737 | uuid, instance_scenario_id, sce_net_id, wim_id, wim_account_id: |
| 738 | attributes that can be used at the WHERE clause |
| 739 | """ |
| 740 | kwargs.setdefault('uuid', uuid) |
| 741 | kwargs.setdefault('error_if_none', False) |
| 742 | |
| 743 | criteria_fields = ('uuid', 'instance_scenario_id', 'sce_net_id', |
| 744 | 'wim_id', 'wim_account_id') |
| 745 | criteria = remove_none_items(filter_dict_keys(kwargs, criteria_fields)) |
| 746 | kwargs = filter_out_dict_keys(kwargs, criteria_fields) |
| 747 | |
| 748 | return self.query('instance_wim_nets', WHERE=criteria, **kwargs) |
| 749 | |
| 750 | def update_wan_link(self, uuid, properties): |
| 751 | wan_link = self.get_by_uuid('instance_wim_nets', uuid) |
| 752 | |
| 753 | wim_info = remove_none_items(merge_dicts( |
| 754 | wan_link.get('wim_info') or {}, |
| 755 | properties.get('wim_info') or {})) |
| 756 | |
| 757 | updates = preprocess_record( |
| 758 | merge_dicts(wan_link, properties, wim_info=wim_info)) |
| 759 | |
| 760 | self.logger.debug({'UPDATE': updates}) |
| 761 | with self.lock: |
| 762 | num_changes = self.db.update_rows( |
| 763 | 'instance_wim_nets', UPDATE=updates, |
| 764 | WHERE={'uuid': wan_link['uuid']}) |
| 765 | |
| 766 | if num_changes is None: |
| 767 | raise UnexpectedDatabaseError( |
| 768 | 'Impossible to update instance_wim_nets ' + wan_link['uuid']) |
| 769 | |
| 770 | return num_changes |
| 771 | |
| 772 | def get_instance_nets(self, instance_scenario_id, sce_net_id, **kwargs): |
| 773 | """Retrieve all the instance nets related to the same instance_scenario |
| 774 | and scenario network |
| 775 | """ |
| 776 | return self.query( |
| 777 | 'instance_nets', |
| 778 | WHERE={'instance_scenario_id': instance_scenario_id, |
| 779 | 'sce_net_id': sce_net_id}, |
| 780 | ORDER_BY=kwargs.pop( |
| 781 | 'ORDER_BY', ('instance_scenario_id', 'sce_net_id')), |
| 782 | **kwargs) |
| 783 | |
| 784 | def update_instance_action_counters(self, uuid, failed=None, done=None): |
| 785 | """Atomically increment/decrement number_done and number_failed fields |
| 786 | in the instance action table |
| 787 | """ |
| 788 | changes = remove_none_items({ |
| 789 | 'number_failed': failed and {'INCREMENT': failed}, |
| 790 | 'number_done': done and {'INCREMENT': done} |
| 791 | }) |
| 792 | |
| 793 | if not changes: |
| 794 | return 0 |
| 795 | |
| 796 | with self.lock: |
| 797 | return self.db.update_rows('instance_actions', |
| 798 | WHERE={'uuid': uuid}, UPDATE=changes) |
| 799 | |
| 800 | def get_only_vm_with_external_net(self, instance_net_id, **kwargs): |
| 801 | """Return an instance VM if that is the only VM connected to an |
| 802 | external network identified by instance_net_id |
| 803 | """ |
| 804 | counting = ('SELECT DISTINCT instance_net_id ' |
| 805 | 'FROM instance_interfaces ' |
| 806 | 'WHERE instance_net_id="{}" AND type="external" ' |
| 807 | 'GROUP BY instance_net_id ' |
| 808 | 'HAVING COUNT(*)=1').format(self.safe_str(instance_net_id)) |
| 809 | |
| 810 | vm_item = ('SELECT DISTINCT instance_vm_id ' |
| 811 | 'FROM instance_interfaces NATURAL JOIN ({}) AS a' |
| 812 | .format(counting)) |
| 813 | |
| 814 | return self.query_one( |
| 815 | 'instance_vms JOIN ({}) as instance_interface ' |
| 816 | 'ON instance_vms.uuid=instance_interface.instance_vm_id' |
| 817 | .format(vm_item), **kwargs) |
| 818 | |
| 819 | def safe_str(self, string): |
| 820 | """Return a SQL safe string""" |
| 821 | return self.db.escape_string(string) |
| 822 | |
| 823 | def _generate_port_mapping_id(self, mapping_info): |
| 824 | """Given a port mapping represented by a dict with a 'type' field, |
| 825 | generate a unique string, in a injective way. |
| 826 | """ |
| 827 | mapping_info = mapping_info.copy() # Avoid mutating original object |
| 828 | mapping_type = mapping_info.pop('mapping_type', None) |
| 829 | if not mapping_type: |
| 830 | raise UndefinedWanMappingType(mapping_info) |
| 831 | |
| 832 | unique_fields = UNIQUE_PORT_MAPPING_INFO_FIELDS.get(mapping_type) |
| 833 | |
| 834 | if unique_fields: |
| 835 | mapping_info = filter_dict_keys(mapping_info, unique_fields) |
| 836 | else: |
| 837 | self.logger.warning('Unique fields for WIM port mapping of type ' |
| 838 | '%s not defined. Please add a list of fields ' |
| 839 | 'which combination should be unique in ' |
| 840 | 'UNIQUE_PORT_MAPPING_INFO_FIELDS ' |
| 841 | '(`wim/persistency.py) ', mapping_type) |
| 842 | |
| 843 | repeatable_repr = json.dumps(mapping_info, encoding='utf-8', |
| 844 | sort_keys=True, indent=False) |
| 845 | |
| 846 | return ':'.join([mapping_type, _str2id(repeatable_repr)]) |
| 847 | |
| 848 | |
| 849 | def _serialize(value): |
| 850 | """Serialize an arbitrary value in a consistent way, |
| 851 | so it can be stored in a database inside a text field |
| 852 | """ |
| 853 | return yaml.safe_dump(value, default_flow_style=True, width=256) |
| 854 | |
| 855 | |
| 856 | def _unserialize(text): |
| 857 | """Unserialize text representation into an arbitrary value, |
| 858 | so it can be loaded from the database |
| 859 | """ |
| 860 | return yaml.safe_load(text) |
| 861 | |
| 862 | |
| 863 | def preprocess_record(record): |
| 864 | """Small transformations to be applied to the data that cames from the |
| 865 | user before writing it to the database. By default, filter out timestamps, |
| 866 | and serialize the ``config`` field. |
| 867 | """ |
| 868 | automatic_fields = ['created_at', 'modified_at'] |
| 869 | record = serialize_fields(filter_out_dict_keys(record, automatic_fields)) |
| 870 | |
| 871 | return record |
| 872 | |
| 873 | |
| 874 | def _preprocess_wim_account(wim_account): |
| 875 | """Do the default preprocessing and convert the 'created' field from |
| 876 | boolean to string |
| 877 | """ |
| 878 | wim_account = preprocess_record(wim_account) |
| 879 | |
| 880 | created = wim_account.get('created') |
| 881 | wim_account['created'] = ( |
| 882 | 'true' if created is True or created == 'true' else 'false') |
| 883 | |
| 884 | return wim_account |
| 885 | |
| 886 | |
| 887 | def _postprocess_record(record, hide=_CONFIDENTIAL_FIELDS): |
| 888 | """By default, hide passwords fields, unserialize ``config`` fields, and |
| 889 | convert float timestamps to strings |
| 890 | """ |
| 891 | record = hide_confidential_fields(record, hide) |
| 892 | record = unserialize_fields(record, hide) |
| 893 | |
| 894 | convert_float_timestamp2str(record) |
| 895 | |
| 896 | return record |
| 897 | |
| 898 | |
| 899 | def _postprocess_action(action): |
| 900 | if action.get('extra'): |
| 901 | action['extra'] = _unserialize(action['extra']) |
| 902 | |
| 903 | return action |
| 904 | |
| 905 | |
| 906 | def _postprocess_wim_account(wim_account, hide=_CONFIDENTIAL_FIELDS): |
| 907 | """Do the default postprocessing and convert the 'created' field from |
| 908 | string to boolean |
| 909 | """ |
| 910 | # Fix fields from join |
| 911 | for field in ('type', 'description', 'wim_url'): |
| 912 | if field in wim_account: |
| 913 | wim_account['wim.'+field] = wim_account.pop(field) |
| 914 | |
| 915 | for field in ('id', 'nfvo_tenant_id', 'wim_account_id'): |
| 916 | if field in wim_account: |
| 917 | wim_account['association.'+field] = wim_account.pop(field) |
| 918 | |
| 919 | wim_account = _postprocess_record(wim_account, hide) |
| 920 | |
| 921 | created = wim_account.get('created') |
| 922 | wim_account['created'] = (created is True or created == 'true') |
| 923 | |
| 924 | return wim_account |
| 925 | |
| 926 | |
| 927 | def _postprocess_wim_port_mapping(mapping, hide=_CONFIDENTIAL_FIELDS): |
| 928 | mapping = _postprocess_record(mapping, hide=hide) |
| 929 | mapping_info = mapping.get('wan_service_mapping_info', None) or {} |
| 930 | mapping['wan_service_mapping_info'] = mapping_info |
| 931 | return mapping |
| 932 | |
| 933 | |
| 934 | def hide_confidential_fields(record, fields=_CONFIDENTIAL_FIELDS): |
| 935 | """Obfuscate confidential fields from the input dict. |
| 936 | |
| 937 | Note: |
| 938 | This function performs a SHALLOW operation. |
| 939 | """ |
| 940 | if not(isinstance(record, dict) and fields): |
| 941 | return record |
| 942 | |
| 943 | keys = record.iterkeys() |
| 944 | keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f)) |
| 945 | |
| 946 | return merge_dicts(record, {k: '********' for k in keys if record[k]}) |
| 947 | |
| 948 | |
| 949 | def unserialize_fields(record, hide=_CONFIDENTIAL_FIELDS, |
| 950 | fields=_SERIALIZED_FIELDS): |
| 951 | """Unserialize fields that where stored in the database as a serialized |
| 952 | YAML (or JSON) |
| 953 | """ |
| 954 | keys = record.iterkeys() |
| 955 | keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f)) |
| 956 | |
| 957 | return merge_dicts(record, { |
| 958 | key: hide_confidential_fields(_unserialize(record[key]), hide) |
| 959 | for key in keys if record[key] |
| 960 | }) |
| 961 | |
| 962 | |
| 963 | def serialize_fields(record, fields=_SERIALIZED_FIELDS): |
| 964 | """Serialize fields to be stored in the database as YAML""" |
| 965 | keys = record.iterkeys() |
| 966 | keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f)) |
| 967 | |
| 968 | return merge_dicts(record, { |
| 969 | key: _serialize(record[key]) |
| 970 | for key in keys if record[key] is not None |
| 971 | }) |
| 972 | |
| 973 | |
| 974 | def _decide_name_or_uuid(value): |
| 975 | reference = value |
| 976 | |
| 977 | if isinstance(value, (list, tuple)): |
| 978 | reference = value[0] if value else '' |
| 979 | |
| 980 | return 'uuid' if check_valid_uuid(reference) else 'name' |
| 981 | |
| 982 | |
| 983 | def _compose_where_from_uuids_or_names(**conditions): |
| 984 | """Create a dict containing the right conditions to be used in a database |
| 985 | query. |
| 986 | |
| 987 | This function chooses between ``names`` and ``uuid`` fields based on the |
| 988 | format of the passed string. |
| 989 | If a list is passed, the first element of the list will be used to choose |
| 990 | the name of the field. |
| 991 | If a ``None`` value is passed, ``uuid`` is used. |
| 992 | |
| 993 | Note that this function automatically translates ``tenant`` to |
| 994 | ``nfvo_tenant`` for the sake of brevity. |
| 995 | |
| 996 | Example: |
| 997 | >>> _compose_where_from_uuids_or_names( |
| 998 | wim='abcdef', |
| 999 | tenant=['xyz123', 'def456'] |
| 1000 | datacenter='5286a274-8a1b-4b8d-a667-9c94261ad855') |
| 1001 | {'wim.name': 'abcdef', |
| 1002 | 'nfvo_tenant.name': ['xyz123', 'def456'] |
| 1003 | 'datacenter.uuid': '5286a274-8a1b-4b8d-a667-9c94261ad855'} |
| 1004 | """ |
| 1005 | if 'tenant' in conditions: |
| 1006 | conditions['nfvo_tenant'] = conditions.pop('tenant') |
| 1007 | |
| 1008 | return { |
| 1009 | '{}.{}'.format(kind, _decide_name_or_uuid(value)): value |
| 1010 | for kind, value in conditions.items() if value |
| 1011 | } |
| 1012 | |
| 1013 | |
| 1014 | def _str2id(text): |
| 1015 | """Create an ID (following the UUID format) from a piece of arbitrary |
| 1016 | text. |
| 1017 | |
| 1018 | Different texts should generate different IDs, and the same text should |
| 1019 | generate the same ID in a repeatable way. |
| 1020 | """ |
| 1021 | return sha1(text).hexdigest() |