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