+++ /dev/null
-# -*- coding: utf-8 -*-
-##
-# Copyright 2018 University of Bristol - High Performance Networks Research
-# Group
-# All Rights Reserved.
-#
-# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
-# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
-#
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact with: <highperformance-networks@bristol.ac.uk>
-#
-# Neither the name of the University of Bristol nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# This work has been performed in the context of DCMS UK 5G Testbeds
-# & Trials Programme and in the framework of the Metro-Haul project -
-# funded by the European Commission under Grant number 761727 through the
-# Horizon 2020 and 5G-PPP programmes.
-##
-
-"""This module contains only logic related to managing records in a database
-which includes data format normalization, data format validation and etc.
-(It works as an extension to `nfvo_db.py` for the WIM feature)
-
-No domain logic/architectural concern should be present in this file.
-"""
-import json
-import logging
-import string
-from contextlib import contextmanager
-from hashlib import sha1
-from itertools import groupby
-from operator import itemgetter
-# from sys import exc_info
-# from time import time
-from uuid import uuid1 as generate_uuid
-
-import yaml
-
-from ..utils import (
- check_valid_uuid,
- convert_float_timestamp2str,
- expand_joined_fields,
- filter_dict_keys,
- filter_out_dict_keys,
- merge_dicts,
- remove_none_items
-)
-from .errors import (
- DbBaseException,
- InvalidParameters,
- MultipleRecordsFound,
- NoRecordFound,
- UndefinedUuidOrName,
- UndefinedWanMappingType,
- UnexpectedDatabaseError,
- WimAccountOverwrite,
- WimAndTenantAlreadyAttached
-)
-
-_WIM = 'wims AS wim '
-
-_WIM_JOIN = (
- _WIM +
- ' JOIN wim_nfvo_tenants AS association '
- ' ON association.wim_id=wim.uuid '
- ' JOIN nfvo_tenants AS nfvo_tenant '
- ' ON association.nfvo_tenant_id=nfvo_tenant.uuid '
- ' JOIN wim_accounts AS wim_account '
- ' ON association.wim_account_id=wim_account.uuid '
-)
-
-_WIM_ACCOUNT_JOIN = (
- 'wim_accounts AS wim_account '
- ' JOIN wim_nfvo_tenants AS association '
- ' ON association.wim_account_id=wim_account.uuid '
- ' JOIN wims AS wim '
- ' ON association.wim_id=wim.uuid '
- ' JOIN nfvo_tenants AS nfvo_tenant '
- ' ON association.nfvo_tenant_id=nfvo_tenant.uuid '
-)
-
-_DATACENTER_JOIN = (
- 'datacenters AS datacenter '
- ' JOIN tenants_datacenters AS association '
- ' ON association.datacenter_id=datacenter.uuid '
- ' JOIN datacenter_tenants as datacenter_account '
- ' ON association.datacenter_tenant_id=datacenter_account.uuid '
- ' JOIN nfvo_tenants AS nfvo_tenant '
- ' ON association.nfvo_tenant_id=nfvo_tenant.uuid '
-)
-
-_PORT_MAPPING = 'wim_port_mappings as wim_port_mapping '
-
-_PORT_MAPPING_JOIN_WIM = (
- ' JOIN wims as wim '
- ' ON wim_port_mapping.wim_id=wim.uuid '
-)
-
-_PORT_MAPPING_JOIN_DATACENTER = (
- ' JOIN datacenters as datacenter '
- ' ON wim_port_mapping.datacenter_id=datacenter.uuid '
-)
-
-_WIM_SELECT = [
- 'wim.{0} as {0}'.format(_field)
- for _field in 'uuid name description wim_url type config '
- 'created_at modified_at'.split()
-]
-
-_WIM_ACCOUNT_SELECT = 'uuid name user password config'.split()
-
-_PORT_MAPPING_SELECT = ('wim_port_mapping.*', )
-
-_CONFIDENTIAL_FIELDS = ('password', 'passwd')
-
-_SERIALIZED_FIELDS = ('config', 'vim_info', 'wim_info', 'conn_info', 'extra',
- 'service_mapping_info')
-
-UNIQUE_PORT_MAPPING_INFO_FIELDS = {
- 'dpid-port': ('switch_dpid', 'switch_port')
-}
-"""Fields that should be unique for each port mapping that relies on
-service_mapping_info.
-
-For example, for port mappings of type 'dpid-port', each combination of
-switch_dpid and switch_port should be unique (the same switch cannot
-be connected to two different places using the same port)
-"""
-
-
-class WimPersistence(object):
- """High level interactions with the WIM tables in the database"""
-
- def __init__(self, db, logger=None):
- self.db = db
- self.logger = logger or logging.getLogger('openmano.wim.persistence')
-
- def query(self,
- FROM=None,
- SELECT=None,
- WHERE=None,
- ORDER_BY=None,
- LIMIT=None,
- OFFSET=None,
- error_if_none=True,
- error_if_multiple=False,
- postprocess=None,
- hide=_CONFIDENTIAL_FIELDS,
- **kwargs):
- """Retrieve records from the database.
-
- Keyword Arguments:
- SELECT, FROM, WHERE, LIMIT, ORDER_BY: used to compose the SQL
- query. See ``nfvo_db.get_rows``.
- OFFSET: only valid when used togheter with LIMIT.
- Ignore the OFFSET first results of the query.
- error_if_none: by default an error is raised if no record is
- found. With this option it is possible to disable this error.
- error_if_multiple: by default no error is raised if more then one
- record is found.
- With this option it is possible to enable this error.
- postprocess: function applied to every retrieved record.
- This function receives a dict as input and must return it
- after modifications. Moreover this function should accept a
- second optional parameter ``hide`` indicating
- the confidential fiels to be obfuscated.
- By default a minimal postprocessing function is applied,
- obfuscating confidential fields and converting timestamps.
- hide: option proxied to postprocess
-
- All the remaining keyword arguments will be assumed to be ``name``s or
- ``uuid``s to compose the WHERE statement, according to their format.
- If the value corresponds to an array, the first element will determine
- if it is an name or UUID.
-
- For example:
- - ``wim="abcdef"``` will be turned into ``wim.name="abcdef"``,
- - ``datacenter="5286a274-8a1b-4b8d-a667-9c94261ad855"``
- will be turned into
- ``datacenter.uuid="5286a274-8a1b-4b8d-a667-9c94261ad855"``.
- - ``wim=["5286a274-8a1b-4b8d-a667-9c94261ad855", ...]``
- will be turned into
- ``wim.uuid=["5286a274-8a1b-4b8d-a667-9c94261ad855", ...]``
-
- Raises:
- NoRecordFound: if the query result set is empty
- DbBaseException: errors occuring during the execution of the query.
- """
- # Defaults:
- postprocess = postprocess or _postprocess_record
- WHERE = WHERE or {}
-
- # Find remaining keywords by name or uuid
- WHERE.update(_compose_where_from_uuids_or_names(**kwargs))
- WHERE = WHERE or None
- # ^ If the where statement is empty, it is better to leave it as None,
- # so it can be filtered out at a later stage
- LIMIT = ('{:d},{:d}'.format(OFFSET, LIMIT)
- if LIMIT and OFFSET else LIMIT)
-
- query = remove_none_items({
- 'SELECT': SELECT, 'FROM': FROM, 'WHERE': WHERE,
- 'LIMIT': LIMIT, 'ORDER_BY': ORDER_BY})
-
- records = self.db.get_rows(**query)
-
- table = FROM.split()[0]
- if error_if_none and not records:
- raise NoRecordFound(WHERE, table)
-
- if error_if_multiple and len(records) > 1:
- self.logger.error('Multiple records '
- 'FROM %s WHERE %s:\n\n%s\n\n',
- FROM, WHERE, json.dumps(records, indent=4))
- raise MultipleRecordsFound(WHERE, table)
-
- return [
- expand_joined_fields(postprocess(record, hide))
- for record in records
- ]
-
- def query_one(self, *args, **kwargs):
- """Similar to ``query``, but ensuring just one result.
- ``error_if_multiple`` is enabled by default.
- """
- kwargs.setdefault('error_if_multiple', True)
- records = self.query(*args, **kwargs)
- return records[0] if records else None
-
- def get_by_uuid(self, table, uuid, **kwargs):
- """Retrieve one record from the database based on its uuid
-
- Arguments:
- table (str): table name (to be used in SQL's FROM statement).
- uuid (str): unique identifier for record.
-
- For additional keyword arguments and exceptions see :obj:`~.query`
- (``error_if_multiple`` is enabled by default).
- """
- if uuid is None:
- raise UndefinedUuidOrName(table)
- return self.query_one(table, WHERE={'uuid': uuid}, **kwargs)
-
- def get_by_name_or_uuid(self, table, uuid_or_name, **kwargs):
- """Retrieve a record from the database based on a value that can be its
- uuid or name.
-
- Arguments:
- table (str): table name (to be used in SQL's FROM statement).
- uuid_or_name (str): this value can correspond to either uuid or
- name
- For additional keyword arguments and exceptions see :obj:`~.query`
- (``error_if_multiple`` is enabled by default).
- """
- if uuid_or_name is None:
- raise UndefinedUuidOrName(table)
-
- key = 'uuid' if check_valid_uuid(uuid_or_name) else 'name'
- return self.query_one(table, WHERE={key: uuid_or_name}, **kwargs)
-
- def get_wims(self, uuid_or_name=None, tenant=None, **kwargs):
- """Retrieve information about one or more WIMs stored in the database
-
- Arguments:
- uuid_or_name (str): uuid or name for WIM
- tenant (str): [optional] uuid or name for NFVO tenant
-
- See :obj:`~.query` for additional keyword arguments.
- """
- kwargs.update(wim=uuid_or_name, tenant=tenant)
- from_ = _WIM_JOIN if tenant else _WIM
- select_ = _WIM_SELECT[:] + (['wim_account.*'] if tenant else [])
-
- kwargs.setdefault('SELECT', select_)
- return self.query(from_, **kwargs)
-
- def get_wim(self, wim, tenant=None, **kwargs):
- """Similar to ``get_wims`` but ensure only one result is returned"""
- kwargs.setdefault('error_if_multiple', True)
- return self.get_wims(wim, tenant)[0]
-
- def create_wim(self, wim_descriptor):
- """Create a new wim record inside the database and returns its uuid
-
- Arguments:
- wim_descriptor (dict): properties of the record
- (usually each field corresponds to a database column, but extra
- information can be offloaded to another table or serialized as
- JSON/YAML)
- Returns:
- str: UUID of the created WIM
- """
- if "config" in wim_descriptor:
- wim_descriptor["config"] = _serialize(wim_descriptor["config"])
-
- url = wim_descriptor["wim_url"]
- wim_descriptor["wim_url"] = url.strip(string.whitespace + "/")
- # ^ This avoid the common problem caused by trailing spaces/slashes in
- # the URL (due to CTRL+C/CTRL+V)
-
- return self.db.new_row(
- "wims", wim_descriptor, add_uuid=True, confidential_data=True)
-
- def update_wim(self, uuid_or_name, wim_descriptor):
- """Change an existing WIM record on the database"""
- # obtain data, check that only one exist
- wim = self.get_by_name_or_uuid('wims', uuid_or_name)
-
- # edit data
- wim_id = wim['uuid']
- where = {'uuid': wim['uuid']}
-
- # unserialize config, edit and serialize it again
- new_config_dict = wim_descriptor.get('config', {}) or {}
- config_dict = remove_none_items(merge_dicts(
- wim.get('config', {}) or {}, new_config_dict))
- wim_descriptor['config'] = (
- _serialize(config_dict) if config_dict else None)
-
- self.db.update_rows('wims', wim_descriptor, where)
-
- return wim_id
-
- def delete_wim(self, wim):
- # get nfvo_tenant info
- wim = self.get_by_name_or_uuid('wims', wim)
-
- self.db.delete_row_by_id('wims', wim['uuid'])
-
- return wim['uuid'] + ' ' + wim['name']
-
- def get_wim_accounts_by(self, wim=None, tenant=None, uuid=None, **kwargs):
- """Retrieve WIM account information from the database together
- with the related records (wim, nfvo_tenant and wim_nfvo_tenant)
-
- Arguments:
- wim (str): uuid or name for WIM
- tenant (str): [optional] uuid or name for NFVO tenant
-
- See :obj:`~.query` for additional keyword arguments.
- """
- kwargs.update(wim=wim, tenant=tenant)
- kwargs.setdefault('postprocess', _postprocess_wim_account)
- if uuid:
- kwargs.setdefault('WHERE', {'wim_account.uuid': uuid})
- return self.query(FROM=_WIM_ACCOUNT_JOIN, **kwargs)
-
- def get_wim_account_by(self, wim=None, tenant=None, uuid=None, **kwargs):
- """Similar to ``get_wim_accounts_by``, but ensuring just one result"""
- kwargs.setdefault('error_if_multiple', True)
- return self.get_wim_accounts_by(wim, tenant, uuid, **kwargs)[0]
-
- def get_wim_accounts(self, **kwargs):
- """Retrieve all the accounts from the database"""
- kwargs.setdefault('postprocess', _postprocess_wim_account)
- kwargs.setdefault('WHERE', {"sdn": "false"})
- return self.query(FROM=_WIM_ACCOUNT_JOIN, **kwargs)
-
- def get_wim_account(self, uuid_or_name, **kwargs):
- """Retrieve WIM Account record by UUID or name,
- See :obj:`get_by_name_or_uuid` for keyword arguments.
- """
- kwargs.setdefault('postprocess', _postprocess_wim_account)
- kwargs.setdefault('SELECT', _WIM_ACCOUNT_SELECT)
- return self.get_by_name_or_uuid('wim_accounts', uuid_or_name, **kwargs)
-
- @contextmanager
- def _associate(self, wim_id, nfvo_tenant_id):
- """Auxiliary method for ``create_wim_account``
-
- This method just create a row in the association table
- ``wim_nfvo_tenants``
- """
- try:
- yield
- except DbBaseException as db_exception:
- error_msg = str(db_exception)
- if all([msg in error_msg
- for msg in ("already in use", "'wim_nfvo_tenant'")]):
- ex = WimAndTenantAlreadyAttached(wim_id, nfvo_tenant_id)
- raise ex from db_exception
- raise
-
- def create_wim_account(self, wim, tenant, properties):
- """Associate a wim to a tenant using the ``wim_nfvo_tenants`` table
- and create a ``wim_account`` to store credentials and configurations.
-
- For the sake of simplification, we assume that each NFVO tenant can be
- attached to a WIM using only one WIM account. This is automatically
- guaranteed via database constraints.
- For corner cases, the same WIM can be registered twice using another
- name.
-
- Arguments:
- wim (str): name or uuid of the WIM related to the account being
- created
- tenant (str): name or uuid of the nfvo tenant to which the account
- will be created
- properties (dict): properties of the account
- (eg. user, password, ...)
- """
- wim_id = self.get_by_name_or_uuid('wims', wim, SELECT=['uuid'])['uuid']
- tenant = self.get_by_name_or_uuid('nfvo_tenants', tenant,
- SELECT=['uuid', 'name'])
- account = properties.setdefault('name', tenant['name'])
-
- wim_account = self.query_one('wim_accounts',
- WHERE={'wim_id': wim_id, 'name': account},
- error_if_none=False)
-
- transaction = []
- used_uuids = []
-
- if wim_account is None:
- # If a row for the wim account doesn't exist yet, we need to
- # create one, otherwise we can just re-use it.
- account_id = str(generate_uuid())
- used_uuids.append(account_id)
- row = merge_dicts(properties, wim_id=wim_id, uuid=account_id)
- transaction.append({'wim_accounts': _preprocess_wim_account(row)})
- else:
- account_id = wim_account['uuid']
- properties.pop('config', None) # Config is too complex to compare
- diff = {k: v for k, v in properties.items() if v != wim_account[k]}
- if diff:
- tip = 'Edit the account first, and then attach it to a tenant'
- raise WimAccountOverwrite(wim_account, diff, tip)
-
- transaction.append({
- 'wim_nfvo_tenants': {'nfvo_tenant_id': tenant['uuid'],
- 'wim_id': wim_id,
- 'wim_account_id': account_id}})
-
- with self._associate(wim_id, tenant['uuid']):
- self.db.new_rows(transaction, used_uuids, confidential_data=True)
-
- return account_id
-
- def update_wim_account(self, uuid, properties, hide=_CONFIDENTIAL_FIELDS):
- """Update WIM account record by overwriting fields with new values
-
- Specially for the field ``config`` this means that a new dict will be
- merged to the existing one.
-
- Attributes:
- uuid (str): UUID for the WIM account
- properties (dict): fields that should be overwritten
-
- Returns:
- Updated wim_account
- """
- wim_account = self.get_by_uuid('wim_accounts', uuid)
- safe_fields = 'user password name created'.split()
- updates = _preprocess_wim_account(
- merge_dicts(wim_account, filter_dict_keys(properties, safe_fields))
- )
-
- if properties.get('config'):
- old_config = wim_account.get('config') or {}
- new_config = merge_dicts(old_config, properties['config'])
- updates['config'] = _serialize(new_config)
-
- num_changes = self.db.update_rows('wim_accounts', UPDATE=updates,
- WHERE={'uuid': wim_account['uuid']})
-
- if num_changes is None:
- raise UnexpectedDatabaseError('Impossible to update wim_account '
- '{name}:{uuid}'.format(*wim_account))
-
- return self.get_wim_account(wim_account['uuid'], hide=hide)
-
- def delete_wim_account(self, uuid):
- """Remove WIM account record from the database"""
- # Since we have foreign keys configured with ON CASCADE, we can rely
- # on the database engine to guarantee consistency, deleting the
- # dependant records
- return self.db.delete_row_by_id('wim_accounts', uuid)
-
- def get_datacenters_by(self, datacenter=None, tenant=None, **kwargs):
- """Retrieve datacenter information from the database together
- with the related records (nfvo_tenant)
-
- Arguments:
- datacenter (str): uuid or name for datacenter
- tenant (str): [optional] uuid or name for NFVO tenant
-
- See :obj:`~.query` for additional keyword arguments.
- """
- if tenant:
- kwargs.update(datacenter=datacenter, tenant=tenant)
- return self.query(_DATACENTER_JOIN, **kwargs)
- else:
- return [self.get_by_name_or_uuid('datacenters',
- datacenter, **kwargs)]
-
- def get_datacenter_by(self, datacenter=None, tenant=None, **kwargs):
- """Similar to ``get_datacenters_by``, but ensuring just one result"""
- kwargs.setdefault('error_if_multiple', True)
- return self.get_datacenters_by(datacenter, tenant, **kwargs)[0]
-
- def _create_single_port_mapping(self, properties):
- info = properties.setdefault('service_mapping_info', {})
- endpoint_id = properties.get('service_endpoint_id')
-
- if info.get('mapping_type') and not endpoint_id:
- properties['service_endpoint_id'] = (
- self._generate_port_mapping_id(info))
-
- properties['service_mapping_info'] = _serialize(info)
-
- try:
- self.db.new_row('wim_port_mappings', properties,
- add_uuid=False, confidential_data=True)
- except DbBaseException as old_exception:
- self.logger.exception(old_exception)
- ex = InvalidParameters(
- "The mapping must contain the "
- "'device_id', 'device_interface_id', and "
- "service_mapping_info: "
- "('switch_dpid' and 'switch_port') or "
- "'service_endpoint_id}'")
- raise ex from old_exception
-
- return properties
-
- def create_wim_port_mappings(self, wim, port_mappings, tenant=None):
- if not isinstance(wim, dict):
- wim = self.get_by_name_or_uuid('wims', wim)
-
- for port_mapping in port_mappings:
- port_mapping['wim_name'] = wim['name']
- datacenter = self.get_datacenter_by(
- port_mapping['datacenter_name'], tenant)
- for pop_wan_port_mapping in port_mapping['pop_wan_mappings']:
- element = merge_dicts(pop_wan_port_mapping, {
- 'wim_id': wim['uuid'],
- 'datacenter_id': datacenter['uuid']})
- self._create_single_port_mapping(element)
-
- return port_mappings
-
- def _filter_port_mappings_by_tenant(self, mappings, tenant):
- """Make sure all the datacenters and wims listed in the port mapping
- belong to an specific tenant
- """
-
- # NOTE: Theoretically this could be done at SQL level, but given the
- # number of tables involved (wim_port_mappings, wim_accounts,
- # wims, wim_nfvo_tenants, datacenters, datacenter_tenants,
- # tenants_datacents and nfvo_tenants), it would result in a
- # extremely complex query. Moreover, the predicate can vary:
- # for `get_wim_port_mappings` we can have any combination of
- # (wim, datacenter, tenant), not all of them having the 3 values
- # so we have combinatorial trouble to write the 'FROM' statement.
-
- kwargs = {'tenant': tenant, 'error_if_none': False}
- # Cache results to speedup things
- datacenters = {}
- wims = {}
-
- def _get_datacenter(uuid):
- return (
- datacenters.get(uuid) or
- datacenters.setdefault(
- uuid, self.get_datacenters_by(uuid, **kwargs)))
-
- def _get_wims(uuid):
- return (wims.get(uuid) or
- wims.setdefault(uuid, self.get_wims(uuid, **kwargs)))
-
- return [
- mapping
- for mapping in mappings
- if (_get_datacenter(mapping['datacenter_id']) and
- _get_wims(mapping['wim_id']))
- ]
-
- def get_wim_port_mappings(self, wim=None, datacenter=None, tenant=None,
- **kwargs):
- """List all the port mappings, optionally filtering by wim, datacenter
- AND/OR tenant
- """
- from_ = [_PORT_MAPPING,
- _PORT_MAPPING_JOIN_WIM if wim else '',
- _PORT_MAPPING_JOIN_DATACENTER if datacenter else '']
-
- criteria = ('wim_id', 'datacenter_id')
- kwargs.setdefault('error_if_none', False)
- mappings = self.query(
- ' '.join(from_),
- SELECT=_PORT_MAPPING_SELECT,
- ORDER_BY=['wim_port_mapping.{}'.format(c) for c in criteria],
- wim=wim, datacenter=datacenter,
- postprocess=_postprocess_wim_port_mapping,
- **kwargs)
-
- if tenant:
- mappings = self._filter_port_mappings_by_tenant(mappings, tenant)
-
- # We don't have to sort, since we have used 'ORDER_BY'
- grouped_mappings = groupby(mappings, key=itemgetter(*criteria))
-
- return [
- {'wim_id': key[0],
- 'datacenter_id': key[1],
- 'pop_wan_mappings': [
- filter_out_dict_keys(mapping, (
- 'id', 'wim_id', 'datacenter_id',
- 'created_at', 'modified_at'))
- for mapping in group]}
- for key, group in grouped_mappings
- ]
-
- def delete_wim_port_mappings(self, wim_id):
- self.db.delete_row(FROM='wim_port_mappings', WHERE={"wim_id": wim_id})
- return "port mapping for wim {} deleted.".format(wim_id)
-
- def update_wim_port_mapping(self, id, properties):
- original = self.query_one('wim_port_mappings', WHERE={'id': id})
-
- mapping_info = remove_none_items(merge_dicts(
- original.get('service_mapping_info') or {},
- properties.get('service_mapping_info') or {}))
-
- updates = preprocess_record(
- merge_dicts(original, remove_none_items(properties),
- service_mapping_info=mapping_info))
-
- num_changes = self.db.update_rows('wim_port_mappings',
- UPDATE=updates, WHERE={'id': id})
-
- if num_changes is None:
- raise UnexpectedDatabaseError(
- 'Impossible to update wim_port_mappings {}:\n{}\n'.format(
- id, _serialize(properties))
- )
-
- return num_changes
-
- def get_actions_in_groups(self, wim_account_id,
- item_types=('instance_wim_nets',),
- group_offset=0, group_limit=150):
- """Retrieve actions from the database in groups.
- Each group contains all the actions that have the same ``item`` type
- and ``item_id``.
-
- Arguments:
- wim_account_id: restrict the search to actions to be performed
- using the same account
- item_types (list): [optional] filter the actions to the given
- item types
- group_limit (int): maximum number of groups returned by the
- function
- group_offset (int): skip the N first groups. Used together with
- group_limit for pagination purposes.
-
- Returns:
- List of groups, where each group is a tuple ``(key, actions)``.
- In turn, ``key`` is a tuple containing the values of
- ``(item, item_id)`` used to create the group and ``actions`` is a
- list of ``vim_wim_actions`` records (dicts).
- """
-
- type_options = set(
- '"{}"'.format(self.db.escape_string(t)) for t in item_types)
-
- items = ('SELECT DISTINCT a.item, a.item_id, a.wim_account_id '
- 'FROM vim_wim_actions AS a '
- 'WHERE a.wim_account_id="{}" AND a.item IN ({}) '
- 'ORDER BY a.item, a.item_id '
- 'LIMIT {:d},{:d}').format(
- self.safe_str(wim_account_id),
- ','.join(type_options),
- group_offset, group_limit)
-
- join = 'vim_wim_actions NATURAL JOIN ({}) AS items'.format(items)
- db_results = self.db.get_rows(
- FROM=join, ORDER_BY=('item', 'item_id', 'created_at'))
-
- results = (_postprocess_action(r) for r in db_results)
- criteria = itemgetter('item', 'item_id')
- return [(k, list(g)) for k, g in groupby(results, key=criteria)]
-
- def update_action(self, instance_action_id, task_index, properties):
- condition = {'instance_action_id': instance_action_id,
- 'task_index': task_index}
- try:
- action = self.query_one('vim_wim_actions', WHERE=condition)
- except Exception:
- actions = self.query('vim_wim_actions', WHERE=condition)
- self.logger.error('More then one action found:\n%s',
- json.dumps(actions, indent=4))
- action = actions[0]
-
- extra = remove_none_items(merge_dicts(
- action.get('extra') or {},
- properties.get('extra') or {}))
-
- updates = preprocess_record(
- merge_dicts(action, properties, extra=extra))
-
- num_changes = self.db.update_rows('vim_wim_actions', UPDATE=updates, WHERE=condition)
-
- if num_changes is None:
- raise UnexpectedDatabaseError(
- 'Impossible to update vim_wim_actions '
- '{instance_action_id}[{task_index}]'.format(*action))
-
- return num_changes
-
- def get_wan_links(self, uuid=None, **kwargs):
- """Retrieve WAN link records from the database
-
- Keyword Arguments:
- uuid, instance_scenario_id, sce_net_id, wim_id, wim_account_id:
- attributes that can be used at the WHERE clause
- """
- kwargs.setdefault('uuid', uuid)
- kwargs.setdefault('error_if_none', False)
-
- criteria_fields = ('uuid', 'instance_scenario_id', 'sce_net_id',
- 'wim_id', 'wim_account_id', 'sdn')
- criteria = remove_none_items(filter_dict_keys(kwargs, criteria_fields))
- kwargs = filter_out_dict_keys(kwargs, criteria_fields)
-
- return self.query('instance_wim_nets', WHERE=criteria, **kwargs)
-
- def update_wan_link(self, uuid, properties):
- wan_link = self.get_by_uuid('instance_wim_nets', uuid)
-
- wim_info = remove_none_items(merge_dicts(
- wan_link.get('wim_info') or {},
- properties.get('wim_info') or {}))
-
- updates = preprocess_record(
- merge_dicts(wan_link, properties, wim_info=wim_info))
-
- self.logger.debug({'UPDATE': updates})
- num_changes = self.db.update_rows(
- 'instance_wim_nets', UPDATE=updates,
- WHERE={'uuid': wan_link['uuid']})
-
- if num_changes is None:
- raise UnexpectedDatabaseError(
- 'Impossible to update instance_wim_nets ' + wan_link['uuid'])
-
- return num_changes
-
- def get_instance_nets(self, instance_scenario_id, sce_net_id, **kwargs):
- """Retrieve all the instance nets related to the same instance_scenario
- and scenario network
- """
- return self.query(
- 'instance_nets',
- WHERE={'instance_scenario_id': instance_scenario_id,
- 'sce_net_id': sce_net_id},
- ORDER_BY=kwargs.pop(
- 'ORDER_BY', ('instance_scenario_id', 'sce_net_id')),
- **kwargs)
-
- def update_instance_action_counters(self, uuid, failed=None, done=None):
- """Atomically increment/decrement number_done and number_failed fields
- in the instance action table
- """
- changes = remove_none_items({
- 'number_failed': failed and {'INCREMENT': failed},
- 'number_done': done and {'INCREMENT': done}
- })
-
- if not changes:
- return 0
-
- return self.db.update_rows('instance_actions', WHERE={'uuid': uuid}, UPDATE=changes)
-
- def get_only_vm_with_external_net(self, instance_net_id, **kwargs):
- """Return an instance VM if that is the only VM connected to an
- external network identified by instance_net_id
- """
- counting = ('SELECT DISTINCT instance_net_id '
- 'FROM instance_interfaces '
- 'WHERE instance_net_id="{}" AND type="external" '
- 'GROUP BY instance_net_id '
- 'HAVING COUNT(*)=1').format(self.safe_str(instance_net_id))
-
- vm_item = ('SELECT DISTINCT instance_vm_id '
- 'FROM instance_interfaces NATURAL JOIN ({}) AS a'
- .format(counting))
-
- return self.query_one(
- 'instance_vms JOIN ({}) as instance_interface '
- 'ON instance_vms.uuid=instance_interface.instance_vm_id'
- .format(vm_item), **kwargs)
-
- def safe_str(self, string):
- """Return a SQL safe string"""
- return self.db.escape_string(string)
-
- def reconnect(self):
- self.db.reconnect()
-
- def _generate_port_mapping_id(self, mapping_info):
- """Given a port mapping represented by a dict with a 'type' field,
- generate a unique string, in a injective way.
- """
- mapping_info = mapping_info.copy() # Avoid mutating original object
- mapping_type = mapping_info.pop('mapping_type', None)
- if not mapping_type:
- raise UndefinedWanMappingType(mapping_info)
-
- unique_fields = UNIQUE_PORT_MAPPING_INFO_FIELDS.get(mapping_type)
-
- if unique_fields:
- mapping_info = filter_dict_keys(mapping_info, unique_fields)
- else:
- self.logger.warning('Unique fields for WIM port mapping of type '
- '%s not defined. Please add a list of fields '
- 'which combination should be unique in '
- 'UNIQUE_PORT_MAPPING_INFO_FIELDS '
- '(`wim/persistency.py) ', mapping_type)
-
- repeatable_repr = json.dumps(mapping_info, encoding='utf-8',
- sort_keys=True, indent=False)
-
- return ':'.join([mapping_type, _str2id(repeatable_repr)])
-
-
-def _serialize(value):
- """Serialize an arbitrary value in a consistent way,
- so it can be stored in a database inside a text field
- """
- return yaml.safe_dump(value, default_flow_style=True, width=256)
-
-
-def _unserialize(text):
- """Unserialize text representation into an arbitrary value,
- so it can be loaded from the database
- """
- return yaml.safe_load(text)
-
-
-def preprocess_record(record):
- """Small transformations to be applied to the data that cames from the
- user before writing it to the database. By default, filter out timestamps,
- and serialize the ``config`` field.
- """
- automatic_fields = ['created_at', 'modified_at']
- record = serialize_fields(filter_out_dict_keys(record, automatic_fields))
-
- return record
-
-
-def _preprocess_wim_account(wim_account):
- """Do the default preprocessing and convert the 'created' field from
- boolean to string
- """
- wim_account = preprocess_record(wim_account)
-
- wim_account['sdn'] = False
- return wim_account
-
-
-def _postprocess_record(record, hide=_CONFIDENTIAL_FIELDS):
- """By default, hide passwords fields, unserialize ``config`` fields, and
- convert float timestamps to strings
- """
- record = hide_confidential_fields(record, hide)
- record = unserialize_fields(record, hide)
-
- convert_float_timestamp2str(record)
-
- return record
-
-
-def _postprocess_action(action):
- if action.get('extra'):
- action['extra'] = _unserialize(action['extra'])
-
- return action
-
-
-def _postprocess_wim_account(wim_account, hide=_CONFIDENTIAL_FIELDS):
- """Do the default postprocessing and convert the 'created' field from
- string to boolean
- """
- # Fix fields from join
- for field in ('type', 'description', 'wim_url'):
- if field in wim_account:
- wim_account['wim.'+field] = wim_account.pop(field)
-
- for field in ('id', 'nfvo_tenant_id', 'wim_account_id'):
- if field in wim_account:
- wim_account['association.'+field] = wim_account.pop(field)
-
- wim_account = _postprocess_record(wim_account, hide)
-
- created = wim_account.get('created')
- wim_account['created'] = (created is True or created == 'true')
-
- return wim_account
-
-
-def _postprocess_wim_port_mapping(mapping, hide=_CONFIDENTIAL_FIELDS):
- mapping = _postprocess_record(mapping, hide=hide)
- mapping_info = mapping.get('service_mapping_info', None) or {}
- mapping['service_mapping_info'] = mapping_info
- return mapping
-
-
-def hide_confidential_fields(record, fields=_CONFIDENTIAL_FIELDS):
- """Obfuscate confidential fields from the input dict.
-
- Note:
- This function performs a SHALLOW operation.
- """
- if not(isinstance(record, dict) and fields):
- return record
-
- keys = list(record.keys())
- keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f))
-
- return merge_dicts(record, {k: '********' for k in keys if record[k]})
-
-
-def unserialize_fields(record, hide=_CONFIDENTIAL_FIELDS,
- fields=_SERIALIZED_FIELDS):
- """Unserialize fields that where stored in the database as a serialized
- YAML (or JSON)
- """
- keys = list(record.keys())
- keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f))
-
- return merge_dicts(record, {
- key: hide_confidential_fields(_unserialize(record[key]), hide)
- for key in keys if record[key]
- })
-
-
-def serialize_fields(record, fields=_SERIALIZED_FIELDS):
- """Serialize fields to be stored in the database as YAML"""
- keys = list(record.keys())
- keys = (k for k in keys for f in fields if k == f or k.endswith('.'+f))
-
- return merge_dicts(record, {
- key: _serialize(record[key])
- for key in keys if record[key] is not None
- })
-
-
-def _decide_name_or_uuid(value):
- reference = value
-
- if isinstance(value, (list, tuple)):
- reference = value[0] if value else ''
-
- return 'uuid' if check_valid_uuid(reference) else 'name'
-
-
-def _compose_where_from_uuids_or_names(**conditions):
- """Create a dict containing the right conditions to be used in a database
- query.
-
- This function chooses between ``names`` and ``uuid`` fields based on the
- format of the passed string.
- If a list is passed, the first element of the list will be used to choose
- the name of the field.
- If a ``None`` value is passed, ``uuid`` is used.
-
- Note that this function automatically translates ``tenant`` to
- ``nfvo_tenant`` for the sake of brevity.
-
- Example:
- >>> _compose_where_from_uuids_or_names(
- wim='abcdef',
- tenant=['xyz123', 'def456']
- datacenter='5286a274-8a1b-4b8d-a667-9c94261ad855')
- {'wim.name': 'abcdef',
- 'nfvo_tenant.name': ['xyz123', 'def456']
- 'datacenter.uuid': '5286a274-8a1b-4b8d-a667-9c94261ad855'}
- """
- if 'tenant' in conditions:
- conditions['nfvo_tenant'] = conditions.pop('tenant')
-
- return {
- '{}.{}'.format(kind, _decide_name_or_uuid(value)): value
- for kind, value in conditions.items() if value
- }
-
-
-def _str2id(text):
- """Create an ID (following the UUID format) from a piece of arbitrary
- text.
-
- Different texts should generate different IDs, and the same text should
- generate the same ID in a repeatable way.
- """
- return sha1(text).hexdigest()