blob: 2e72640b6dc8f9c9107f61ae4702e8e7c68118c4 [file] [log] [blame]
# -*- 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
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
from six import reraise
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',
'wan_service_mapping_info')
UNIQUE_PORT_MAPPING_INFO_FIELDS = {
'dpid-port': ('wan_switch_dpid', 'wan_switch_port')
}
"""Fields that should be unique for each port mapping that relies on
wan_service_mapping_info.
For example, for port mappings of type 'dpid-port', each combination of
wan_switch_dpid and wan_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"])
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)
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)
reraise(ex.__class__, ex, exc_info()[2])
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('wan_service_mapping_info', {})
endpoint_id = properties.get('wan_service_endpoint_id')
if info.get('mapping_type') and not endpoint_id:
properties['wan_service_endpoint_id'] = (
self._generate_port_mapping_id(info))
properties['wan_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 "
"'pop_switch_dpid', 'pop_switch_port', and "
"wan_service_mapping_info: "
"('wan_switch_dpid' and 'wan_switch_port') or "
"'wan_service_endpoint_id}'")
reraise(ex.__class__, ex, exc_info()[2])
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('wan_service_mapping_info') or {},
properties.get('wan_service_mapping_info') or {}))
updates = preprocess_record(
merge_dicts(original, remove_none_items(properties),
wan_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 %s:\n%s\n',
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:
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')
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)
created = wim_account.get('created')
wim_account['created'] = (
'true' if created is True or created == 'true' else '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('wan_service_mapping_info', None) or {}
mapping['wan_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 = record.iterkeys()
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 = record.iterkeys()
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 = record.iterkeys()
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()