diff --git a/lower-constraints.txt b/lower-constraints.txt index d90640a64..12b574419 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -19,7 +19,7 @@ coverage==4.0 cryptography==2.1 ddt===1.0.1 debtcollector==1.19.0 -decorator==4.2.1 +decorator==4.4.1 deprecation==2.0 doc8==0.6.0 docutils==0.14 @@ -46,8 +46,8 @@ Jinja2==2.10 jmespath==0.9.3 jsonpatch==1.21 jsonpointer==2.0 -jsonschema==2.6.0 -keystoneauth1==3.11.0 +jsonschema==3.0.0 +keystoneauth1==3.15.0 keystonemiddleware==4.17.0 kombu==4.0.0 kubernetes==5.0.0 @@ -64,10 +64,10 @@ netaddr==0.7.18 netifaces==0.10.6 oauthlib==2.0.7 openstackdocstheme==1.20.0 -openstacksdk==0.12.0 +openstacksdk==0.44.0 os-api-ref==1.5.0 os-client-config==1.29.0 -os-service-types==1.2.0 +os-service-types==1.7.0 osc-lib==1.10.0 oslo.cache==1.29.0 oslo.concurrency==3.26.0 diff --git a/requirements.txt b/requirements.txt index 33972417e..58d510612 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ anyjson>=0.3.3 # BSD Babel!=2.4.0,>=2.3.4 # BSD eventlet!=0.23.0,!=0.25.0,>=0.22.0 # MIT requests>=2.14.2 # Apache-2.0 -jsonschema>=2.6.0 # MIT +jsonschema>=3.0.0 # MIT keystonemiddleware>=4.17.0 # Apache-2.0 kombu!=4.0.2,>=4.0.0 # BSD netaddr>=0.7.18 # BSD @@ -37,6 +37,7 @@ oslo.upgradecheck>=0.1.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 oslo.versionedobjects>=1.33.3 # Apache-2.0 openstackdocstheme>=1.20.0 # Apache-2.0 +openstacksdk>=0.44.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0 tosca-parser>=1.6.0 # Apache-2.0 diff --git a/tacker/api/schemas/vnf_lcm.py b/tacker/api/schemas/vnf_lcm.py index 6f8f4da10..3d9fafa07 100644 --- a/tacker/api/schemas/vnf_lcm.py +++ b/tacker/api/schemas/vnf_lcm.py @@ -19,7 +19,161 @@ Schema for vnf lcm APIs. """ from tacker.api.validation import parameter_types +from tacker.objects import fields +_extManagedVirtualLinkData = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': parameter_types.identifier, + 'vnfVirtualLinkDescId': parameter_types.identifier_in_vnfd, + 'resourceId': parameter_types.identifier_in_vim + }, + 'required': ['id', 'vnfVirtualLinkDescId', 'resourceId'], + 'additionalProperties': False, + }, +} + +_ipaddresses = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'type': {'enum': fields.IpAddressType.ALL}, + 'subnetId': parameter_types.identifier_in_vim, + 'fixedAddresses': {'type': 'array'} + }, + 'if': {'properties': {'type': {'const': fields.IpAddressType.IPV4}}}, + 'then': { + 'properties': { + 'fixedAddresses': { + 'type': 'array', + 'items': {'type': 'string', 'format': 'ipv4'} + } + } + }, + 'else': { + 'properties': { + 'fixedAddresses': { + 'type': 'array', + 'items': {'type': 'string', 'format': 'ipv6'} + } + } + }, + 'required': ['type', 'fixedAddresses'], + 'additionalProperties': False + } +} + +_ipOverEthernetAddressData = { + 'type': 'object', + 'properties': { + 'macAddress': parameter_types.mac_address_or_none, + 'ipAddresses': _ipaddresses, + }, + 'anyOf': [ + {'required': ['macAddress']}, + {'required': ['ipAddresses']} + ], + 'additionalProperties': False +} + +_cpProtocolData = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'layerProtocol': {'type': 'string', + 'enum': 'IP_OVER_ETHERNET' + }, + 'ipOverEthernet': _ipOverEthernetAddressData, + }, + 'required': ['layerProtocol'], + 'additionalProperties': False, + } +} + +_vnfExtCpConfig = { + 'type': 'array', 'minItems': 1, 'maxItems': 1, + 'items': { + 'type': 'object', + 'properties': { + 'cpInstanceId': parameter_types.identifier_in_vnf, + 'linkPortId': parameter_types.identifier, + 'cpProtocolData': _cpProtocolData, + }, + 'additionalProperties': False, + } +} + +_vnfExtCpData = { + 'type': 'array', 'minItems': 1, + 'items': { + 'type': 'object', + 'properties': { + 'cpdId': parameter_types.identifier_in_vnfd, + 'cpConfig': _vnfExtCpConfig, + }, + 'required': ['cpdId', 'cpConfig'], + 'additionalProperties': False, + }, +} + +_resourceHandle = { + 'type': 'object', + 'properties': { + 'resourceId': parameter_types.identifier_in_vim, + 'vimLevelResourceType': {'type': 'string', 'maxLength': 255}, + }, + 'required': ['resourceId'], + 'additionalProperties': False, +} + +_extLinkPortData = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': parameter_types.identifier, + 'resourceHandle': _resourceHandle, + }, + 'required': ['id', 'resourceHandle'], + 'additionalProperties': False, + } +} + +_extVirtualLinkData = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': parameter_types.identifier, + 'resourceId': parameter_types.identifier_in_vim, + 'extCps': _vnfExtCpData, + 'extLinkPorts': _extLinkPortData, + + }, + 'required': ['id', 'resourceId', 'extCps'], + 'additionalProperties': False, + } +} + +_vimConnectionInfo = { + 'type': 'array', + 'maxItems': 1, + 'items': { + 'type': 'object', + 'properties': { + 'id': parameter_types.identifier, + 'vimId': parameter_types.identifier, + 'vimType': {'type': 'string', 'minLength': 1, 'maxLength': 255}, + 'accessInfo': parameter_types.keyvalue_pairs, + }, + 'required': ['id', 'vimType'], + 'additionalProperties': False, + } +} create = { 'type': 'object', @@ -31,3 +185,17 @@ create = { 'required': ['vnfdId'], 'additionalProperties': False, } + +instantiate = { + 'type': 'object', + 'properties': { + 'flavourId': {'type': 'string', 'maxLength': 255}, + 'instantiationLevelId': {'type': 'string', 'maxLength': 255}, + 'extVirtualLinks': _extVirtualLinkData, + 'extManagedVirtualLinks': _extManagedVirtualLinkData, + 'vimConnectionInfo': _vimConnectionInfo, + 'additionalParams': parameter_types.keyvalue_pairs, + }, + 'required': ['flavourId'], + 'additionalProperties': False, +} diff --git a/tacker/api/validation/parameter_types.py b/tacker/api/validation/parameter_types.py index a3ec47aad..2deba9287 100644 --- a/tacker/api/validation/parameter_types.py +++ b/tacker/api/validation/parameter_types.py @@ -138,3 +138,36 @@ uuid = { name_allow_zero_min_length = { 'type': 'string', 'minLength': 0, 'maxLength': 255 } + +ip_address = { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'} + ] +} + +positive_integer = { + 'type': ['integer', 'string'], + 'pattern': '^[0-9]*$', 'minimum': 1, 'minLength': 1 +} + +identifier = { + 'type': 'string', 'minLength': 1, 'maxLength': 255 +} + +identifier_in_vim = { + 'type': 'string', 'minLength': 1, 'maxLength': 255 +} + +identifier_in_vnfd = { + 'type': 'string', 'minLength': 1, 'maxLength': 255 +} + +identifier_in_vnf = { + 'type': 'string', 'minLength': 1, 'maxLength': 255 +} + +mac_address_or_none = { + 'type': 'string', 'format': 'mac_address_or_none' +} diff --git a/tacker/api/validation/validators.py b/tacker/api/validation/validators.py index 03f1b1e82..0a20c5845 100644 --- a/tacker/api/validation/validators.py +++ b/tacker/api/validation/validators.py @@ -20,9 +20,11 @@ Internal implementation of request Body validating middleware. import jsonschema from jsonschema import exceptions as jsonschema_exc +import netaddr from oslo_utils import uuidutils import rfc3986 import six +import webob from tacker.common import exceptions as exception @@ -38,6 +40,21 @@ def _validate_uuid_format(instance): return uuidutils.is_uuid_like(instance) +@jsonschema.FormatChecker.cls_checks('mac_address_or_none', + webob.exc.HTTPBadRequest) +def validate_mac_address_or_none(instance): + """Validate instance is a MAC address""" + + if instance is None: + return + + if not netaddr.valid_mac(instance): + msg = _("'%s' is not a valid mac address") + raise webob.exc.HTTPBadRequest(explanation=msg % instance) + + return True + + class FormatChecker(jsonschema.FormatChecker): """A FormatChecker can output the message from cause exception @@ -76,14 +93,14 @@ class FormatChecker(jsonschema.FormatChecker): class _SchemaValidator(object): """A validator class - This class is changed from Draft4Validator to validate minimum/maximum + This class is changed from Draft7Validator to validate minimum/maximum value of a string number(e.g. '10'). This changes can be removed when we tighten up the API definition and the XML conversion. Also FormatCheckers are added for checking data formats which would be passed through cinder api commonly. """ - validator_org = jsonschema.Draft4Validator + validator_org = jsonschema.Draft7Validator def __init__(self, schema): validator_cls = jsonschema.validators.extend(self.validator_org, @@ -95,7 +112,9 @@ class _SchemaValidator(object): try: self.validator.validate(*args, **kwargs) except jsonschema.ValidationError as ex: - if len(ex.path) > 0: + if isinstance(ex.cause, webob.exc.HTTPBadRequest): + detail = str(ex.cause) + elif len(ex.path) > 0: detail = _("Invalid input for field/attribute %(path)s." " Value: %(value)s. %(message)s") % { 'path': ex.path.pop(), 'value': ex.instance, diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 471245f45..63d47011a 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import uuidutils + import six from six.moves import http_client import webob @@ -22,19 +24,141 @@ from tacker.api import validation from tacker.api.views import vnf_lcm as vnf_lcm_view from tacker.common import exceptions from tacker.common import utils +from tacker.conductor.conductorrpc import vnf_lcm_rpc +from tacker.extensions import nfvo from tacker import objects from tacker.objects import fields from tacker.policies import vnf_lcm as vnf_lcm_policies +from tacker.vnfm import vim_client from tacker import wsgi +def check_vnf_state(action, instantiation_state=None, task_state=(None,)): + """Decorator to check vnf states are valid for particular action. + + If the vnf is in the wrong state, it will raise conflict exception. + """ + + if instantiation_state is not None and not \ + isinstance(instantiation_state, set): + instantiation_state = set(instantiation_state) + if task_state is not None and not isinstance(task_state, set): + task_state = set(task_state) + + def outer(f): + @six.wraps(f) + def inner(self, context, vnf_instance, *args, **kw): + if instantiation_state is not None and \ + vnf_instance.instantiation_state not in \ + instantiation_state: + raise exceptions.VnfInstanceConflictState( + attr='instantiation_state', + uuid=vnf_instance.id, + state=vnf_instance.instantiation_state, + action=action) + if (task_state is not None and + vnf_instance.task_state not in task_state): + raise exceptions.VnfInstanceConflictState( + attr='task_state', + uuid=vnf_instance.id, + state=vnf_instance.task_state, + action=action) + return f(self, context, vnf_instance, *args, **kw) + return inner + return outer + + class VnfLcmController(wsgi.Controller): _view_builder_class = vnf_lcm_view.ViewBuilder + def __init__(self): + super(VnfLcmController, self).__init__() + self.rpc_api = vnf_lcm_rpc.VNFLcmRPCAPI() + def _get_vnf_instance_href(self, vnf_instance): return '/vnflcm/v1/vnf_instances/%s' % vnf_instance.id + def _get_vnf_instance(self, context, id): + # check if id is of type uuid format + if not uuidutils.is_uuid_like(id): + msg = _("Can not find requested vnf instance: %s") % id + raise webob.exc.HTTPNotFound(explanation=msg) + + try: + vnf_instance = objects.VnfInstance.get_by_id(context, id) + except exceptions.VnfInstanceNotFound: + msg = _("Can not find requested vnf instance: %s") % id + raise webob.exc.HTTPNotFound(explanation=msg) + + return vnf_instance + + def _validate_flavour_and_inst_level(self, context, req_body, + vnf_instance): + inst_levels = {} + flavour_list = [] + vnf_package_vnfd = objects.VnfPackageVnfd.get_by_id( + context, vnf_instance.vnfd_id) + vnf_package = objects.VnfPackage.get_by_id( + context, vnf_package_vnfd.package_uuid) + deployment_flavour = vnf_package.vnf_deployment_flavours + for dep_flavour in deployment_flavour.objects: + flavour_list.append(dep_flavour.flavour_id) + if dep_flavour.instantiation_levels: + inst_levels.update({ + dep_flavour.flavour_id: dep_flavour.instantiation_levels}) + + if req_body['flavour_id'] not in flavour_list: + raise exceptions.FlavourNotFound(flavour_id=req_body['flavour_id']) + + req_inst_level_id = req_body.get('instantiation_level_id') + if req_inst_level_id is None: + return + + if not inst_levels: + raise exceptions.InstantiationLevelNotFound( + inst_level_id=req_inst_level_id) + + for flavour, inst_level in inst_levels.items(): + if flavour != req_body['flavour_id']: + continue + + if req_inst_level_id in inst_level.get('levels').keys(): + # Found instantiation level + return + + raise exceptions.InstantiationLevelNotFound( + inst_level_id=req_body['instantiation_level_id']) + + def _validate_vim_connection(self, context, instantiate_vnf_request): + if instantiate_vnf_request.vim_connection_info: + vim_id = instantiate_vnf_request.vim_connection_info[0].vim_id + access_info = \ + instantiate_vnf_request.vim_connection_info[0].access_info + if access_info: + region_name = access_info.get('region') + else: + region_name = None + else: + vim_id = None + region_name = None + + vim_client_obj = vim_client.VimClient() + + try: + vim_client_obj.get_vim( + context, vim_id, region_name=region_name) + except nfvo.VimDefaultNotDefined as exp: + raise webob.exc.HTTPBadRequest(explanation=exp.message) + except nfvo.VimNotFoundException: + msg = _("VimConnection id is not found: %s")\ + % vim_id + raise webob.exc.HTTPBadRequest(explanation=msg) + except nfvo.VimRegionNotFoundException: + msg = _("Region not found for the VimConnection: %s")\ + % vim_id + raise webob.exc.HTTPBadRequest(explanation=msg) + @wsgi.response(http_client.CREATED) @wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN)) @validation.schema(vnf_lcm.create) @@ -77,8 +201,42 @@ class VnfLcmController(wsgi.Controller): def delete(self, request, id): raise webob.exc.HTTPNotImplemented() + @check_vnf_state(action="instantiate", + instantiation_state=[fields.VnfInstanceState.NOT_INSTANTIATED], + task_state=[None]) + def _instantiate(self, context, vnf_instance, request_body): + req_body = utils.convert_camelcase_to_snakecase(request_body) + + try: + self._validate_flavour_and_inst_level(context, req_body, + vnf_instance) + except exceptions.NotFound as ex: + raise webob.exc.HTTPBadRequest(explanation=six.text_type(ex)) + + instantiate_vnf_request = \ + objects.InstantiateVnfRequest.obj_from_primitive( + req_body, context=context) + + # validate the vim connection id passed through request is exist or not + self._validate_vim_connection(context, instantiate_vnf_request) + + vnf_instance.task_state = fields.VnfInstanceTaskState.INSTANTIATING + vnf_instance.save() + + self.rpc_api.instantiate(context, vnf_instance, + instantiate_vnf_request) + + @wsgi.response(http_client.ACCEPTED) + @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND, + http_client.CONFLICT, http_client.BAD_REQUEST)) + @validation.schema(vnf_lcm.instantiate) def instantiate(self, request, id, body): - raise webob.exc.HTTPNotImplemented() + context = request.environ['tacker.context'] + context.can(vnf_lcm_policies.VNFLCM % 'instantiate') + + vnf_instance = self._get_vnf_instance(context, id) + + self._instantiate(context, vnf_instance, body) def terminate(self, request, id, body): raise webob.exc.HTTPNotImplemented() diff --git a/tacker/common/clients.py b/tacker/common/clients.py index 93154d60d..a1d592a90 100644 --- a/tacker/common/clients.py +++ b/tacker/common/clients.py @@ -10,7 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + from heatclient import client as heatclient +from openstack import connection + from tacker.vnfm import keystone @@ -23,6 +27,13 @@ class OpenstackClients(object): self.mistral_client = None self.keystone_client = None self.region_name = region_name + + if auth_attr: + # Note(tpatil): In vnflcm, auth_attr contains region information + # which should be popped before creating the keystoneclient. + auth_attr = copy.deepcopy(auth_attr) + auth_attr.pop('region', None) + self.auth_attr = auth_attr def _keystone_client(self): @@ -49,3 +60,31 @@ class OpenstackClients(object): if not self.heat_client: self.heat_client = self._heat_client() return self.heat_client + + +class OpenstackSdkConnection(object): + + def __init__(self, vim_connection_info, version=None): + super(OpenstackSdkConnection, self).__init__() + self.keystone_plugin = keystone.Keystone() + self.connection = self.openstack_connection(vim_connection_info, + version) + + def openstack_connection(self, vim_connection_info, version): + access_info = vim_connection_info.access_info + auth = dict(auth_url=access_info['auth_url'], + username=access_info['username'], + password=access_info['password'], + project_name=access_info['project_name'], + user_domain_name=access_info['user_domain_name'], + project_domain_name=access_info['project_domain_name']) + + session = self.keystone_plugin.initialize_client(**auth).session + + conn = connection.Connection( + region_name=access_info.get('region'), + session=session, + identity_interface='internal', + image_api_version=version) + + return conn diff --git a/tacker/common/driver_manager.py b/tacker/common/driver_manager.py index 6feb68d62..9460663e9 100644 --- a/tacker/common/driver_manager.py +++ b/tacker/common/driver_manager.py @@ -43,7 +43,6 @@ class DriverManager(object): LOG.error(msg) raise SystemExit(msg) drivers[type_] = ext - self._drivers = dict((type_, ext.obj) for (type_, ext) in drivers.items()) LOG.info("Registered drivers from %(namespace)s: %(keys)s", diff --git a/tacker/common/exceptions.py b/tacker/common/exceptions.py index 0981d7933..bcd1b5d79 100644 --- a/tacker/common/exceptions.py +++ b/tacker/common/exceptions.py @@ -215,6 +215,23 @@ class VnfInstanceNotFound(NotFound): message = _("No vnf instance with id %(id)s.") +class VnfInstanceConflictState(Conflict): + message = _("Vnf instance %(uuid)s in %(attr)s %(state)s. Cannot " + "%(action)s while the vnf instance is in this state.") + + +class FlavourNotFound(NotFound): + message = _("No flavour with id '%(flavour_id)s'.") + + +class InstantiationLevelNotFound(NotFound): + message = _("No instantiation level with id '%(inst_level_id)s'.") + + +class VimConnectionNotFound(NotFound): + message = _("No vim found with id '%(vim_id)s'.") + + class VnfResourceNotFound(NotFound): message = _("No vnf resource with id %(id)s.") @@ -235,6 +252,20 @@ class VnfInstantiatedInfoNotFound(NotFound): message = _("No vnf instantiated info for vnf id %(vnf_instance_id)s.") +class VnfInstantiationFailed(TackerException): + message = _("Vnf instantiation failed for vnf %(id)s, error: %(error)s") + + +class VnfInstantiationWaitFailed(TackerException): + message = _("Vnf instantiation wait failed for vnf %(id)s, " + "error: %(error)s") + + +class VnfPreInstantiationFailed(TackerException): + message = _("Vnf '%(id)s' failed during pre-instantiation due to error: " + "%(error)s") + + class OrphanedObjectError(TackerException): msg_fmt = _('Cannot call %(method)s on orphaned %(objtype)s object') diff --git a/tacker/common/utils.py b/tacker/common/utils.py index 733f7006c..ccdf34ef0 100644 --- a/tacker/common/utils.py +++ b/tacker/common/utils.py @@ -21,6 +21,7 @@ import functools import inspect import logging as std_logging +import math import os import random import re @@ -36,6 +37,7 @@ from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import importutils +from six.moves import urllib from stevedore import driver try: from eventlet import sleep @@ -392,6 +394,14 @@ def convert_snakecase_to_camelcase(request_data): return request_data +def is_url(url): + try: + urllib.request.urlopen(url) + return True + except Exception: + return False + + class CooperativeReader(object): """An eventlet thread friendly class for reading in image data. @@ -516,3 +526,71 @@ class LimitingReader(object): if self.bytes_read > self.limit: raise self.exception_class() return result + + +class MemoryUnit(object): + + UNIT_SIZE_DEFAULT = 'B' + UNIT_SIZE_DICT = {'B': 1, 'kB': 1000, 'KiB': 1024, 'MB': 1000000, + 'MiB': 1048576, 'GB': 1000000000, + 'GiB': 1073741824, 'TB': 1000000000000, + 'TiB': 1099511627776} + + @staticmethod + def convert_unit_size_to_num(size, unit=None): + """Convert given size to a number representing given unit. + + If unit is None, convert to a number representing UNIT_SIZE_DEFAULT + :param size: unit size e.g. 1 TB + :param unit: unit to be converted to e.g GB + :return: converted number e.g. 1000 for 1 TB size and unit GB + """ + if unit: + unit = MemoryUnit.validate_unit(unit) + else: + unit = MemoryUnit.UNIT_SIZE_DEFAULT + LOG.info(_('A memory unit is not provided for size; using the ' + 'default unit %(default)s.') % {'default': 'B'}) + regex = re.compile('(\d*)\s*(\w*)') + result = regex.match(str(size)).groups() + if result[1]: + unit_size = MemoryUnit.validate_unit(result[1]) + converted = int(str_to_num(result[0]) * + MemoryUnit.UNIT_SIZE_DICT[unit_size] * + math.pow(MemoryUnit.UNIT_SIZE_DICT + [unit], -1)) + LOG.info(_('Given size %(size)s is converted to %(num)s ' + '%(unit)s.') % {'size': size, + 'num': converted, 'unit': unit}) + else: + converted = (str_to_num(result[0])) + return converted + + @staticmethod + def validate_unit(unit): + if unit in MemoryUnit.UNIT_SIZE_DICT.keys(): + return unit + else: + for key in MemoryUnit.UNIT_SIZE_DICT.keys(): + if key.upper() == unit.upper(): + return key + + msg = _('Provided unit "{0}" is not valid. The valid units are ' + '{1}').format(unit, MemoryUnit.UNIT_SIZE_DICT.keys()) + LOG.error(msg) + raise ValueError(msg) + + +def str_to_num(value): + """Convert a string representation of a number into a numeric type.""" + if (isinstance(value, int) or + isinstance(value, float)): + return value + + try: + return int(value) + except ValueError: + try: + return float(value) + except ValueError: + return None diff --git a/tacker/conductor/conductor_server.py b/tacker/conductor/conductor_server.py index ffa722ff1..c6408e53c 100644 --- a/tacker/conductor/conductor_server.py +++ b/tacker/conductor/conductor_server.py @@ -22,6 +22,7 @@ import shutil import sys from glance_store import exceptions as store_exceptions +from oslo_config import cfg from oslo_log import log as logging import oslo_messaging from oslo_service import periodic_task @@ -37,7 +38,6 @@ from tacker.common import exceptions from tacker.common import safe_utils from tacker.common import topics from tacker.common import utils -import tacker.conf from tacker import context as t_context from tacker.db.common_services import common_services_db from tacker.db.nfvo import nfvo_db @@ -50,9 +50,41 @@ from tacker.objects.vnf_package import VnfPackagesList from tacker.plugins.common import constants from tacker import service as tacker_service from tacker import version +from tacker.vnflcm import vnflcm_driver +from tacker.vnfm import plugin +CONF = cfg.CONF + +# NOTE(tpatil): keystone_authtoken opts registered explicitly as conductor +# service doesn't use the keystonemiddleware.authtoken middleware as it's +# used by the tacker.service in the api-paste.ini +OPTS = [cfg.StrOpt('user_domain_id', + default='default', + help='User Domain Id'), + cfg.StrOpt('project_domain_id', + default='default', + help='Project Domain Id'), + cfg.StrOpt('password', + default='default', + help='User Password'), + cfg.StrOpt('username', + default='default', + help='User Name'), + cfg.StrOpt('user_domain_name', + default='default', + help='Use Domain Name'), + cfg.StrOpt('project_name', + default='default', + help='Project Name'), + cfg.StrOpt('project_domain_name', + default='default', + help='Project Domain Name'), + cfg.StrOpt('auth_url', + default='http://localhost/identity/v3', + help='Keystone endpoint')] + +cfg.CONF.register_opts(OPTS, 'keystone_authtoken') -CONF = tacker.conf.CONF LOG = logging.getLogger(__name__) @@ -109,6 +141,8 @@ class Conductor(manager.Manager): else: self.conf = CONF super(Conductor, self).__init__(host=self.conf.host) + self.vnfm_plugin = plugin.VNFMPlugin() + self.vnflcm_driver = vnflcm_driver.VnfLcmDriver() def init_host(self): glance_store.initialize_glance_store() @@ -361,6 +395,35 @@ class Conductor(manager.Manager): {'zip': csar_path, 'folder': csar_zip_temp_path, 'uuid': vnf_pack.id}) + def instantiate(self, context, vnf_instance, instantiate_vnf): + self.vnflcm_driver.instantiate_vnf(context, vnf_instance, + instantiate_vnf) + + vnf_package_vnfd = objects.VnfPackageVnfd.get_by_id(context, + vnf_instance.vnfd_id) + vnf_package = objects.VnfPackage.get_by_id(context, + vnf_package_vnfd.package_uuid, expected_attrs=['vnfd']) + try: + self._update_package_usage_state(context, vnf_package) + except Exception: + LOG.error("Failed to update usage_state of vnf package %s", + vnf_package.id) + + def _update_package_usage_state(self, context, vnf_package): + """Update vnf package usage state to IN_USE/NOT_IN_USE + + If vnf package is not used by any of the vnf instances, it's usage + state should be set to NOT_IN_USE otherwise it should be set to + IN_USE. + """ + result = vnf_package.is_package_in_use(context) + if result: + vnf_package.usage_state = fields.PackageUsageStateType.IN_USE + else: + vnf_package.usage_state = fields.PackageUsageStateType.NOT_IN_USE + + vnf_package.save() + def init(args, **kwargs): CONF(args=args, project='tacker', diff --git a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py new file mode 100644 index 000000000..7a109d88c --- /dev/null +++ b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py @@ -0,0 +1,40 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import oslo_messaging + +from tacker.common import rpc +from tacker.common import topics +from tacker.objects import base as objects_base + + +class VNFLcmRPCAPI(object): + + target = oslo_messaging.Target( + exchange='tacker', + topic=topics.TOPIC_CONDUCTOR, + fanout=False, + version='1.0') + + def instantiate(self, context, vnf_instance, instantiate_vnf, cast=True): + serializer = objects_base.TackerObjectSerializer() + + client = rpc.get_client(self.target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast if cast else cctxt.call + return rpc_method(context, 'instantiate', + vnf_instance=vnf_instance, + instantiate_vnf=instantiate_vnf) diff --git a/tacker/conf/__init__.py b/tacker/conf/__init__.py index e1d20cb69..0180fba79 100644 --- a/tacker/conf/__init__.py +++ b/tacker/conf/__init__.py @@ -19,8 +19,8 @@ from oslo_config import cfg from tacker.conf import conductor from tacker.conf import vnf_package - CONF = cfg.CONF +CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token') vnf_package.register_opts(CONF) conductor.register_opts(CONF) diff --git a/tacker/extensions/vnflcm.py b/tacker/extensions/vnflcm.py new file mode 100644 index 000000000..f02d06d5f --- /dev/null +++ b/tacker/extensions/vnflcm.py @@ -0,0 +1,31 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# 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. + + +from oslo_log import log as logging + +from tacker._i18n import _ +from tacker.common import exceptions + + +LOG = logging.getLogger(__name__) + + +class GlanceClientException(exceptions.TackerException): + message = _("%(msg)s") + + +class ImageCreateWaitFailed(exceptions.TackerException): + message = _('Create image failed %(reason)s') diff --git a/tacker/extensions/vnfm.py b/tacker/extensions/vnfm.py index 73180a0d4..2be75b4a3 100644 --- a/tacker/extensions/vnfm.py +++ b/tacker/extensions/vnfm.py @@ -199,6 +199,11 @@ class InvalidKubernetesInputParameter(exceptions.InvalidInput): message = _("Found unsupported keys for %(found_keys)s ") +class InvalidInstReqInfoForScaling(exceptions.InvalidInput): + message = _("Scaling resource cannot be set to " + "fixed ip_address or mac_address.") + + def _validate_service_type_list(data, valid_values=None): if not isinstance(data, list): msg = _("Invalid data format for service list: '%s'") % data diff --git a/tacker/objects/__init__.py b/tacker/objects/__init__.py index d8d5587da..d94bf6cd3 100644 --- a/tacker/objects/__init__.py +++ b/tacker/objects/__init__.py @@ -32,4 +32,5 @@ def register_all(): __import__('tacker.objects.vnf_instance') __import__('tacker.objects.vnf_instantiated_info') __import__('tacker.objects.vim_connection') + __import__('tacker.objects.instantiate_vnf_req') __import__('tacker.objects.vnf_resources') diff --git a/tacker/objects/instantiate_vnf_req.py b/tacker/objects/instantiate_vnf_req.py new file mode 100644 index 000000000..76f5d08d7 --- /dev/null +++ b/tacker/objects/instantiate_vnf_req.py @@ -0,0 +1,368 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging + +from tacker import objects +from tacker.objects import base +from tacker.objects import fields + +LOG = logging.getLogger(__name__) + + +@base.TackerObjectRegistry.register +class InstantiateVnfRequest(base.TackerObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'flavour_id': fields.StringField(nullable=False), + 'instantiation_level_id': fields.StringField(nullable=True, + default=None), + 'ext_managed_virtual_links': fields.ListOfObjectsField( + 'ExtManagedVirtualLinkData', nullable=True, default=[]), + 'vim_connection_info': fields.ListOfObjectsField( + 'VimConnectionInfo', nullable=True, default=[]), + 'ext_virtual_links': fields.ListOfObjectsField( + 'ExtVirtualLinkData', nullable=True, default=[]), + 'additional_params': fields.DictOfStringsField(nullable=True, + default={}), + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + obj_instantiate_vnf_req = super( + InstantiateVnfRequest, cls).obj_from_primitive( + primitive, context) + else: + if 'ext_managed_virtual_links' in primitive.keys(): + obj_data = [ExtManagedVirtualLinkData._from_dict( + ext_manage) for ext_manage in primitive.get( + 'ext_managed_virtual_links', [])] + primitive.update({'ext_managed_virtual_links': obj_data}) + + if 'vim_connection_info' in primitive.keys(): + obj_data = [objects.VimConnectionInfo._from_dict( + vim_conn) for vim_conn in primitive.get( + 'vim_connection_info', [])] + primitive.update({'vim_connection_info': obj_data}) + + if 'ext_virtual_links' in primitive.keys(): + obj_data = [ExtVirtualLinkData.obj_from_primitive( + ext_vir_link, context) for ext_vir_link in primitive.get( + 'ext_virtual_links', [])] + primitive.update({'ext_virtual_links': obj_data}) + obj_instantiate_vnf_req = InstantiateVnfRequest._from_dict( + primitive) + + return obj_instantiate_vnf_req + + @classmethod + def _from_dict(cls, data_dict): + flavour_id = data_dict.get('flavour_id') + instantiation_level_id = data_dict.get('instantiation_level_id') + ext_managed_virtual_links = data_dict.get('ext_managed_virtual_links', + []) + vim_connection_info = data_dict.get('vim_connection_info', []) + ext_virtual_links = data_dict.get('ext_virtual_links', []) + additional_params = data_dict.get('additional_params', {}) + + return cls(flavour_id=flavour_id, + instantiation_level_id=instantiation_level_id, + ext_managed_virtual_links=ext_managed_virtual_links, + vim_connection_info=vim_connection_info, + ext_virtual_links=ext_virtual_links, + additional_params=additional_params) + + +@base.TackerObjectRegistry.register +class ExtManagedVirtualLinkData(base.TackerObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'vnf_virtual_link_desc_id': fields.StringField(nullable=False), + 'resource_id': fields.StringField(nullable=False), + } + + @classmethod + def _from_dict(cls, data_dict): + id = data_dict.get('id') + vnf_virtual_link_desc_id = data_dict.get( + 'vnf_virtual_link_desc_id') + resource_id = data_dict.get('resource_id') + obj = cls(id=id, vnf_virtual_link_desc_id=vnf_virtual_link_desc_id, + resource_id=resource_id) + return obj + + +@base.TackerObjectRegistry.register +class ExtVirtualLinkData(base.TackerObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'resource_id': fields.StringField(nullable=False), + 'ext_cps': fields.ListOfObjectsField( + 'VnfExtCpData', nullable=True, default=[]), + 'ext_link_ports': fields.ListOfObjectsField( + 'ExtLinkPortData', nullable=True, default=[]), + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + obj_ext_virt_link = super( + ExtVirtualLinkData, cls).obj_from_primitive( + primitive, context) + else: + if 'ext_cps' in primitive.keys(): + obj_data = [VnfExtCpData.obj_from_primitive( + ext_cp, context) for ext_cp in primitive.get( + 'ext_cps', [])] + primitive.update({'ext_cps': obj_data}) + + if 'ext_link_ports' in primitive.keys(): + obj_data = [ExtLinkPortData.obj_from_primitive( + ext_link_port_data, context) + for ext_link_port_data in primitive.get( + 'ext_link_ports', [])] + primitive.update({'ext_link_ports': obj_data}) + + obj_ext_virt_link = ExtVirtualLinkData._from_dict(primitive) + + return obj_ext_virt_link + + @classmethod + def _from_dict(cls, data_dict): + id = data_dict.get('id') + resource_id = data_dict.get('resource_id') + ext_cps = data_dict.get('ext_cps', []) + ext_link_ports = data_dict.get('ext_link_ports', []) + + obj = cls(id=id, resource_id=resource_id, ext_cps=ext_cps, + ext_link_ports=ext_link_ports) + return obj + + +@base.TackerObjectRegistry.register +class VnfExtCpData(base.TackerObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'cpd_id': fields.StringField(nullable=False), + 'cp_config': fields.ListOfObjectsField( + 'VnfExtCpConfig', nullable=True, default=[]), + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + obj_vnf_ext_cp_data = super(VnfExtCpData, cls).obj_from_primitive( + primitive, context) + else: + if 'cp_config' in primitive.keys(): + obj_data = [VnfExtCpConfig.obj_from_primitive( + vnf_ext_cp_conf, context) + for vnf_ext_cp_conf in primitive.get('cp_config', [])] + primitive.update({'cp_config': obj_data}) + + obj_vnf_ext_cp_data = VnfExtCpData._from_dict(primitive) + + return obj_vnf_ext_cp_data + + @classmethod + def _from_dict(cls, data_dict): + cpd_id = data_dict.get('cpd_id') + cp_config = data_dict.get('cp_config', []) + + obj = cls(cpd_id=cpd_id, cp_config=cp_config) + return obj + + +@base.TackerObjectRegistry.register +class VnfExtCpConfig(base.TackerObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'cp_instance_id': fields.StringField(nullable=True, default=None), + 'link_port_id': fields.StringField(nullable=True, default=None), + 'cp_protocol_data': fields.ListOfObjectsField( + 'CpProtocolData', nullable=True, default=[]), + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + obj_ext_cp_config = super(VnfExtCpConfig, cls).obj_from_primitive( + primitive, context) + else: + if 'cp_protocol_data' in primitive.keys(): + obj_data = [CpProtocolData.obj_from_primitive( + cp_protocol, context) for cp_protocol in primitive.get( + 'cp_protocol_data', [])] + primitive.update({'cp_protocol_data': obj_data}) + + obj_ext_cp_config = VnfExtCpConfig._from_dict(primitive) + + return obj_ext_cp_config + + @classmethod + def _from_dict(cls, data_dict): + cp_instance_id = data_dict.get('cp_instance_id') + link_port_id = data_dict.get('link_port_id') + cp_protocol_data = data_dict.get('cp_protocol_data', []) + + obj = cls(cp_instance_id=cp_instance_id, + link_port_id=link_port_id, cp_protocol_data=cp_protocol_data) + return obj + + +@base.TackerObjectRegistry.register +class CpProtocolData(base.TackerObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'layer_protocol': fields.StringField(nullable=False), + 'ip_over_ethernet': fields.ObjectField( + 'IpOverEthernetAddressData', nullable=True, default=None), + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + obj_cp_protocal = super(CpProtocolData, cls).obj_from_primitive( + primitive, context) + else: + if 'ip_over_ethernet' in primitive.keys(): + obj_data = IpOverEthernetAddressData.obj_from_primitive( + primitive.get('ip_over_ethernet', {}), context) + primitive.update({'ip_over_ethernet': obj_data}) + obj_cp_protocal = CpProtocolData._from_dict(primitive) + + return obj_cp_protocal + + @classmethod + def _from_dict(cls, data_dict): + layer_protocol = data_dict.get('layer_protocol') + ip_over_ethernet = data_dict.get('ip_over_ethernet') + + obj = cls(layer_protocol=layer_protocol, + ip_over_ethernet=ip_over_ethernet) + return obj + + +@base.TackerObjectRegistry.register +class IpOverEthernetAddressData(base.TackerObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'mac_address': fields.StringField(nullable=True, default=None), + 'ip_addresses': fields.ListOfObjectsField('IpAddress', nullable=True, + default=[]), + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + ip_over_ethernet = super( + IpOverEthernetAddressData, cls).obj_from_primitive( + primitive, context) + else: + if 'ip_addresses' in primitive.keys(): + obj_data = [IpAddress._from_dict( + ip_address) for ip_address in primitive.get( + 'ip_addresses', [])] + primitive.update({'ip_addresses': obj_data}) + + ip_over_ethernet = IpOverEthernetAddressData._from_dict(primitive) + + return ip_over_ethernet + + @classmethod + def _from_dict(cls, data_dict): + mac_address = data_dict.get('mac_address') + ip_addresses = data_dict.get('ip_addresses', []) + obj = cls(mac_address=mac_address, ip_addresses=ip_addresses) + return obj + + +@base.TackerObjectRegistry.register +class IpAddress(base.TackerObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'type': fields.IpAddressTypeField(nullable=False), + 'subnet_id': fields.StringField(nullable=True, default=None), + 'fixed_addresses': fields.ListOfStringsField(nullable=True, + default=[]) + } + + @classmethod + def _from_dict(cls, data_dict): + type = data_dict.get('type') + subnet_id = data_dict.get('subnet_id') + fixed_addresses = data_dict.get('fixed_addresses', []) + + obj = cls(type=type, subnet_id=subnet_id, + fixed_addresses=fixed_addresses) + + return obj + + +@base.TackerObjectRegistry.register +class ExtLinkPortData(base.TackerObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.UUIDField(nullable=False), + 'resource_handle': fields.ObjectField( + 'ResourceHandle', nullable=False), + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + obj_link_port_data = super( + ExtLinkPortData, cls).obj_from_primitive(primitive, context) + else: + if 'resource_handle' in primitive.keys(): + obj_data = objects.ResourceHandle._from_dict(primitive.get( + 'resource_handle', [])) + primitive.update({'resource_handle': obj_data}) + + obj_link_port_data = ExtLinkPortData._from_dict(primitive) + + return obj_link_port_data + + @classmethod + def _from_dict(cls, data_dict): + id = data_dict.get('id') + resource_handle = data_dict.get('resource_handle') + + obj = cls(id=id, resource_handle=resource_handle) + return obj diff --git a/tacker/objects/vnf_package.py b/tacker/objects/vnf_package.py index cb19ad839..a40593cd3 100644 --- a/tacker/objects/vnf_package.py +++ b/tacker/objects/vnf_package.py @@ -19,6 +19,7 @@ from oslo_utils import timeutils from oslo_utils import uuidutils from oslo_versionedobjects import base as ovoo_base from sqlalchemy.orm import joinedload +from sqlalchemy.sql import func from tacker._i18n import _ from tacker.common import exceptions @@ -238,7 +239,8 @@ def _make_vnf_packages_list(context, vnf_package_list, db_vnf_package_list, @base.TackerObjectRegistry.register -class VnfPackage(base.TackerObject, base.TackerPersistentObject): +class VnfPackage(base.TackerObject, base.TackerPersistentObject, + base.TackerObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' @@ -428,6 +430,23 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject): self.id, updates) self._from_db_object(self._context, self, db_vnf_package) + @base.remotable + def is_package_in_use(self, context): + if self.onboarding_state == \ + fields.PackageOnboardingStateType.ONBOARDED: + # check if vnf package is used by any vnf instances. + query = context.session.query( + func.count(models.VnfInstance.id)).\ + filter_by( + instantiation_state=fields.VnfInstanceState.INSTANTIATED).\ + filter_by(tenant_id=self.tenant_id).\ + filter_by(vnfd_id=self.vnfd.vnfd_id).\ + filter_by(deleted=False) + result = query.scalar() + return True if result > 0 else False + else: + return False + @base.TackerObjectRegistry.register class VnfPackagesList(ovoo_base.ObjectListBase, base.TackerObject): diff --git a/tacker/policies/vnf_lcm.py b/tacker/policies/vnf_lcm.py index cbfc190f6..566052797 100644 --- a/tacker/policies/vnf_lcm.py +++ b/tacker/policies/vnf_lcm.py @@ -33,6 +33,17 @@ rules = [ } ] ), + policy.DocumentedRuleDefault( + name=VNFLCM % 'instantiate', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Instantiate vnf instance.", + operations=[ + { + 'method': 'POST', + 'path': '/vnflcm/v1/vnf_instances/{vnfInstanceId}/instantiate' + } + ] + ) ] diff --git a/tacker/tests/etc/samples/sample_vnf_package_csar.zip b/tacker/tests/etc/samples/sample_vnf_package_csar.zip index c6233e518..70e48ff0b 100644 Binary files a/tacker/tests/etc/samples/sample_vnf_package_csar.zip and b/tacker/tests/etc/samples/sample_vnf_package_csar.zip differ diff --git a/tacker/tests/etc/samples/sample_vnf_package_csar_with_policy.zip b/tacker/tests/etc/samples/sample_vnf_package_csar_with_policy.zip new file mode 100644 index 000000000..664b2e951 Binary files /dev/null and b/tacker/tests/etc/samples/sample_vnf_package_csar_with_policy.zip differ diff --git a/tacker/tests/etc/samples/sample_vnf_package_csar_with_short_notation.zip b/tacker/tests/etc/samples/sample_vnf_package_csar_with_short_notation.zip new file mode 100644 index 000000000..52d0bc4d4 Binary files /dev/null and b/tacker/tests/etc/samples/sample_vnf_package_csar_with_short_notation.zip differ diff --git a/tacker/tests/etc/samples/sample_vnf_package_csar_without_policy.zip b/tacker/tests/etc/samples/sample_vnf_package_csar_without_policy.zip new file mode 100644 index 000000000..bde4aca3a Binary files /dev/null and b/tacker/tests/etc/samples/sample_vnf_package_csar_without_policy.zip differ diff --git a/tacker/tests/unit/base.py b/tacker/tests/unit/base.py index 34839750c..ac1a763ad 100644 --- a/tacker/tests/unit/base.py +++ b/tacker/tests/unit/base.py @@ -56,11 +56,17 @@ class TestCase(base.BaseTestCase): class FixturedTestCase(TestCase): client_fixture_class = None + sdk_connection_fixure_class = None def setUp(self): super(FixturedTestCase, self).setUp() - if self.client_fixture_class: + if self.client_fixture_class or self.sdk_connection_fixure_class: self.requests_mock = self.useFixture(requests_mock_fixture. Fixture()) - fix = self.client_fixture_class(self.requests_mock) - self.cs = self.useFixture(fix).client + if self.client_fixture_class: + hc_fix = self.client_fixture_class(self.requests_mock) + self.cs = self.useFixture(hc_fix).client + + if self.sdk_connection_fixure_class: + sdk_conn_fix = self.sdk_connection_fixure_class(self.requests_mock) + self.sdk_conn = self.useFixture(sdk_conn_fix).client diff --git a/tacker/tests/unit/conductor/test_conductor_server.py b/tacker/tests/unit/conductor/test_conductor_server.py index e974fbda2..776f8219a 100644 --- a/tacker/tests/unit/conductor/test_conductor_server.py +++ b/tacker/tests/unit/conductor/test_conductor_server.py @@ -32,19 +32,46 @@ from tacker import objects from tacker.objects import vnf_package from tacker.tests.unit.conductor import fakes from tacker.tests.unit.db.base import SqlTestCase +from tacker.tests.unit.objects import fakes as fake_obj +from tacker.tests.unit.vnflcm import fakes as vnflcm_fakes from tacker.tests import uuidsentinel CONF = tacker.conf.CONF +class FakeVnfLcmDriver(mock.Mock): + pass + + +class FakeVNFMPlugin(mock.Mock): + pass + + class TestConductor(SqlTestCase): def setUp(self): super(TestConductor, self).setUp() + self.addCleanup(mock.patch.stopall) self.context = context.get_admin_context() + self._mock_vnflcm_driver() + self._mock_vnfm_plugin() self.conductor = conductor_server.Conductor('host') self.vnf_package = self._create_vnf_package() + def _mock_vnfm_plugin(self): + self.vnfm_plugin = mock.Mock(wraps=FakeVNFMPlugin()) + fake_vnfm_plugin = mock.Mock() + fake_vnfm_plugin.return_value = self.vnfm_plugin + self._mock( + 'tacker.vnfm.plugin.VNFMPlugin', fake_vnfm_plugin) + + def _mock_vnflcm_driver(self): + self.vnflcm_driver = mock.Mock(wraps=FakeVnfLcmDriver()) + fake_vnflcm_driver = mock.Mock() + fake_vnflcm_driver.return_value = self.vnflcm_driver + self._mock( + 'tacker.vnflcm.vnflcm_driver.VnfLcmDriver', fake_vnflcm_driver) + def _create_vnf_package(self): vnfpkgm = vnf_package.VnfPackage(context=self.context, **fakes.VNF_PACKAGE_DATA) @@ -153,6 +180,77 @@ class TestConductor(SqlTestCase): self.vnf_package) shutil.rmtree(fake_csar) + def _create_and_upload_vnf_package(self): + vnf_package = objects.VnfPackage(context=self.context, + **fake_obj.vnf_package_data) + vnf_package.create() + + vnf_pack_vnfd = fake_obj.get_vnf_package_vnfd_data( + vnf_package.id, uuidsentinel.vnfd_id) + + vnf_pack_vnfd_obj = objects.VnfPackageVnfd( + context=self.context, **vnf_pack_vnfd) + vnf_pack_vnfd_obj.create() + + vnf_package.onboarding_state = "ONBOARDED" + vnf_package.save() + + return vnf_pack_vnfd_obj + + @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + def test_instantiate_vnf_instance(self, mock_package_in_use): + vnf_package_vnfd = self._create_and_upload_vnf_package() + vnf_instance_data = fake_obj.get_vnf_instance_data( + vnf_package_vnfd.vnfd_id) + mock_package_in_use.return_value = False + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() + self.conductor.instantiate(self.context, vnf_instance, + instantiate_vnf_req) + self.vnflcm_driver.instantiate_vnf.assert_called_once_with( + self.context, vnf_instance, instantiate_vnf_req) + mock_package_in_use.assert_called_once() + + @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + def test_instantiate_vnf_instance_with_vnf_package_in_use(self, + mock_vnf_package_in_use): + vnf_package_vnfd = self._create_and_upload_vnf_package() + vnf_instance_data = fake_obj.get_vnf_instance_data( + vnf_package_vnfd.vnfd_id) + mock_vnf_package_in_use.return_value = True + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() + self.conductor.instantiate(self.context, vnf_instance, + instantiate_vnf_req) + self.vnflcm_driver.instantiate_vnf.assert_called_once_with( + self.context, vnf_instance, instantiate_vnf_req) + mock_vnf_package_in_use.assert_called_once() + + @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + @mock.patch('tacker.conductor.conductor_server.LOG') + def test_instantiate_vnf_instance_failed_with_exception( + self, mock_log, mock_is_package_in_use): + vnf_package_vnfd = self._create_and_upload_vnf_package() + vnf_instance_data = fake_obj.get_vnf_instance_data( + vnf_package_vnfd.vnfd_id) + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() + mock_is_package_in_use.side_effect = Exception + self.conductor.instantiate(self.context, vnf_instance, + instantiate_vnf_req) + self.vnflcm_driver.instantiate_vnf.assert_called_once_with( + self.context, vnf_instance, instantiate_vnf_req) + mock_is_package_in_use.assert_called_once() + expected_log = 'Failed to update usage_state of vnf package %s' + mock_log.error.assert_called_once_with(expected_log, + vnf_package_vnfd.package_uuid) + @mock.patch.object(os, 'remove') @mock.patch.object(shutil, 'rmtree') @mock.patch.object(os.path, 'exists') diff --git a/tacker/tests/unit/db/utils.py b/tacker/tests/unit/db/utils.py index bbb6d9ac6..ce370fee4 100644 --- a/tacker/tests/unit/db/utils.py +++ b/tacker/tests/unit/db/utils.py @@ -446,3 +446,28 @@ def get_dummy_ns_obj_2(): 'attributes': { 'param_values': {'nsd': {'vl1_name': 'net_mgmt', 'vl2_name': 'net0'}}}}} + + +def get_dummy_vnf_instance(): + connection_info = get_dummy_vim_connection_info() + return {'created_at': '', 'deleted': False, 'deleted_at': None, + 'id': 'fake_id', 'instantiated_vnf_info': None, + 'instantiation_state': 'NOT_INSTANTIATED', + 'tenant_id': 'fake_tenant_id', 'updated_at': '', + 'vim_connection_info': [connection_info], + 'vnf_instance_description': 'VNF Description', + 'vnf_instance_name': 'test', 'vnf_product_name': 'Sample VNF', + 'vnf_provider': 'Company', 'vnf_software_version': '1.0', + 'vnfd_id': 'fake_vnfd_id', 'vnfd_version': '1.0'} + + +def get_dummy_vim_connection_info(): + return {'access_info': { + 'auth_url': 'fake/url', + 'cert_verify': 'False', 'password': 'admin', + 'project_domain_name': 'Default', + 'project_id': None, 'project_name': 'admin', + 'user_domain_name': 'Default', 'username': 'admin'}, + 'created_at': '', 'deleted': False, 'deleted_at': '', + 'id': 'fake_id', 'updated_at': '', + 'vim_id': 'fake_vim_id', 'vim_type': 'openstack'} diff --git a/tacker/tests/unit/objects/fakes.py b/tacker/tests/unit/objects/fakes.py index adcbf195b..783bd9bd1 100644 --- a/tacker/tests/unit/objects/fakes.py +++ b/tacker/tests/unit/objects/fakes.py @@ -220,8 +220,19 @@ ext_managed_virtual_link_info = { 'vnf_link_ports': [vnf_link_ports], } +vnfc_resource_info = { + 'id': uuidsentinel.resource_info_id, + 'vdu_id': 'vdu1', + 'compute_resource': None, + 'storage_resource_ids': [uuidsentinel.id1, uuidsentinel.id2], + 'reservation_id': uuidsentinel.reservation_id, + 'vnfc_cp_info': None, + 'metadata': {'key': 'value'} + +} + vnfc_cp_info = { - 'id': uuidsentinel.cp_info, + 'id': uuidsentinel.cp_instance_id, 'cpd_id': uuidsentinel.cpd_id, 'vnf_ext_cp_id': uuidsentinel.vnf_ext_cp_id, 'cp_protocol_info': [cp_protocol_info], diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index ecae2e2a2..098e27698 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -22,7 +22,12 @@ import webob from tacker.api.vnflcm.v1.router import VnflcmAPIRouter from tacker import context from tacker.db.db_sqlalchemy import models +from tacker import objects from tacker.objects import fields +from tacker.objects.instantiate_vnf_req import ExtManagedVirtualLinkData +from tacker.objects.instantiate_vnf_req import ExtVirtualLinkData +from tacker.objects.instantiate_vnf_req import InstantiateVnfRequest +from tacker.objects.vim_connection import VimConnectionInfo from tacker.tests import constants from tacker.tests import uuidsentinel from tacker import wsgi @@ -94,7 +99,47 @@ def return_vnf_instance_model( return model_obj -def fake_vnf_instance_response(**updates): +def return_vnf_instance( + instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, + **updates): + + if instantiated_state == fields.VnfInstanceState.NOT_INSTANTIATED: + data = _model_non_instantiated_vnf_instance(**updates) + data['instantiation_state'] = instantiated_state + vnf_instance_obj = objects.VnfInstance(**data) + else: + data = _model_non_instantiated_vnf_instance(**updates) + data['instantiation_state'] = instantiated_state + vnf_instance_obj = objects.VnfInstance(**data) + inst_vnf_info = objects.InstantiatedVnfInfo.obj_from_primitive({ + "ext_cp_info": [], + 'ext_virtual_link_info': [], + 'ext_managed_virtual_link_info': [], + 'vnfc_resource_info': [], + 'vnf_virtual_link_resource_info': [], + 'virtual_storage_resource_info': [], + "flavour_id": "simple", + "additional_params": {"key": "value"}, + 'vnf_state': "STARTED"}, None) + + vnf_instance_obj.instantiated_vnf_info = inst_vnf_info + + return vnf_instance_obj + + +def _instantiated_vnf_links(vnf_instance_id): + links = { + "self": {"href": "/vnflcm/v1/vnf_instances/%s" % vnf_instance_id}, + "terminate": {"href": "/vnflcm/v1/vnf_instances/%s/terminate" % + vnf_instance_id}, + "heal": {"href": "/vnflcm/v1/vnf_instances/%s/heal" % + vnf_instance_id}} + + return links + + +def _fake_vnf_instance_not_instantiated_response( + **updates): vnf_instance = { 'vnfInstanceDescription': 'Vnf instance description', 'vnfInstanceName': 'Vnf instance name', @@ -121,6 +166,530 @@ def fake_vnf_instance_response(**updates): return vnf_instance +def fake_vnf_instance_response( + instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, + **updates): + if instantiated_state == fields.VnfInstanceState.NOT_INSTANTIATED: + data = _fake_vnf_instance_not_instantiated_response(**updates) + else: + data = _fake_vnf_instance_not_instantiated_response(**updates) + data['_links'] = _instantiated_vnf_links(uuidsentinel.vnf_instance_id) + data['instantiationState'] = instantiated_state + data['vimConnectionInfo'] = [] + + def _instantiated_vnf_info(): + inst_vnf_info = {} + inst_vnf_info['extCpInfo'] = [] + inst_vnf_info['flavourId'] = 'simple' + inst_vnf_info['vnfState'] = 'STARTED' + inst_vnf_info['additionalParams'] = {"key": "value"} + return inst_vnf_info + + data['instantiatedVnfInfo'] = _instantiated_vnf_info() + + return data + + +def fake_vnf_package(**updates): + vnf_package = { + 'algorithm': None, + 'deleted': False, + 'deleted_at': None, + 'updated_at': None, + 'created_at': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'hash': None, + 'location_glance_store': None, + 'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED', + 'tenant_id': uuidsentinel.tenant_id, + 'usage_state': 'NOT_IN_USE', + 'user_data': {'abc': 'xyz'}, + 'id': constants.UUID, + } + + if updates: + vnf_package.update(updates) + + return vnf_package + + +def fake_vnf_package_deployment_flavour(**updates): + vnf_package_deployment_data = { + 'flavour_description': 'flavour_description', + 'instantiation_levels': ('{"levels": {' + '"instantiation_level_1":' + '{"description": "Smallest size",' + ' "scale_info": {"worker_instance":' + '{"scale_level": 0}}}},' + ' "default_level": ' + '"instantiation_level_1"}'), + 'package_uuid': constants.UUID, + 'flavour_id': 'simple', + } + + if updates: + vnf_package_deployment_data.update(updates) + + return vnf_package_deployment_data + + +def fake_vnf_package_software_image(**updates): + vnf_package_software_image_data = { + 'id': constants.UUID, + 'name': 'name', + 'provider': 'provider', + 'version': 'version', + 'algorithm': 'algorithm', + 'hash': 'hash', + 'container_format': 'container_format', + 'disk_format': 'disk_format', + 'min_disk': 2, + 'min_ram': 10, + 'size': 5, + 'image_path': 'image/path', + 'flavour_uuid': constants.UUID, + 'software_image_id': constants.UUID, + 'created_at': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC) + } + + if updates: + vnf_package_software_image_data.update(updates) + + return vnf_package_software_image_data + + +def return_vnf_deployment_flavour(): + model_obj = models.VnfDeploymentFlavour() + model_obj.update(fake_vnf_package_deployment_flavour()) + return model_obj + + +def return_vnf_software_image(): + model_obj = models.VnfSoftwareImage() + model_obj.update(fake_vnf_package_software_image()) + return model_obj + + +def return_vnf_package(): + model_obj = models.VnfPackage() + model_obj.update(fake_vnf_package()) + return model_obj + + +def return_vnf_package_with_deployment_flavour(): + vnf_package = objects.VnfPackage._from_db_object( + context, objects.VnfPackage(), return_vnf_package(), + expected_attrs=None) + vnf_package_deployment_flavour = \ + objects.VnfDeploymentFlavour._from_db_object( + context, objects.VnfDeploymentFlavour(), + return_vnf_deployment_flavour(), expected_attrs=None) + vnf_software_image = objects.VnfSoftwareImage._from_db_object( + context, objects.VnfSoftwareImage(), return_vnf_software_image(), + expected_attrs=None) + vnf_software_image_list = objects.VnfSoftwareImagesList() + vnf_software_image_list.objects = [vnf_software_image] + vnf_package_deployment_flavour.software_images = vnf_software_image_list + vnf_package_deployment_flavour_list = objects.VnfDeploymentFlavoursList() + vnf_package_deployment_flavour_list.objects = \ + [vnf_package_deployment_flavour] + vnf_package.vnf_deployment_flavours = vnf_package_deployment_flavour_list + return vnf_package + + +def get_vnf_instantiation_request_body(): + instantiation_req_body = { + "flavourId": "simple", + "instantiationLevelId": "instantiation_level_1", + "additionalParams": {"key1": 'value1', "key2": 'value2'}, + "extVirtualLinks": [{ + "id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "resourceId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "extCps": [{ + "cpdId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "cpConfig": [{ + "cpInstanceId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "linkPortId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "cpProtocolData": [ + { + "layerProtocol": 'IP_OVER_ETHERNET', + "ipOverEthernet": { + "macAddress": + 'fa:16:3e:11:11:11', + "ipAddresses": [ + { + "type": "IPV4", + "fixedAddresses": [ + '192.168.11.01', + '192.168.21.202' + ], + "subnetId": + 'actual-subnet-id' + } + ] + } + } + ] + } + ] + }], + "extLinkPorts": [ + { + "id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "resourceHandle": { + "resourceId": + 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vimLevelResourceType": + 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + } + }, + { + "id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "resourceHandle": { + "resourceId": + 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vimLevelResourceType": + 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + } + }], + }], + "extManagedVirtualLinks": [ + {"id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vnfVirtualLinkDescId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "resourceId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa'}, + {"id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vnfVirtualLinkDescId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "resourceId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa'}], + "vimConnectionInfo": [ + {"id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vimId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vimType": 'openstack', + "accessInfo": {"key1": 'value1', "key2": 'value2'}}], + } + + return instantiation_req_body + + +def get_instantiate_vnf_request_obj(): + instantiate_vnf_req = InstantiateVnfRequest() + ext_managed_virtual_link_data = ExtManagedVirtualLinkData() + vim_connection_info = VimConnectionInfo() + ext_virtual_link_data = ExtVirtualLinkData() + instantiate_vnf_req.additional_params = None + instantiate_vnf_req.deleted = 0 + instantiate_vnf_req.ext_managed_virtual_links = \ + [ext_managed_virtual_link_data] + instantiate_vnf_req.ext_virtual_link_data = [ext_virtual_link_data] + instantiate_vnf_req.flavour_id = 'test' + instantiate_vnf_req.instantiation_level_id = 'instantiation_level_1' + instantiate_vnf_req.vim_connection_info = [vim_connection_info] + + return instantiate_vnf_req + + +def create_types_yaml_file(): + yaml_str = ("""imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: + company.provider.VNF: + derived_from: tosca.nodes.nfv.VNF + properties: + descriptor_id: + type: string + constraints: [ valid_values: [ b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 ]] + default: 1111 + descriptor_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: 'fake desc version' + provider: + type: string + constraints: [ valid_values: [ 'Company' ] ] + default: 'Company' + product_name: + type: string + constraints: [ valid_values: [ 'Sample VNF' ] ] + default: 'fake product name' + software_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: 'fake software version' + vnfm_info: + type: list + entry_schema: + type: string + constraints: [ valid_values: [ Tacker ] ] + default: [ Tacker ] + flavour_id: + type: string + constraints: [ valid_values: [ simple ] ] + default: fake id + flavour_description: + type: string + default: "fake flavour" + requirements: + - virtual_link_external: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm""") + file_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), 'types.yaml')) + yaml_file = open(file_path, "w+") + yaml_file.write(yaml_str) + yaml_file.close() + + +def delete_types_yaml_file(): + file_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), 'types.yaml')) + if os.path.exists(file_path): + os.remove(file_path) + + +def create_vnfd_dict_file(): + vnfd_dict_str = str(get_vnfd_dict()) + file_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), 'vnfd_dict.yaml')) + vnfd_dict_file = open(file_path, "w+") + vnfd_dict_file.write(vnfd_dict_str) + vnfd_dict_file.close() + + +def delete_vnfd_dict_yaml_file(): + file_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), 'vnfd_dict.yaml')) + if os.path.exists(file_path): + os.remove(file_path) + + +def get_vnfd_dict(image_path=None): + + if image_path is None: + image_path = 'fake/image/path' + + vnfd_dict = { + 'description': 'Simple deployment flavour for Sample VNF', + 'imports': ['/opt/stack/tacker/tacker/tests/unit/vnflcm/types.yaml'], + 'topology_template': + {'inputs': {'descriptor_id': {'type': 'string'}, + 'descriptor_version': {'type': 'string'}, + 'flavour_description': {'type': 'string'}, + 'flavour_id': {'type': 'string'}, + 'product_name': {'type': 'string'}, + 'provider': {'type': 'string'}, + 'software_version': {'type': 'string'}, + 'vnfm_info': { + 'entry_schema': {'type': 'string'}, + 'type': 'list'}}, + 'node_templates': { + 'CP3': {'properties': { + 'layer_protocols': ['ipv4'], 'order': 2}, + 'requirements': [{'virtual_binding': 'VDU1'}, + {'virtual_link': 'VL3'}], + 'type': 'tosca.nodes.nfv.VduCp'}, + 'CP4': {'properties': {'layer_protocols': ['ipv4'], + 'order': 3}, + 'requirements': [{'virtual_binding': 'VDU1'}, + {'virtual_link': 'VL4'}], + 'type': 'tosca.nodes.nfv.VduCp'}, + 'VDU1': {'artifacts': { + 'sw_image': { + 'file': image_path, + 'type': 'tosca.artifacts.nfv.SwImage'}}, + 'capabilities': { + 'virtual_compute': {'properties': { + 'virtual_cpu': {'num_virtual_cpu': 1}, + 'virtual_local_storage': [ + {'size_of_storage': '1 ''GiB'}], + 'virtual_memory': { + 'virtual_mem_size': '512 ''MiB'}}}}, + 'properties': { + 'description': 'VDU1 compute node', + 'name': 'VDU1', + 'sw_image_data': { + 'checksum': { + 'algorithm': 'fake algo', + 'hash': 'fake hash'}, + 'container_format': + 'fake container format', + 'disk_format': 'fake disk format', + 'min_disk': '1''GiB', + 'name': 'fake name', + 'size': 'fake size ' 'GiB', + 'version': 'fake version'}, + 'vdu_profile': { + 'max_number_of_instances': 1, + 'min_number_of_instances': 1}}, + 'type': 'tosca.nodes.nfv.Vdu.Compute'}, + 'VL3': {'properties': { + 'connectivity_type': {'layer_protocols': []}, + 'description': 'Internal virtual link in VNF', + 'vl_profile': { + 'max_bitrate_requirements': { + 'leaf': 1048576, + 'root': 1048576 + }, + 'min_bitrate_requirements': { + 'leaf': 1048576, + 'root': 1048576 + }, + 'virtual_link_protocol_data': [ + {'layer_protocol': 'ipv4', + 'l3_protocol_data': {} + }]}}, + 'type': 'tosca.nodes.nfv.VnfVirtualLink'}, + 'VL4': {'properties': {'connectivity_type': { + 'layer_protocols': ['ipv4']}, + 'description': 'Internal virtual link in VNF', + 'vl_profile': {}}, + 'type': 'tosca.nodes.nfv.VnfVirtualLink'}, + 'VNF': {'interfaces': {'Vnflcm': { + 'instantiate': [], + 'instantiate_end': [], + 'instantiate_start': [], + 'modify_information': [], + 'modify_information_end': [], + 'modify_information_start': [], 'terminate': [], + 'terminate_end': [], 'terminate_start': []}}, + 'properties': { + 'flavour_description': 'A simple flavor'}, + 'type': 'company.provider.VNF'}}, + 'substitution_mappings': { + 'node_type': 'company.provider.VNF', + 'properties': {'flavour_id': 'simple'}, + 'requirements': { + 'virtual_link_external': [ + 'CP1', 'virtual_link']}}}, + 'tosca_definitions_version': 'tosca_simple_yaml_1_2'} + + return vnfd_dict + + +def get_dummy_vnf_instance(): + connection_info = get_dummy_vim_connection_info() + return {'created_at': '', 'deleted': False, 'deleted_at': None, + 'id': 'fake_id', 'instantiated_vnf_info': None, + 'instantiation_state': 'NOT_INSTANTIATED', + 'tenant_id': 'fake_tenant_id', 'updated_at': '', + 'vim_connection_info': [connection_info], + 'vnf_instance_description': 'VNF Description', + 'vnf_instance_name': 'test', 'vnf_product_name': 'Sample VNF', + 'vnf_provider': 'Company', 'vnf_software_version': '1.0', + 'vnfd_id': 'fake_vnfd_id', 'vnfd_version': '1.0'} + + +def get_dummy_vim_connection_info(): + return {'access_info': { + 'auth_url': 'fake/url', + 'cert_verify': 'False', 'password': 'admin', + 'project_domain_name': 'Default', + 'project_id': None, 'project_name': 'admin', + 'user_domain_name': 'Default', 'username': 'admin'}, + 'created_at': '', 'deleted': False, 'deleted_at': '', + 'id': 'fake_id', 'updated_at': '', + 'vim_id': 'fake_vim_id', 'vim_type': 'openstack'} + + +def get_dummy_instantiate_vnf_request(**updates): + instantiate_vnf_request = { + 'additional_params': None, 'created_at': '', 'deleted': '', + 'deleted_at': '', 'flavour_id': 'simple', + 'instantiation_level_id': 'instantiation_level_1', + 'updated_at': '', 'vim_connection_info': []} + + if updates: + instantiate_vnf_request['vim_connection_info'].append(updates) + + return instantiate_vnf_request + + +def get_instantiate_vnf_request_with_ext_virtual_links(**updates): + instantiate_vnf_request = \ + {"flavourId": "simple", + "instantiationLevelId": "instantiation_level_1", + "extVirtualLinks": [{ + "id": "ext-vl-uuid-VL1", + "vimConnectionId": "8a3adb69-0784-43c7-833e-aab0b6ab4470", + "resourceId": "f671ea41-bb4a-4b86-b6bd-b058f68f0498", + "extCps": [{ + "cpdId": "CP1", + "cpConfig": [{ + "linkPortId": "ee2982f6-8d0d-4649-9357-e527bcb68ed1", + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + "ipAddresses": [{ + "type": "IPV4", + "fixedAddresses": ["192.168.120.95"], + "subnetId": "f577b050-b80a-baed-96db88cd529b" + }]}}]}]}]}, + { + "id": "ext-vl-uuid-VL1", + "vimConnectionId": "8a3adb69-0784-43c7-833e-aab0b6ab4470", + "resourceId": "f671ea41-bb4a-4b86-b6bd-b058f68f0498", + "extCps": [{ + "cpdId": "CP2", + "cpConfig": [{ + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + "ipAddresses": [{ + "type": "IPV4", + "fixedAddresses": ["192.168.120.96"], + "subnetId": "f577b050-b80a-96db88cd529b" + }]}}]}]}]}], + "extManagedVirtualLinks": [{ + "id": "extMngVLnk-uuid_VL3", + "vnfVirtualLinkDescId": "VL3", + "vimConnectionId": "8a3adb69-0784-43c7-833e-aab0b6ab4470", + "resourceId": "f671ea41-bb4a-4b86-b6bd-b058f68f0498" + }], + "vimConnectionInfo": [] + } + + if updates: + instantiate_vnf_request.update(updates) + + return instantiate_vnf_request + + +def get_dummy_grant_response(): + return {'VDU1': {'checksum': {'algorithm': 'fake algo', + 'hash': 'fake hash'}, + 'container_format': 'fake container format', + 'disk_format': 'fake disk format', + 'image_path': ('/var/lib/tacker/vnfpackages/' + + uuidsentinel.instance_id + + '/Files/images/path'), + 'min_disk': 1, + 'min_ram': 0, + 'name': 'fake name', + 'version': 'fake version'}} + + +def return_vnf_resource(): + version_obj = objects.VnfResource( + created_at=datetime.datetime(1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC), + deleted=False, + deleted_at=None, + id=uuidsentinel.vnf_resource_id, + resource_identifier=uuidsentinel.resource_identifier, + resource_name='test-image', + resource_status='CREATED', + resource_type='image', + updated_at=None, + vnf_instance_id=uuidsentinel.vnf_instance_id + ) + return version_obj + + class InjectContext(wsgi.Middleware): """Add a 'tacker.context' to WSGI environ.""" diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 1123235ca..ecfbf440d 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -21,11 +21,16 @@ from webob import exc from tacker.api.vnflcm.v1 import controller from tacker.common import exceptions +from tacker.conductor.conductorrpc.vnf_lcm_rpc import VNFLcmRPCAPI +from tacker.extensions import nfvo from tacker import objects +from tacker.objects import fields +from tacker.tests import constants from tacker.tests.unit import base from tacker.tests.unit import fake_request from tacker.tests.unit.vnflcm import fakes from tacker.tests import uuidsentinel +from tacker.vnfm import vim_client @ddt.ddt @@ -193,3 +198,448 @@ class TestController(base.TestCase): req.method = 'POST' resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + @mock.patch.object(VNFLcmRPCAPI, "instantiate") + def test_instantiate_with_deployment_flavour( + self, mock_instantiate, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, mock_save, + mock_vnf_instance_get_by_id, mock_get_vim): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + + body = {"flavourId": "simple"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_instantiate.assert_called_once() + + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + def test_instantiate_with_non_existing_deployment_flavour( + self, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + + body = {"flavourId": "invalid"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("No flavour with id 'invalid'.", + resp.json['badRequest']['message']) + + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + @mock.patch.object(VNFLcmRPCAPI, "instantiate") + def test_instantiate_with_instantiation_level( + self, mock_instantiate, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, mock_save, + mock_vnf_instance_get_by_id, mock_get_vim): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + + body = {"flavourId": "simple", + "instantiationLevelId": "instantiation_level_1"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_instantiate.assert_called_once() + + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + @mock.patch.object(VNFLcmRPCAPI, "instantiate") + def test_instantiate_with_no_inst_level_in_flavour( + self, mock_instantiate, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, mock_save, + mock_vnf_instance_get_by_id, mock_get_vim): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + vnf_package = fakes.return_vnf_package_with_deployment_flavour() + vnf_package.vnf_deployment_flavours[0].instantiation_levels = None + mock_vnf_package_get_by_id.return_value = vnf_package + + # No instantiation level in deployment flavour but it's passed in the + # request + body = {"flavourId": "simple", + "instantiationLevelId": "instantiation_level_1"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("No instantiation level with id " + "'instantiation_level_1'.", resp.json['badRequest']['message']) + + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + @mock.patch.object(VNFLcmRPCAPI, "instantiate") + def test_instantiate_with_non_existing_instantiation_level( + self, mock_instantiate, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + + body = {"flavourId": "simple", + "instantiationLevelId": "non-existing"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("No instantiation level with id 'non-existing'.", + resp.json['badRequest']['message']) + + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + @mock.patch.object(VNFLcmRPCAPI, "instantiate") + def test_instantiate_with_vim_connection( + self, mock_instantiate, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, mock_save, + mock_vnf_instance_get_by_id, mock_get_vim): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + + body = {"flavourId": "simple", + "vimConnectionInfo": [ + {"id": uuidsentinel.vim_connection_id, + "vimId": uuidsentinel.vim_id, + "vimType": 'openstack'} + ]} + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_instantiate.assert_called_once() + + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + def test_instantiate_with_non_existing_vim( + self, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id, mock_get_vim): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + mock_get_vim.side_effect = nfvo.VimNotFoundException + + body = {"flavourId": "simple", + "vimConnectionInfo": [ + {"id": uuidsentinel.vim_connection_id, + "vimId": uuidsentinel.vim_id, + "vimType": 'openstack'} + ]} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("VimConnection id is not found: %s" % + uuidsentinel.vim_id, resp.json['badRequest']['message']) + + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + def test_instantiate_with_non_existing_region_vim( + self, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id, mock_get_vim): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + mock_get_vim.side_effect = nfvo.VimRegionNotFoundException + + body = {"flavourId": "simple", + "vimConnectionInfo": [ + {'id': uuidsentinel.vim_connection_id, + 'vimId': uuidsentinel.vim_id, + 'vimType': 'openstack', + 'accessInfo': {"region": 'region_non_existing'}} + ]} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("Region not found for the VimConnection: %s" % + uuidsentinel.vim_id, resp.json['badRequest']['message']) + + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + def test_instantiate_with_default_vim_not_configured( + self, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id, mock_get_vim): + + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + mock_get_vim.side_effect = nfvo.VimDefaultNotDefined + + body = {"flavourId": "simple"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("Default VIM is not defined.", + resp.json['badRequest']['message']) + + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + def test_instantiate_incorrect_instantiation_state(self, mock_vnf_by_id): + vnf_instance = fakes.return_vnf_instance_model() + vnf_instance.instantiation_state = 'INSTANTIATED' + mock_vnf_by_id.return_value = vnf_instance + + body = {"flavourId": "simple"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.CONFLICT, resp.status_code) + + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + def test_instantiate_incorrect_task_state(self, mock_vnf_by_id): + vnf_instance = fakes.return_vnf_instance_model( + task_state=fields.VnfInstanceTaskState.INSTANTIATING) + mock_vnf_by_id.return_value = vnf_instance + + body = {"flavourId": "simple"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + resp = req.get_response(self.app) + + self.assertEqual(http_client.CONFLICT, resp.status_code) + expected_msg = ("Vnf instance %s in task_state INSTANTIATING. Cannot " + "instantiate while the vnf instance is in this state.") + self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, + resp.json['conflictingRequest']['message']) + + @ddt.data({'attribute': 'flavourId', 'value': 123, + 'expected_type': 'string'}, + {'attribute': 'flavourId', 'value': True, + 'expected_type': 'string'}, + {'attribute': 'instantiationLevelId', 'value': 123, + 'expected_type': 'string'}, + {'attribute': 'instantiationLevelId', 'value': True, + 'expected_type': 'string'}, + {'attribute': 'additionalParams', 'value': ['val1', 'val2'], + 'expected_type': 'object'}, + {'attribute': 'additionalParams', 'value': True, + 'expected_type': 'object'}, + {'attribute': 'additionalParams', 'value': 123, + 'expected_type': 'object'}, + ) + @ddt.unpack + def test_instantiate_with_invalid_request_body( + self, attribute, value, expected_type): + body = fakes.get_vnf_instantiation_request_body() + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + + body.update({attribute: value}) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + exception = self.assertRaises( + exceptions.ValidationError, self.controller.instantiate, + req, body=body) + expected_message = \ + ("Invalid input for field/attribute {attribute}. Value: {value}. " + "{value} is not of type '{expected_type}'". + format(value=value, attribute=attribute, + expected_type=expected_type)) + + self.assertEqual(expected_message, exception.msg) + + def test_instantiate_without_flavour_id(self): + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes({}) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("'flavourId' is a required property", + resp.json['badRequest']['message']) + + def test_instantiate_invalid_request_parameter(self): + body = {"flavourId": "simple"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + + # Pass invalid request parameter + body = {"flavourId": "simple"} + body.update({'additional_property': 'test_value'}) + + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("Additional properties are not allowed " + "('additional_property' was unexpected)", + resp.json['badRequest']['message']) + + def test_instantiate_with_invalid_uuid(self): + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % constants.INVALID_UUID) + body = {"flavourId": "simple"} + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + + self.assertEqual(http_client.NOT_FOUND, resp.status_code) + self.assertEqual( + "Can not find requested vnf instance: %s" % constants.INVALID_UUID, + resp.json['itemNotFound']['message']) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_instantiate_with_non_existing_vnf_instance( + self, mock_vnf_by_id): + mock_vnf_by_id.side_effect = exceptions.VnfInstanceNotFound + body = {"flavourId": "simple"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + + self.assertEqual(http_client.NOT_FOUND, resp.status_code) + self.assertEqual("Can not find requested vnf instance: %s" % + uuidsentinel.vnf_instance_id, + resp.json['itemNotFound']['message']) + + @ddt.data('HEAD', 'PUT', 'DELETE', 'PATCH', 'GET') + def test_instantiate_invalid_http_method(self, method): + # Wrong HTTP method + body = fakes.get_vnf_instantiation_request_body() + req = fake_request.HTTPRequest.blank( + '/vnf_instances/29c770a3-02bc-4dfc-b4be-eb173ac00567/instantiate') + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = method + resp = req.get_response(self.app) + self.assertEqual(http_client.METHOD_NOT_ALLOWED, resp.status_code) diff --git a/tacker/tests/unit/vnflcm/test_utils.py b/tacker/tests/unit/vnflcm/test_utils.py new file mode 100644 index 000000000..77b3f2605 --- /dev/null +++ b/tacker/tests/unit/vnflcm/test_utils.py @@ -0,0 +1,49 @@ +# Copyright (c) 2020 NTT DATA +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import ddt +from oslo_config import cfg + +from tacker.tests.unit import base +from tacker.tests.unit.vnflcm import fakes +from tacker.tests import uuidsentinel +from tacker.vnflcm import utils as vnflcm_utils + + +@ddt.ddt +class VnfLcmUtilsTestCase(base.TestCase): + + @ddt.data( + {'image_path': 'cirros-0.4.0-x86_64-disk.img', + 'extracted_path': 'cirros-0.4.0-x86_64-disk.img'}, + {'image_path': '../ImageFiles/image/cirros-0.4.0-x86_64-disk.img', + 'extracted_path': 'ImageFiles/image/cirros-0.4.0-x86_64-disk.img'}, + {'image_path': '../../Files/image/cirros-0.4.0-x86_64-disk.img', + 'extracted_path': 'Files/image/cirros-0.4.0-x86_64-disk.img'} + ) + @ddt.unpack + def test_create_grant_request_with_software_image_path(self, image_path, + extracted_path): + vnf_package_id = uuidsentinel.package_uuid + vnfd_dict = fakes.get_vnfd_dict(image_path=image_path) + vnf_software_images = vnflcm_utils._create_grant_request( + vnfd_dict, vnf_package_id) + vnf_package_path = cfg.CONF.vnf_package.vnf_package_csar_path + expected_image_path = os.path.join(vnf_package_path, vnf_package_id, + extracted_path) + self.assertEqual(expected_image_path, + vnf_software_images['VDU1'].image_path) diff --git a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py new file mode 100644 index 000000000..7cde0d099 --- /dev/null +++ b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py @@ -0,0 +1,336 @@ +# Copyright (c) 2020 NTT DATA +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import shutil +import zipfile + +import fixtures +import mock +from oslo_config import cfg +from tacker.common import exceptions +from tacker.common import utils +from tacker import context +from tacker import objects +from tacker.tests.unit.db import base as db_base +from tacker.tests.unit.vnflcm import fakes +from tacker.tests import uuidsentinel +from tacker.vnflcm import vnflcm_driver + + +class InfraDriverException(Exception): + pass + + +class FakeDriverManager(mock.Mock): + def __init__(self, fail_method_name=None, vnf_resource_count=1): + super(FakeDriverManager, self).__init__() + self.fail_method_name = fail_method_name + self.vnf_resource_count = vnf_resource_count + + def invoke(self, *args, **kwargs): + if 'pre_instantiation_vnf' in args: + vnf_resource_list = [fakes.return_vnf_resource() for index in + range(self.vnf_resource_count)] + return {'node_name': vnf_resource_list} + if 'instantiate_vnf' in args: + if self.fail_method_name and \ + self.fail_method_name == 'instantiate_vnf': + raise InfraDriverException("instantiate_vnf failed") + + instance_id = uuidsentinel.instance_id + vnfd_dict = kwargs.get('vnfd_dict') + vnfd_dict['instance_id'] = instance_id + return instance_id + if 'create_wait' in args: + if self.fail_method_name and \ + self.fail_method_name == 'create_wait': + raise InfraDriverException("create_wait failed") + if 'post_vnf_instantiation' in args: + pass + if 'delete' in args: + if self.fail_method_name and \ + self.fail_method_name == 'delete': + raise InfraDriverException("delete failed") + if 'delete_wait' in args: + if self.fail_method_name and \ + self.fail_method_name == 'delete_wait': + raise InfraDriverException("delete_wait failed") + if 'delete_vnf_instance_resource' in args: + if self.fail_method_name and \ + self.fail_method_name == 'delete_vnf_resource': + raise InfraDriverException("delete_vnf_resource failed") + + +class FakeVimClient(mock.Mock): + pass + + +class TestVnflcmDriver(db_base.SqlTestCase): + + def setUp(self): + super(TestVnflcmDriver, self).setUp() + self.addCleanup(mock.patch.stopall) + self.context = context.get_admin_context() + self._mock_vim_client() + self._stub_get_vim() + self.temp_dir = self.useFixture(fixtures.TempDir()).path + + def _mock_vnf_manager(self, fail_method_name=None, vnf_resource_count=1): + self._vnf_manager = mock.Mock(wraps=FakeDriverManager( + fail_method_name=fail_method_name, + vnf_resource_count=vnf_resource_count)) + self._vnf_manager.__contains__ = mock.Mock( + return_value=True) + fake_vnf_manager = mock.Mock() + fake_vnf_manager.return_value = self._vnf_manager + self._mock( + 'tacker.common.driver_manager.DriverManager', fake_vnf_manager) + + def _mock_vim_client(self): + self.vim_client = mock.Mock(wraps=FakeVimClient()) + fake_vim_client = mock.Mock() + fake_vim_client.return_value = self.vim_client + self._mock( + 'tacker.vnfm.vim_client.VimClient', fake_vim_client) + + def _stub_get_vim(self): + vim_obj = {'vim_id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'vim_name': 'fake_vim', 'vim_auth': + {'auth_url': 'http://localhost/identity', 'password': + 'test_pw', 'username': 'test_user', 'project_name': + 'test_project'}, 'vim_type': 'openstack'} + self.vim_client.get_vim.return_value = vim_obj + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfInstance, "save") + def test_instantiate_vnf(self, mock_vnf_instance_save, + mock_vnf_package_vnfd, mock_create): + vnf_package_vnfd = fakes.return_vnf_package_vnfd() + vnf_package_id = vnf_package_vnfd.package_uuid + mock_vnf_package_vnfd.return_value = vnf_package_vnfd + instantiate_vnf_req_dict = fakes.get_dummy_instantiate_vnf_request() + instantiate_vnf_req_obj = \ + objects.InstantiateVnfRequest.obj_from_primitive( + instantiate_vnf_req_dict, self.context) + vnf_instance_obj = fakes.return_vnf_instance() + + fake_csar = os.path.join(self.temp_dir, vnf_package_id) + cfg.CONF.set_override('vnf_package_csar_path', self.temp_dir, + group='vnf_package') + base_path = os.path.dirname(os.path.abspath(__file__)) + sample_vnf_package_zip = os.path.join( + base_path, "../../etc/samples/sample_vnf_package_csar.zip") + extracted_zip_path = fake_csar + zipfile.ZipFile(sample_vnf_package_zip, 'r').extractall( + extracted_zip_path) + + self._mock_vnf_manager() + driver = vnflcm_driver.VnfLcmDriver() + driver.instantiate_vnf(self.context, vnf_instance_obj, + instantiate_vnf_req_obj) + + self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) + self.assertEqual(2, mock_vnf_instance_save.call_count) + self.assertEqual(4, self._vnf_manager.invoke.call_count) + shutil.rmtree(fake_csar) + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfInstance, "save") + def test_instantiate_vnf_with_ext_virtual_links( + self, mock_vnf_instance_save, mock_vnf_package_vnfd, mock_create): + vnf_package_vnfd = fakes.return_vnf_package_vnfd() + vnf_package_id = vnf_package_vnfd.package_uuid + mock_vnf_package_vnfd.return_value = vnf_package_vnfd + req_body = fakes.get_instantiate_vnf_request_with_ext_virtual_links() + instantiate_vnf_req_dict = utils.convert_camelcase_to_snakecase( + req_body) + instantiate_vnf_req_obj = \ + objects.InstantiateVnfRequest.obj_from_primitive( + instantiate_vnf_req_dict, self.context) + vnf_instance_obj = fakes.return_vnf_instance() + + fake_csar = os.path.join(self.temp_dir, vnf_package_id) + cfg.CONF.set_override('vnf_package_csar_path', self.temp_dir, + group='vnf_package') + base_path = os.path.dirname(os.path.abspath(__file__)) + sample_vnf_package_zip = os.path.join( + base_path, "../../etc/samples/sample_vnf_package_csar.zip") + extracted_zip_path = fake_csar + zipfile.ZipFile(sample_vnf_package_zip, 'r').extractall( + extracted_zip_path) + + self._mock_vnf_manager() + driver = vnflcm_driver.VnfLcmDriver() + driver.instantiate_vnf(self.context, vnf_instance_obj, + instantiate_vnf_req_obj) + + self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) + self.assertEqual(2, mock_vnf_instance_save.call_count) + self.assertEqual(4, self._vnf_manager.invoke.call_count) + shutil.rmtree(fake_csar) + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfInstance, "save") + def test_instantiate_vnf_vim_connection_info( + self, mock_vnf_instance_save, mock_vnf_package_vnfd, mock_create): + vnf_package_vnfd = fakes.return_vnf_package_vnfd() + vnf_package_id = vnf_package_vnfd.package_uuid + mock_vnf_package_vnfd.return_value = vnf_package_vnfd + vim_connection_info = fakes.get_dummy_vim_connection_info() + instantiate_vnf_req_dict = \ + fakes.get_dummy_instantiate_vnf_request(**vim_connection_info) + instantiate_vnf_req_obj = \ + objects.InstantiateVnfRequest.obj_from_primitive( + instantiate_vnf_req_dict, self.context) + vnf_instance_obj = fakes.return_vnf_instance() + + fake_csar = os.path.join(self.temp_dir, vnf_package_id) + cfg.CONF.set_override('vnf_package_csar_path', self.temp_dir, + group='vnf_package') + base_path = os.path.dirname(os.path.abspath(__file__)) + sample_vnf_package_zip = os.path.join( + base_path, "../../etc/samples/sample_vnf_package_csar.zip") + extracted_zip_path = fake_csar + zipfile.ZipFile(sample_vnf_package_zip, 'r').extractall( + extracted_zip_path) + + self._mock_vnf_manager() + driver = vnflcm_driver.VnfLcmDriver() + driver.instantiate_vnf(self.context, vnf_instance_obj, + instantiate_vnf_req_obj) + + self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) + self.assertEqual(2, mock_vnf_instance_save.call_count) + self.assertEqual(4, self._vnf_manager.invoke.call_count) + shutil.rmtree(fake_csar) + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfInstance, "save") + def test_instantiate_vnf_infra_fails_to_instantiate( + self, mock_vnf_instance_save, mock_vnf_package_vnfd, mock_create): + vnf_package_vnfd = fakes.return_vnf_package_vnfd() + vnf_package_id = vnf_package_vnfd.package_uuid + mock_vnf_package_vnfd.return_value = vnf_package_vnfd + vim_connection_info = fakes.get_dummy_vim_connection_info() + instantiate_vnf_req_dict = \ + fakes.get_dummy_instantiate_vnf_request(**vim_connection_info) + instantiate_vnf_req_obj = \ + objects.InstantiateVnfRequest.obj_from_primitive( + instantiate_vnf_req_dict, self.context) + vnf_instance_obj = fakes.return_vnf_instance() + + fake_csar = os.path.join(self.temp_dir, vnf_package_id) + cfg.CONF.set_override('vnf_package_csar_path', self.temp_dir, + group='vnf_package') + base_path = os.path.dirname(os.path.abspath(__file__)) + sample_vnf_package_zip = os.path.join( + base_path, "../../etc/samples/sample_vnf_package_csar.zip") + extracted_zip_path = fake_csar + zipfile.ZipFile(sample_vnf_package_zip, 'r').extractall( + extracted_zip_path) + + self._mock_vnf_manager(fail_method_name="instantiate_vnf") + driver = vnflcm_driver.VnfLcmDriver() + error = self.assertRaises(exceptions.VnfInstantiationFailed, + driver.instantiate_vnf, self.context, vnf_instance_obj, + instantiate_vnf_req_obj) + expected_error = ("Vnf instantiation failed for vnf %s, error: " + "instantiate_vnf failed") + + self.assertEqual(expected_error % vnf_instance_obj.id, str(error)) + self.assertEqual("NOT_INSTANTIATED", + vnf_instance_obj.instantiation_state) + self.assertEqual(1, mock_vnf_instance_save.call_count) + self.assertEqual(2, self._vnf_manager.invoke.call_count) + + shutil.rmtree(fake_csar) + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfInstance, "save") + def test_instantiate_vnf_infra_fails_to_wait_after_instantiate( + self, mock_vnf_instance_save, mock_vnf_package_vnfd, mock_create): + vnf_package_vnfd = fakes.return_vnf_package_vnfd() + vnf_package_id = vnf_package_vnfd.package_uuid + mock_vnf_package_vnfd.return_value = vnf_package_vnfd + vim_connection_info = fakes.get_dummy_vim_connection_info() + instantiate_vnf_req_dict = \ + fakes.get_dummy_instantiate_vnf_request(**vim_connection_info) + instantiate_vnf_req_obj = \ + objects.InstantiateVnfRequest.obj_from_primitive( + instantiate_vnf_req_dict, self.context) + vnf_instance_obj = fakes.return_vnf_instance() + + fake_csar = os.path.join(self.temp_dir, vnf_package_id) + cfg.CONF.set_override('vnf_package_csar_path', self.temp_dir, + group='vnf_package') + base_path = os.path.dirname(os.path.abspath(__file__)) + sample_vnf_package_zip = os.path.join( + base_path, "../../etc/samples/sample_vnf_package_csar.zip") + extracted_zip_path = fake_csar + zipfile.ZipFile(sample_vnf_package_zip, 'r').extractall( + extracted_zip_path) + + self._mock_vnf_manager(fail_method_name='create_wait') + driver = vnflcm_driver.VnfLcmDriver() + error = self.assertRaises(exceptions.VnfInstantiationWaitFailed, + driver.instantiate_vnf, self.context, vnf_instance_obj, + instantiate_vnf_req_obj) + expected_error = ("Vnf instantiation wait failed for vnf %s, error: " + "create_wait failed") + + self.assertEqual(expected_error % vnf_instance_obj.id, str(error)) + self.assertEqual("NOT_INSTANTIATED", + vnf_instance_obj.instantiation_state) + self.assertEqual(1, mock_vnf_instance_save.call_count) + self.assertEqual(3, self._vnf_manager.invoke.call_count) + + shutil.rmtree(fake_csar) + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfInstance, "save") + def test_instantiate_vnf_with_short_notation(self, mock_vnf_instance_save, + mock_vnf_package_vnfd, mock_create): + vnf_package_vnfd = fakes.return_vnf_package_vnfd() + vnf_package_id = vnf_package_vnfd.package_uuid + mock_vnf_package_vnfd.return_value = vnf_package_vnfd + instantiate_vnf_req_dict = fakes.get_dummy_instantiate_vnf_request() + instantiate_vnf_req_obj = \ + objects.InstantiateVnfRequest.obj_from_primitive( + instantiate_vnf_req_dict, self.context) + vnf_instance_obj = fakes.return_vnf_instance() + + fake_csar = os.path.join(self.temp_dir, vnf_package_id) + cfg.CONF.set_override('vnf_package_csar_path', self.temp_dir, + group='vnf_package') + base_path = os.path.dirname(os.path.abspath(__file__)) + sample_vnf_package_zip = os.path.join( + base_path, "../../etc/samples/" + "sample_vnf_package_csar_with_short_notation.zip") + extracted_zip_path = fake_csar + zipfile.ZipFile(sample_vnf_package_zip, 'r').extractall( + extracted_zip_path) + self._mock_vnf_manager(vnf_resource_count=2) + driver = vnflcm_driver.VnfLcmDriver() + driver.instantiate_vnf(self.context, vnf_instance_obj, + instantiate_vnf_req_obj) + self.assertEqual(2, mock_create.call_count) + self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) + shutil.rmtree(fake_csar) diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/etsi_nfv_sol001_common_types.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/etsi_nfv_sol001_common_types.yaml new file mode 100644 index 000000000..cf89a2805 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/etsi_nfv_sol001_common_types.yaml @@ -0,0 +1,202 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 +description: ETSI NFV SOL 001 common types definitions version 2.6.1 +metadata: + template_name: etsi_nfv_sol001_common_types + template_author: ETSI_NFV + template_version: 2.6.1 + +data_types: + tosca.datatypes.nfv.L2AddressData: + derived_from: tosca.datatypes.Root + description: Describes the information on the MAC addresses to be assigned to a connection point. + properties: + mac_address_assignment: + type: boolean + description: Specifies if the address assignment is the responsibility of management and orchestration function or not. If it is set to True, it is the management and orchestration function responsibility + required: true + + tosca.datatypes.nfv.L3AddressData: + derived_from: tosca.datatypes.Root + description: Provides information about Layer 3 level addressing scheme and parameters applicable to a CP + properties: + ip_address_assignment: + type: boolean + description: Specifies if the address assignment is the responsibility of management and orchestration function or not. If it is set to True, it is the management and orchestration function responsibility + required: true + floating_ip_activated: + type: boolean + description: Specifies if the floating IP scheme is activated on the Connection Point or not + required: true + ip_address_type: + type: string + description: Defines address type. The address type should be aligned with the address type supported by the layer_protocols properties of the parent VnfExtCp + required: false + constraints: + - valid_values: [ ipv4, ipv6 ] + number_of_ip_address: + type: integer + description: Minimum number of IP addresses to be assigned + required: false + constraints: + - greater_than: 0 + + tosca.datatypes.nfv.AddressData: + derived_from: tosca.datatypes.Root + description: Describes information about the addressing scheme and parameters applicable to a CP + properties: + address_type: + type: string + description: Describes the type of the address to be assigned to a connection point. The content type shall be aligned with the address type supported by the layerProtocol property of the connection point + required: true + constraints: + - valid_values: [ mac_address, ip_address ] + l2_address_data: + type: tosca.datatypes.nfv.L2AddressData + description: Provides the information on the MAC addresses to be assigned to a connection point. + required: false + l3_address_data: + type: tosca.datatypes.nfv.L3AddressData + description: Provides the information on the IP addresses to be assigned to a connection point + required: false + + tosca.datatypes.nfv.ConnectivityType: + derived_from: tosca.datatypes.Root + description: describes additional connectivity information of a virtualLink + properties: + layer_protocols: + type: list + description: Identifies the protocol a virtualLink gives access to (ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire).The top layer protocol of the virtualLink protocol stack shall always be provided. The lower layer protocols may be included when there are specific requirements on these layers. + required: true + entry_schema: + type: string + constraints: + - valid_values: [ ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire ] + flow_pattern: + type: string + description: Identifies the flow pattern of the connectivity + required: false + constraints: + - valid_values: [ line, tree, mesh ] + + tosca.datatypes.nfv.LinkBitrateRequirements: + derived_from: tosca.datatypes.Root + description: describes the requirements in terms of bitrate for a virtual link + properties: + root: + type: integer # in bits per second + description: Specifies the throughput requirement in bits per second of the link (e.g. bitrate of E-Line, root bitrate of E-Tree, aggregate capacity of E-LAN). + required: true + constraints: + - greater_or_equal: 0 + leaf: + type: integer # in bits per second + description: Specifies the throughput requirement in bits per second of leaf connections to the link when applicable to the connectivity type (e.g. for E-Tree and E LAN branches). + required: false + constraints: + - greater_or_equal: 0 + + tosca.datatypes.nfv.CpProtocolData: + derived_from: tosca.datatypes.Root + description: Describes and associates the protocol layer that a CP uses together with other protocol and connection point information + properties: + associated_layer_protocol: + type: string + required: true + description: One of the values of the property layer_protocols of the CP + constraints: + - valid_values: [ ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire ] + address_data: + type: list + description: Provides information on the addresses to be assigned to the CP + entry_schema: + type: tosca.datatypes.nfv.AddressData + required: false + + tosca.datatypes.nfv.VnfProfile: + derived_from: tosca.datatypes.Root + description: describes a profile for instantiating VNFs of a particular NS DF according to a specific VNFD and VNF DF. + properties: + instantiation_level: + type: string + description: Identifier of the instantiation level of the VNF DF to be used for instantiation. If not present, the default instantiation level as declared in the VNFD shall be used. + required: false + min_number_of_instances: + type: integer + description: Minimum number of instances of the VNF based on this VNFD that is permitted to exist for this VnfProfile. + required: true + constraints: + - greater_or_equal: 0 + max_number_of_instances: + type: integer + description: Maximum number of instances of the VNF based on this VNFD that is permitted to exist for this VnfProfile. + required: true + constraints: + - greater_or_equal: 0 + + tosca.datatypes.nfv.Qos: + derived_from: tosca.datatypes.Root + description: describes QoS data for a given VL used in a VNF deployment flavour + properties: + latency: + type: scalar-unit.time #Number + description: Specifies the maximum latency + required: true + constraints: + - greater_than: 0 s + packet_delay_variation: + type: scalar-unit.time #Number + description: Specifies the maximum jitter + required: true + constraints: + - greater_or_equal: 0 s + packet_loss_ratio: + type: float + description: Specifies the maximum packet loss ratio + required: false + constraints: + - in_range: [ 0.0, 1.0 ] + +capability_types: + tosca.capabilities.nfv.VirtualLinkable: + derived_from: tosca.capabilities.Node + description: A node type that includes the VirtualLinkable capability indicates that it can be pointed by tosca.relationships.nfv.VirtualLinksTo relationship type + +relationship_types: + tosca.relationships.nfv.VirtualLinksTo: + derived_from: tosca.relationships.DependsOn + description: Represents an association relationship between the VduCp and VnfVirtualLink node types + valid_target_types: [ tosca.capabilities.nfv.VirtualLinkable ] + +node_types: + tosca.nodes.nfv.Cp: + derived_from: tosca.nodes.Root + description: Provides information regarding the purpose of the connection point + properties: + layer_protocols: + type: list + description: Identifies which protocol the connection point uses for connectivity purposes + required: true + entry_schema: + type: string + constraints: + - valid_values: [ ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire ] + role: #Name in ETSI NFV IFA011 v0.7.3: cpRole + type: string + description: Identifies the role of the port in the context of the traffic flow patterns in the VNF or parent NS + required: false + constraints: + - valid_values: [ root, leaf ] + description: + type: string + description: Provides human-readable information on the purpose of the connection point + required: false + protocol: + type: list + description: Provides information on the addresses to be assigned to the connection point(s) instantiated from this Connection Point Descriptor + required: false + entry_schema: + type: tosca.datatypes.nfv.CpProtocolData + trunk_mode: + type: boolean + description: Provides information about whether the CP instantiated from this Cp is in Trunk mode (802.1Q or other), When operating in "trunk mode", the Cp is capable of carrying traffic for several VLANs. Absence of this property implies that trunkMode is not configured for the Cp i.e. It is equivalent to boolean value "false". + required: false diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/etsi_nfv_sol001_vnfd_types.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/etsi_nfv_sol001_vnfd_types.yaml new file mode 100644 index 000000000..458842e0c --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/etsi_nfv_sol001_vnfd_types.yaml @@ -0,0 +1,1352 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 +description: ETSI NFV SOL 001 vnfd types definitions version 2.6.1 +metadata: + template_name: etsi_nfv_sol001_vnfd_types + template_author: ETSI_NFV + template_version: 2.6.1 + +data_types: + tosca.datatypes.nfv.VirtualNetworkInterfaceRequirements: + derived_from: tosca.datatypes.Root + description: Describes requirements on a virtual network interface + properties: + name: + type: string + description: Provides a human readable name for the requirement. + required: false + description: + type: string + description: Provides a human readable description of the requirement. + required: false + support_mandatory: + type: boolean + description: Indicates whether fulfilling the constraint is mandatory (TRUE) for successful operation or desirable (FALSE). + required: true + network_interface_requirements: + type: map + description: The network interface requirements. A map of strings that contain a set of key-value pairs that describes the hardware platform specific network interface deployment requirements. + required: true + entry_schema: + type: string + nic_io_requirements: + type: tosca.datatypes.nfv.LogicalNodeData + description: references (couples) the CP with any logical node I/O requirements (for network devices) that may have been created. Linking these attributes is necessary so that so that I/O requirements that need to be articulated at the logical node level can be associated with the network interface requirements associated with the CP. + required: false + + tosca.datatypes.nfv.RequestedAdditionalCapability: + derived_from: tosca.datatypes.Root + description: describes requested additional capability for a particular VDU + properties: + requested_additional_capability_name: + type: string + description: Identifies a requested additional capability for the VDU. + required: true + support_mandatory: + type: boolean + description: Indicates whether the requested additional capability is mandatory for successful operation. + required: true + min_requested_additional_capability_version: + type: string + description: Identifies the minimum version of the requested additional capability. + required: false + preferred_requested_additional_capability_version: + type: string + description: Identifies the preferred version of the requested additional capability. + required: false + target_performance_parameters: + type: map + description: Identifies specific attributes, dependent on the requested additional capability type. + required: true + entry_schema: + type: string + + tosca.datatypes.nfv.VirtualMemory: + derived_from: tosca.datatypes.Root + description: supports the specification of requirements related to virtual memory of a virtual compute resource + properties: + virtual_mem_size: + type: scalar-unit.size + description: Amount of virtual memory. + required: true + virtual_mem_oversubscription_policy: + type: string + description: The memory core oversubscription policy in terms of virtual memory to physical memory on the platform. + required: false + vdu_mem_requirements: + type: map + description: The hardware platform specific VDU memory requirements. A map of strings that contains a set of key-value pairs that describes hardware platform specific VDU memory requirements. + required: false + entry_schema: + type: string + numa_enabled: + type: boolean + description: It specifies the memory allocation to be cognisant of the relevant process/core allocation. + required: false + default: false + + tosca.datatypes.nfv.VirtualCpu: + derived_from: tosca.datatypes.Root + description: Supports the specification of requirements related to virtual CPU(s) of a virtual compute resource + properties: + cpu_architecture: + type: string + description: CPU architecture type. Examples are x86, ARM + required: false + num_virtual_cpu: + type: integer + description: Number of virtual CPUs + required: true + constraints: + - greater_than: 0 + virtual_cpu_clock: + type: scalar-unit.frequency + description: Minimum virtual CPU clock rate + required: false + virtual_cpu_oversubscription_policy: + type: string + description: CPU core oversubscription policy e.g. the relation of virtual CPU cores to physical CPU cores/threads. + required: false + vdu_cpu_requirements: + type: map + description: The hardware platform specific VDU CPU requirements. A map of strings that contains a set of key-value pairs describing VDU CPU specific hardware platform requirements. + required: false + entry_schema: + type: string + virtual_cpu_pinning: + type: tosca.datatypes.nfv.VirtualCpuPinning + description: The virtual CPU pinning configuration for the virtualised compute resource. + required: false + + tosca.datatypes.nfv.VirtualCpuPinning: + derived_from: tosca.datatypes.Root + description: Supports the specification of requirements related to the virtual CPU pinning configuration of a virtual compute resource + properties: + virtual_cpu_pinning_policy: + type: string + description: 'Indicates the policy for CPU pinning. The policy can take values of "static" or "dynamic". In case of "dynamic" the allocation of virtual CPU cores to logical CPU cores is decided by the VIM. (e.g.: SMT (Simultaneous Multi-Threading) requirements). In case of "static" the allocation is requested to be according to the virtual_cpu_pinning_rule.' + required: false + constraints: + - valid_values: [ static, dynamic ] + virtual_cpu_pinning_rule: + type: list + description: Provides the list of rules for allocating virtual CPU cores to logical CPU cores/threads + required: false + entry_schema: + type: string + + tosca.datatypes.nfv.VnfcConfigurableProperties: + derived_from: tosca.datatypes.Root + description: Defines the configurable properties of a VNFC + + tosca.datatypes.nfv.VnfcAdditionalConfigurableProperties: + derived_from: tosca.datatypes.Root + description: VnfcAdditionalConfigurableProperties type is an empty base type for deriving data types for describing additional configurable properties for a given VNFC. + + tosca.datatypes.nfv.VduProfile: + derived_from: tosca.datatypes.Root + description: describes additional instantiation data for a given Vdu.Compute used in a specific deployment flavour. + properties: + min_number_of_instances: + type: integer + description: Minimum number of instances of the VNFC based on this Vdu.Compute that is permitted to exist for a particular VNF deployment flavour. + required: true + constraints: + - greater_or_equal: 0 + max_number_of_instances: + type: integer + description: Maximum number of instances of the VNFC based on this Vdu.Compute that is permitted to exist for a particular VNF deployment flavour. + required: true + constraints: + - greater_or_equal: 0 + + tosca.datatypes.nfv.VlProfile: + derived_from: tosca.datatypes.Root + description: Describes additional instantiation data for a given VL used in a specific VNF deployment flavour. + properties: + max_bitrate_requirements: + type: tosca.datatypes.nfv.LinkBitrateRequirements + description: Specifies the maximum bitrate requirements for a VL instantiated according to this profile. + required: true + min_bitrate_requirements: + type: tosca.datatypes.nfv.LinkBitrateRequirements + description: Specifies the minimum bitrate requirements for a VL instantiated according to this profile. + required: true + qos: + type: tosca.datatypes.nfv.Qos + description: Specifies the QoS requirements of a VL instantiated according to this profile. + required: false + virtual_link_protocol_data: + type: list + description: Specifies the protocol data for a virtual link. + required: false + entry_schema: + type: tosca.datatypes.nfv.VirtualLinkProtocolData + + tosca.datatypes.nfv.VirtualLinkProtocolData: + derived_from: tosca.datatypes.Root + description: describes one protocol layer and associated protocol data for a given virtual link used in a specific VNF deployment flavour + properties: + associated_layer_protocol: + type: string + description: Identifies one of the protocols a virtualLink gives access to (ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire) as specified by the connectivity_type property. + required: true + constraints: + - valid_values: [ ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire ] + l2_protocol_data: + type: tosca.datatypes.nfv.L2ProtocolData + description: Specifies the L2 protocol data for a virtual link. Shall be present when the associatedLayerProtocol attribute indicates a L2 protocol and shall be absent otherwise. + required: false + l3_protocol_data: + type: tosca.datatypes.nfv.L3ProtocolData + description: Specifies the L3 protocol data for this virtual link. Shall be present when the associatedLayerProtocol attribute indicates a L3 protocol and shall be absent otherwise. + required: false + + tosca.datatypes.nfv.L2ProtocolData: + derived_from: tosca.datatypes.Root + description: describes L2 protocol data for a given virtual link used in a specific VNF deployment flavour. + properties: + name: + type: string + description: Identifies the network name associated with this L2 protocol. + required: false + network_type: + type: string + description: Specifies the network type for this L2 protocol.The value may be overridden at run-time. + required: false + constraints: + - valid_values: [ flat, vlan, vxlan, gre ] + vlan_transparent: + type: boolean + description: Specifies whether to support VLAN transparency for this L2 protocol or not. + required: false + default: false + mtu: + type: integer + description: Specifies the maximum transmission unit (MTU) value for this L2 protocol. + required: false + constraints: + - greater_than: 0 + + tosca.datatypes.nfv.L3ProtocolData: + derived_from: tosca.datatypes.Root + description: describes L3 protocol data for a given virtual link used in a specific VNF deployment flavour. + properties: + name: + type: string + description: Identifies the network name associated with this L3 protocol. + required: false + ip_version: + type: string + description: Specifies IP version of this L3 protocol.The value of the ip_version property shall be consistent with the value of the layer_protocol in the connectivity_type property of the virtual link node. + required: true + constraints: + - valid_values: [ ipv4, ipv6 ] + cidr: + type: string + description: Specifies the CIDR (Classless Inter-Domain Routing) of this L3 protocol. The value may be overridden at run-time. + required: true + ip_allocation_pools: + type: list + description: Specifies the allocation pools with start and end IP addresses for this L3 protocol. The value may be overridden at run-time. + required: false + entry_schema: + type: tosca.datatypes.nfv.IpAllocationPool + gateway_ip: + type: string + description: Specifies the gateway IP address for this L3 protocol. The value may be overridden at run-time. + required: false + dhcp_enabled: + type: boolean + description: Indicates whether DHCP (Dynamic Host Configuration Protocol) is enabled or disabled for this L3 protocol. The value may be overridden at run-time. + required: false + ipv6_address_mode: + type: string + description: Specifies IPv6 address mode. May be present when the value of the ipVersion attribute is "ipv6" and shall be absent otherwise. The value may be overridden at run-time. + required: false + constraints: + - valid_values: [ slaac, dhcpv6-stateful, dhcpv6-stateless ] + + tosca.datatypes.nfv.IpAllocationPool: + derived_from: tosca.datatypes.Root + description: Specifies a range of IP addresses + properties: + start_ip_address: + type: string + description: The IP address to be used as the first one in a pool of addresses derived from the cidr block full IP range + required: true + end_ip_address: + type: string + description: The IP address to be used as the last one in a pool of addresses derived from the cidr block full IP range + required: true + + tosca.datatypes.nfv.InstantiationLevel: + derived_from: tosca.datatypes.Root + description: Describes the scale level for each aspect that corresponds to a given level of resources to be instantiated within a deployment flavour in term of the number VNFC instances + properties: + description: + type: string + description: Human readable description of the level + required: true + scale_info: + type: map # key: aspectId + description: Represents for each aspect the scale level that corresponds to this instantiation level. scale_info shall be present if the VNF supports scaling. + required: false + entry_schema: + type: tosca.datatypes.nfv.ScaleInfo + + tosca.datatypes.nfv.VduLevel: + derived_from: tosca.datatypes.Root + description: Indicates for a given Vdu.Compute in a given level the number of instances to deploy + properties: + number_of_instances: + type: integer + description: Number of instances of VNFC based on this VDU to deploy for this level. + required: true + constraints: + - greater_or_equal: 0 + + tosca.datatypes.nfv.VnfLcmOperationsConfiguration: + derived_from: tosca.datatypes.Root + description: Represents information to configure lifecycle management operations + properties: + instantiate: + type: tosca.datatypes.nfv.VnfInstantiateOperationConfiguration + description: Configuration parameters for the InstantiateVnf operation + required: false + scale: + type: tosca.datatypes.nfv.VnfScaleOperationConfiguration + description: Configuration parameters for the ScaleVnf operation + required: false + scale_to_level: + type: tosca.datatypes.nfv.VnfScaleToLevelOperationConfiguration + description: Configuration parameters for the ScaleVnfToLevel operation + required: false + change_flavour: + type: tosca.datatypes.nfv.VnfChangeFlavourOperationConfiguration + description: Configuration parameters for the changeVnfFlavourOpConfig operation + required: false + heal: + type: tosca.datatypes.nfv.VnfHealOperationConfiguration + description: Configuration parameters for the HealVnf operation + required: false + terminate: + type: tosca.datatypes.nfv.VnfTerminateOperationConfiguration + description: Configuration parameters for the TerminateVnf operation + required: false + operate: + type: tosca.datatypes.nfv.VnfOperateOperationConfiguration + description: Configuration parameters for the OperateVnf operation + required: false + change_ext_connectivity: + type: tosca.datatypes.nfv.VnfChangeExtConnectivityOperationConfiguration + description: Configuration parameters for the changeExtVnfConnectivityOpConfig operation + required: false + + tosca.datatypes.nfv.VnfInstantiateOperationConfiguration: + derived_from: tosca.datatypes.Root + description: represents information that affect the invocation of the InstantiateVnf operation. + + tosca.datatypes.nfv.VnfScaleOperationConfiguration: + derived_from: tosca.datatypes.Root + description: Represents information that affect the invocation of the ScaleVnf operation + properties: + scaling_by_more_than_one_step_supported: + type: boolean + description: Signals whether passing a value larger than one in the numScalingSteps parameter of the ScaleVnf operation is supported by this VNF. + required: false + default: false + + tosca.datatypes.nfv.VnfScaleToLevelOperationConfiguration: + derived_from: tosca.datatypes.Root + description: represents information that affect the invocation of the ScaleVnfToLevel operation + properties: + arbitrary_target_levels_supported: + type: boolean + description: Signals whether scaling according to the parameter "scaleInfo" is supported by this VNF + required: true + + tosca.datatypes.nfv.VnfHealOperationConfiguration: + derived_from: tosca.datatypes.Root + description: represents information that affect the invocation of the HealVnf operation + properties: + causes: + type: list + description: Supported "cause" parameter values + required: false + entry_schema: + type: string + + tosca.datatypes.nfv.VnfTerminateOperationConfiguration: + derived_from: tosca.datatypes.Root + description: represents information that affect the invocation of the TerminateVnf + properties: + min_graceful_termination_timeout: + type: scalar-unit.time + description: Minimum timeout value for graceful termination of a VNF instance + required: true + max_recommended_graceful_termination_timeout: + type: scalar-unit.time + description: Maximum recommended timeout value that can be needed to gracefully terminate a VNF instance of a particular type under certain conditions, such as maximum load condition. This is provided by VNF provider as information for the operator facilitating the selection of optimal timeout value. This value is not used as constraint + required: false + + tosca.datatypes.nfv.VnfOperateOperationConfiguration: + derived_from: tosca.datatypes.Root + description: represents information that affect the invocation of the OperateVnf operation + properties: + min_graceful_stop_timeout: + type: scalar-unit.time + description: Minimum timeout value for graceful stop of a VNF instance + required: true + max_recommended_graceful_stop_timeout: + type: scalar-unit.time + description: Maximum recommended timeout value that can be needed to gracefully stop a VNF instance of a particular type under certain conditions, such as maximum load condition. This is provided by VNF provider as information for the operator facilitating the selection of optimal timeout value. This value is not used as constraint + required: false + + tosca.datatypes.nfv.ScaleInfo: + derived_from: tosca.datatypes.Root + description: Indicates for a given scaleAspect the corresponding scaleLevel + properties: + scale_level: + type: integer + description: The scale level for a particular aspect + required: true + constraints: + - greater_or_equal: 0 + + tosca.datatypes.nfv.ScalingAspect: + derived_from: tosca.datatypes.Root + properties: + name: + type: string + required: true + description: + type: string + required: true + max_scale_level: + type: integer # positiveInteger + required: true + constraints: + - greater_or_equal: 0 + step_deltas: + type: list + required: false + entry_schema: + type: string # Identifier + + tosca.datatypes.nfv.VnfConfigurableProperties: + derived_from: tosca.datatypes.Root + description: indicates configuration properties for a given VNF (e.g. related to auto scaling and auto healing). + properties: + is_autoscale_enabled: + type: boolean + description: It permits to enable (TRUE)/disable (FALSE) the auto-scaling functionality. If the properties is not present for configuring, then VNF property is not supported + required: false + is_autoheal_enabled: + type: boolean + description: It permits to enable (TRUE)/disable (FALSE) the auto-healing functionality. If the properties is not present for configuring, then VNF property is not supported + required: false + + tosca.datatypes.nfv.VnfAdditionalConfigurableProperties: + derived_from: tosca.datatypes.Root + description: is an empty base type for deriving data types for describing additional configurable properties for a given VNF + + tosca.datatypes.nfv.VnfInfoModifiableAttributes: + derived_from: tosca.datatypes.Root + description: Describes VNF-specific extension and metadata for a given VNF + + tosca.datatypes.nfv.VnfInfoModifiableAttributesExtensions: + derived_from: tosca.datatypes.Root + description: is an empty base type for deriving data types for describing VNF-specific extension + + tosca.datatypes.nfv.VnfInfoModifiableAttributesMetadata: + derived_from: tosca.datatypes.Root + description: is an empty base type for deriving data types for describing VNF-specific metadata + + tosca.datatypes.nfv.LogicalNodeData: + derived_from: tosca.datatypes.Root + description: Describes compute, memory and I/O requirements associated with a particular VDU. + properties: + logical_node_requirements: + type: map + description: The logical node-level compute, memory and I/O requirements. A map of strings that contains a set of key-value pairs that describes hardware platform specific deployment requirements, including the number of CPU cores on this logical node, a memory configuration specific to a logical node or a requirement related to the association of an I/O device with the logical node. + required: false + entry_schema: + type: string + + tosca.datatypes.nfv.SwImageData: + derived_from: tosca.datatypes.Root + description: describes information related to a software image artifact + properties: # in SOL001 v0.8.0: "properties or metadata:" + name: + type: string + description: Name of this software image + required: true + version: + type: string + description: Version of this software image + required: true + checksum: + type: tosca.datatypes.nfv.ChecksumData + description: Checksum of the software image file + required: true + container_format: + type: string + description: The container format describes the container file format in which software image is provided + required: true + constraints: + - valid_values: [ aki, ami, ari, bare, docker, ova, ovf ] + disk_format: + type: string + description: The disk format of a software image is the format of the underlying disk image + required: true + constraints: + - valid_values: [ aki, ami, ari, iso, qcow2, raw, vdi, vhd, vhdx, vmdk ] + min_disk: + type: scalar-unit.size # Number + description: The minimal disk size requirement for this software image + required: true + constraints: + - greater_or_equal: 0 B + min_ram: + type: scalar-unit.size # Number + description: The minimal RAM requirement for this software image + required: false + constraints: + - greater_or_equal: 0 B + size: + type: scalar-unit.size # Number + description: The size of this software image + required: true + operating_system: + type: string + description: Identifies the operating system used in the software image + required: false + supported_virtualisation_environments: + type: list + description: Identifies the virtualisation environments (e.g. hypervisor) compatible with this software image + required: false + entry_schema: + type: string + + tosca.datatypes.nfv.VirtualBlockStorageData: + derived_from: tosca.datatypes.Root + description: VirtualBlockStorageData describes block storage requirements associated with compute resources in a particular VDU, either as a local disk or as virtual attached storage + properties: + size_of_storage: + type: scalar-unit.size + description: Size of virtualised storage resource + required: true + constraints: + - greater_or_equal: 0 B + vdu_storage_requirements: + type: map + description: The hardware platform specific storage requirements. A map of strings that contains a set of key-value pairs that represents the hardware platform specific storage deployment requirements. + required: false + entry_schema: + type: string + rdma_enabled: + type: boolean + description: Indicates if the storage support RDMA + required: false + default: false + + tosca.datatypes.nfv.VirtualObjectStorageData: + derived_from: tosca.datatypes.Root + description: VirtualObjectStorageData describes object storage requirements associated with compute resources in a particular VDU + properties: + max_size_of_storage: + type: scalar-unit.size + description: Maximum size of virtualized storage resource + required: false + constraints: + - greater_or_equal: 0 B + + tosca.datatypes.nfv.VirtualFileStorageData: + derived_from: tosca.datatypes.Root + description: VirtualFileStorageData describes file storage requirements associated with compute resources in a particular VDU + properties: + size_of_storage: + type: scalar-unit.size + description: Size of virtualized storage resource + required: true + constraints: + - greater_or_equal: 0 B + file_system_protocol: + type: string + description: The shared file system protocol (e.g. NFS, CIFS) + required: true + + tosca.datatypes.nfv.VirtualLinkBitrateLevel: + derived_from: tosca.datatypes.Root + description: Describes bitrate requirements applicable to the virtual link instantiated from a particicular VnfVirtualLink + properties: + bitrate_requirements: + type: tosca.datatypes.nfv.LinkBitrateRequirements + description: Virtual link bitrate requirements for an instantiation level or bitrate delta for a scaling step + required: true + + tosca.datatypes.nfv.VnfOperationAdditionalParameters: + derived_from: tosca.datatypes.Root + description: Is an empty base type for deriving data type for describing VNF-specific parameters to be passed when invoking lifecycle management operations + #properties: + + tosca.datatypes.nfv.VnfChangeFlavourOperationConfiguration: + derived_from: tosca.datatypes.Root + description: represents information that affect the invocation of the ChangeVnfFlavour operation + #properties: + + tosca.datatypes.nfv.VnfChangeExtConnectivityOperationConfiguration: + derived_from: tosca.datatypes.Root + description: represents information that affect the invocation of the ChangeExtVnfConnectivity operation + #properties: + + tosca.datatypes.nfv.VnfMonitoringParameter: + derived_from: tosca.datatypes.Root + description: Represents information on virtualised resource related performance metrics applicable to the VNF. + properties: + name: + type: string + description: Human readable name of the monitoring parameter + required: true + performance_metric: + type: string + description: Identifies the performance metric, according to ETSI GS NFV-IFA 027. + required: true + constraints: + - valid_values: [ v_cpu_usage_mean_vnf, v_cpu_usage_peak_vnf, v_memory_usage_mean_vnf, v_memory_usage_peak_vnf, v_disk_usage_mean_vnf, v_disk_usage_peak_vnf, byte_incoming_vnf_ext_cp, byte_outgoing_vnf_ext_cp, +packet_incoming_vnf_ext_cp, packet_outgoing_vnf_ext_cp ] + collection_period: + type: scalar-unit.time + description: Describes the periodicity at which to collect the performance information. + required: false + constraints: + - greater_than: 0 s + + tosca.datatypes.nfv.VnfcMonitoringParameter: + derived_from: tosca.datatypes.Root + description: Represents information on virtualised resource related performance metrics applicable to the VNF. + properties: + name: + type: string + description: Human readable name of the monitoring parameter + required: true + performance_metric: + type: string + description: Identifies the performance metric, according to ETSI GS NFV-IFA 027. + required: true + constraints: + - valid_values: [ v_cpu_usage_mean_vnf, v_cpu_usage_peak_vnf, v_memory_usage_mean_vnf, v_memory_usage_peak_vnf, v_disk_usage_mean_vnf, v_disk_usage_peak_vnf, byte_incoming_vnf_int_cp, byte_outgoing_vnf_int_cp, packet_incoming_vnf_int_cp, packet_outgoing_vnf_int_cp ] + collection_period: + type: scalar-unit.time + description: Describes the periodicity at which to collect the performance information. + required: false + constraints: + - greater_than: 0 s + + tosca.datatypes.nfv.VirtualLinkMonitoringParameter: + derived_from: tosca.datatypes.Root + description: Represents information on virtualised resource related performance metrics applicable to the VNF. + properties: + name: + type: string + description: Human readable name of the monitoring parameter + required: true + performance_metric: + type: string + description: Identifies a performance metric derived from those defined in ETSI GS NFV-IFA 027.The packetOutgoingVirtualLink and packetIncomingVirtualLink metrics shall be obtained by aggregation the PacketOutgoing and PacketIncoming measurements defined in clause 7.1 of GS NFV-IFA 027 of all virtual link ports attached to the virtual link to which the metrics apply. + required: true + constraints: + - valid_values: [ packet_outgoing_virtual_link, packet_incoming_virtual_link ] + collection_period: + type: scalar-unit.time + description: Describes the periodicity at which to collect the performance information. + required: false + constraints: + - greater_than: 0 s + + tosca.datatypes.nfv.InterfaceDetails: + derived_from: tosca.datatypes.Root + description: information used to access an interface exposed by a VNF + properties: + uri_components: + type: tosca.datatypes.nfv.UriComponents + description: Provides components to build a Uniform Ressource Identifier (URI) where to access the interface end point. + required: false + interface_specific_data: + type: map + description: Provides additional details that are specific to the type of interface considered. + required: false + entry_schema: + type: string + + tosca.datatypes.nfv.UriComponents: + derived_from: tosca.datatypes.Root + description: information used to build a URI that complies with IETF RFC 3986 [8]. + properties: + scheme: + type: string # shall comply with IETF RFC3986 + description: scheme component of a URI. + required: true + authority: + type: tosca.datatypes.nfv.UriAuthority + description: Authority component of a URI + required: false + path: + type: string # shall comply with IETF RFC 3986 + description: path component of a URI. + required: false + query: + type: string # shall comply with IETF RFC 3986 + description: query component of a URI. + required: false + fragment: + type: string # shall comply with IETF RFC 3986 + description: fragment component of a URI. + required: false + + tosca.datatypes.nfv.UriAuthority: + derived_from: tosca.datatypes.Root + description: information that corresponds to the authority component of a URI as specified in IETF RFC 3986 [8] + properties: + user_info: + type: string # shall comply with IETF RFC 3986 + description: user_info field of the authority component of a URI + required: false + host: + type: string # shall comply with IETF RFC 3986 + description: host field of the authority component of a URI + required: false + port: + type: string # shall comply with IETF RFC 3986 + description: port field of the authority component of a URI + required: false + + tosca.datatypes.nfv.ChecksumData: + derived_from: tosca.datatypes.Root + description: Describes information about the result of performing a checksum operation over some arbitrary data + properties: + algorithm: + type: string + description: Describes the algorithm used to obtain the checksum value + required: true + constraints: + - valid_values: [sha-224, sha-256, sha-384, sha-512 ] + hash: + type: string + description: Contains the result of applying the algorithm indicated by the algorithm property to the data to which this ChecksumData refers + required: true + +artifact_types: + tosca.artifacts.nfv.SwImage: + derived_from: tosca.artifacts.Deployment.Image + description: describes the software image which is directly loaded on the virtualisation container realizing of the VDU or is to be loaded on a virtual storage resource. + + tosca.artifacts.Implementation.nfv.Mistral: + derived_from: tosca.artifacts.Implementation + description: artifacts for Mistral workflows + mime_type: application/x-yaml + file_ext: [ yaml ] + +capability_types: + tosca.capabilities.nfv.VirtualBindable: + derived_from: tosca.capabilities.Node + description: Indicates that the node that includes it can be pointed by a tosca.relationships.nfv.VirtualBindsTo relationship type which is used to model the VduHasCpd association + + tosca.capabilities.nfv.VirtualCompute: + derived_from: tosca.capabilities.Node + description: Describes the capabilities related to virtual compute resources + properties: + logical_node: + type: map + description: Describes the Logical Node requirements + required: false + entry_schema: + type: tosca.datatypes.nfv.LogicalNodeData + requested_additional_capabilities: + type: map + description: Describes additional capability for a particular VDU + required: false + entry_schema: + type: tosca.datatypes.nfv.RequestedAdditionalCapability + compute_requirements: + type: map + required: false + entry_schema: + type: string + virtual_memory: + type: tosca.datatypes.nfv.VirtualMemory + description: Describes virtual memory of the virtualized compute + required: true + virtual_cpu: + type: tosca.datatypes.nfv.VirtualCpu + description: Describes virtual CPU(s) of the virtualized compute + required: true + virtual_local_storage: + type: list + description: A list of virtual system disks created and destroyed as part of the VM lifecycle + required: false + entry_schema: + type: tosca.datatypes.nfv.VirtualBlockStorageData + description: virtual system disk definition + + tosca.capabilities.nfv.VirtualStorage: + derived_from: tosca.capabilities.Root + description: Describes the attachment capabilities related to Vdu.Storage + +relationship_types: + tosca.relationships.nfv.VirtualBindsTo: + derived_from: tosca.relationships.DependsOn + description: Represents an association relationship between Vdu.Compute and VduCp node types + valid_target_types: [ tosca.capabilities.nfv.VirtualBindable ] + + tosca.relationships.nfv.AttachesTo: + derived_from: tosca.relationships.Root + description: Represents an association relationship between the Vdu.Compute and one of the node types, Vdu.VirtualBlockStorage, Vdu.VirtualObjectStorage or Vdu.VirtualFileStorage + valid_target_types: [ tosca.capabilities.nfv.VirtualStorage ] + +interface_types: + tosca.interfaces.nfv.Vnflcm: + derived_from: tosca.interfaces.Root + description: This interface encompasses a set of TOSCA operations corresponding to the VNF LCM operations defined in ETSI GS NFV-IFA 007 as well as to preamble and postamble procedures to the execution of the VNF LCM operations. + instantiate: + description: Invoked upon receipt of an Instantiate VNF request + instantiate_start: + description: Invoked before instantiate + instantiate_end: + description: Invoked after instantiate + terminate: + description: Invoked upon receipt Terminate VNF request + terminate_start: + description: Invoked before terminate + terminate_end: + description: Invoked after terminate + modify_information: + description: Invoked upon receipt of a Modify VNF Information request + modify_information_start: + description: Invoked before modify_information + modify_information_end: + description: Invoked after modify_information + change_flavour: + description: Invoked upon receipt of a Change VNF Flavour request + change_flavour_start: + description: Invoked before change_flavour + change_flavour_end: + description: Invoked after change_flavour + change_external_connectivity: + description: Invoked upon receipt of a Change External VNF Connectivity request + change_external_connectivity_start: + description: Invoked before change_external_connectivity + change_external_connectivity_end: + description: Invoked after change_external_connectivity + operate: + description: Invoked upon receipt of an Operate VNF request + operate_start: + description: Invoked before operate + operate_end: + description: Invoked after operate + heal: + description: Invoked upon receipt of a Heal VNF request + heal_start: + description: Invoked before heal + heal_end: + description: Invoked after heal + scale: + description: Invoked upon receipt of a Scale VNF request + scale_start: + description: Invoked before scale + scale_end: + description: Invoked after scale + scale_to_level: + description: Invoked upon receipt of a Scale VNF to Level request + scale_to_level_start: + description: Invoked before scale_to_level + scale_to_level_end: + description: Invoked after scale_to_level + +node_types: + tosca.nodes.nfv.VNF: + derived_from: tosca.nodes.Root + description: The generic abstract type from which all VNF specific abstract node types shall be derived to form, together with other node types, the TOSCA service template(s) representing the VNFD + properties: + descriptor_id: # instead of vnfd_id + type: string # GUID + description: Globally unique identifier of the VNFD + required: true + descriptor_version: # instead of vnfd_version + type: string + description: Identifies the version of the VNFD + required: true + provider: # instead of vnf_provider + type: string + description: Provider of the VNF and of the VNFD + required: true + product_name: # instead of vnf_product_name + type: string + description: Human readable name for the VNF Product + required: true + software_version: # instead of vnf_software_version + type: string + description: Software version of the VNF + required: true + product_info_name: # instead of vnf_product_info_name + type: string + description: Human readable name for the VNF Product + required: false + product_info_description: # instead of vnf_product_info_description + type: string + description: Human readable description of the VNF Product + required: false + vnfm_info: + type: list + required: true + description: Identifies VNFM(s) compatible with the VNF + entry_schema: + type: string + constraints: + - pattern: (^etsivnfm:v[0-9]?[0-9]\.[0-9]?[0-9]\.[0-9]?[0-9]$)|(^[0-9]+:[a-zA-Z0-9.-]+$) + localization_languages: + type: list + description: Information about localization languages of the VNF + required: false + entry_schema: + type: string #IETF RFC 5646 string + default_localization_language: + type: string #IETF RFC 5646 string + description: Default localization language that is instantiated if no information about selected localization language is available + required: false + lcm_operations_configuration: + type: tosca.datatypes.nfv.VnfLcmOperationsConfiguration + description: Describes the configuration parameters for the VNF LCM operations + required: false + monitoring_parameters: + type: list + entry_schema: + type: tosca.datatypes.nfv.VnfMonitoringParameter + description: Describes monitoring parameters applicable to the VNF. + required: false + flavour_id: + type: string + description: Identifier of the Deployment Flavour within the VNFD + required: true + flavour_description: + type: string + description: Human readable description of the DF + required: true + vnf_profile: + type: tosca.datatypes.nfv.VnfProfile + description: Describes a profile for instantiating VNFs of a particular NS DF according to a specific VNFD and VNF DF + required: false + requirements: + - virtual_link: + capability: tosca.capabilities.nfv.VirtualLinkable + relationship: tosca.relationships.nfv.VirtualLinksTo + occurrences: [ 0, 1 ] + # Additional requirements shall be defined in the VNF specific node type (deriving from tosca.nodes.nfv.VNF) corresponding to NS virtual links that need to connect to VnfExtCps + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm + + tosca.nodes.nfv.VnfExtCp: + derived_from: tosca.nodes.nfv.Cp + description: Describes a logical external connection point, exposed by the VNF enabling connection with an external Virtual Link + properties: + virtual_network_interface_requirements: + type: list + description: The actual virtual NIC requirements that is been assigned when instantiating the connection point + required: false + entry_schema: + type: tosca.datatypes.nfv.VirtualNetworkInterfaceRequirements + requirements: + - external_virtual_link: + capability: tosca.capabilities.nfv.VirtualLinkable + relationship: tosca.relationships.nfv.VirtualLinksTo + - internal_virtual_link: #name in ETSI NFV IFA011 v0.7.3: intVirtualLinkDesc + capability: tosca.capabilities.nfv.VirtualLinkable + relationship: tosca.relationships.nfv.VirtualLinksTo + + tosca.nodes.nfv.Vdu.Compute: + derived_from: tosca.nodes.Root + description: Describes the virtual compute part of a VDU which is a construct supporting the description of the deployment and operational behavior of a VNFC + properties: + name: + type: string + description: Human readable name of the VDU + required: true + description: + type: string + description: Human readable description of the VDU + required: true + boot_order: + type: list # explicit index (boot index) not necessary, contrary to IFA011 + description: References a node template name from which a valid boot device is created + required: false + entry_schema: + type: string + nfvi_constraints: + type: list + description: Describes constraints on the NFVI for the VNFC instance(s) created from this VDU + required: false + entry_schema: + type: string + monitoring_parameters: + type: list + description: Describes monitoring parameters applicable to a VNFC instantiated from this VDU + required: false + entry_schema: + type: tosca.datatypes.nfv.VnfcMonitoringParameter + vdu_profile: + type: tosca.datatypes.nfv.VduProfile + description: Defines additional instantiation data for the VDU.Compute node + required: true + sw_image_data: + type: tosca.datatypes.nfv.SwImageData + description: Defines information related to a SwImage artifact used by this Vdu.Compute node + required: false # property is required when the node template has an associated artifact of type tosca.artifacts.nfv.SwImage and not required otherwise + boot_data: + type: string + description: Contains a string or a URL to a file contained in the VNF package used to customize a virtualised compute resource at boot time. The bootData may contain variable parts that are replaced by deployment specific values before being sent to the VIM. + required: false + capabilities: + virtual_compute: + type: tosca.capabilities.nfv.VirtualCompute + occurrences: [ 1, 1 ] + virtual_binding: + type: tosca.capabilities.nfv.VirtualBindable + occurrences: [ 1, UNBOUNDED ] + requirements: + - virtual_storage: + capability: tosca.capabilities.nfv.VirtualStorage + relationship: tosca.relationships.nfv.AttachesTo + occurrences: [ 0, UNBOUNDED ] + + tosca.nodes.nfv.Vdu.VirtualBlockStorage: + derived_from: tosca.nodes.Root + description: This node type describes the specifications of requirements related to virtual block storage resources + properties: + virtual_block_storage_data: + type: tosca.datatypes.nfv.VirtualBlockStorageData + description: Describes the block storage characteristics. + required: true + sw_image_data: + type: tosca.datatypes.nfv.SwImageData + description: Defines information related to a SwImage artifact used by this Vdu.Compute node. + required: false # property is required when the node template has an associated artifact of type tosca.artifacts.nfv.SwImage and not required otherwise + capabilities: + virtual_storage: + type: tosca.capabilities.nfv.VirtualStorage + description: Defines the capabilities of virtual_storage. + + tosca.nodes.nfv.Vdu.VirtualObjectStorage: + derived_from: tosca.nodes.Root + description: This node type describes the specifications of requirements related to virtual object storage resources + properties: + virtual_object_storage_data: + type: tosca.datatypes.nfv.VirtualObjectStorageData + description: Describes the object storage characteristics. + required: true + capabilities: + virtual_storage: + type: tosca.capabilities.nfv.VirtualStorage + description: Defines the capabilities of virtual_storage. + + tosca.nodes.nfv.Vdu.VirtualFileStorage: + derived_from: tosca.nodes.Root + description: This node type describes the specifications of requirements related to virtual file storage resources + properties: + virtual_file_storage_data: + type: tosca.datatypes.nfv.VirtualFileStorageData + description: Describes the file storage characteristics. + required: true + capabilities: + virtual_storage: + type: tosca.capabilities.nfv.VirtualStorage + description: Defines the capabilities of virtual_storage. + requirements: + - virtual_link: + capability: tosca.capabilities.nfv.VirtualLinkable + relationship: tosca.relationships.nfv.VirtualLinksTo + #description: Describes the requirements for linking to virtual link + + tosca.nodes.nfv.VduCp: + derived_from: tosca.nodes.nfv.Cp + description: describes network connectivity between a VNFC instance based on this VDU and an internal VL + properties: + bitrate_requirement: + type: integer # in bits per second + description: Bitrate requirement in bit per second on this connection point + required: false + constraints: + - greater_or_equal: 0 + virtual_network_interface_requirements: + type: list + description: Specifies requirements on a virtual network interface realising the CPs instantiated from this CPD + required: false + entry_schema: + type: tosca.datatypes.nfv.VirtualNetworkInterfaceRequirements + order: + type: integer + description: The order of the NIC on the compute instance (e.g.eth2) + required: false + constraints: + - greater_or_equal: 0 + vnic_type: + type: string + description: Describes the type of the virtual network interface realizing the CPs instantiated from this CPD + required: false + constraints: + - valid_values: [ normal, virtio, direct-physical ] + requirements: + - virtual_link: + capability: tosca.capabilities.nfv.VirtualLinkable + relationship: tosca.relationships.nfv.VirtualLinksTo + - virtual_binding: + capability: tosca.capabilities.nfv.VirtualBindable + relationship: tosca.relationships.nfv.VirtualBindsTo + node: tosca.nodes.nfv.Vdu.Compute + + tosca.nodes.nfv.VnfVirtualLink: + derived_from: tosca.nodes.Root + description: Describes the information about an internal VNF VL + properties: + connectivity_type: + type: tosca.datatypes.nfv.ConnectivityType + description: Specifies the protocol exposed by the VL and the flow pattern supported by the VL + required: true + description: + type: string + description: Provides human-readable information on the purpose of the VL + required: false + test_access: + type: list + description: Test access facilities available on the VL + required: false + entry_schema: + type: string + constraints: + - valid_values: [ passive_monitoring, active_loopback ] + vl_profile: + type: tosca.datatypes.nfv.VlProfile + description: Defines additional data for the VL + required: true + monitoring_parameters: + type: list + description: Describes monitoring parameters applicable to the VL + required: false + entry_schema: + type: tosca.datatypes.nfv.VirtualLinkMonitoringParameter + capabilities: + virtual_linkable: + type: tosca.capabilities.nfv.VirtualLinkable + +group_types: + tosca.groups.nfv.PlacementGroup: + derived_from: tosca.groups.Root + description: PlacementGroup is used for describing the affinity or anti-affinity relationship applicable between the virtualization containers to be created based on different VDUs, or between internal VLs to be created based on different VnfVirtualLinkDesc(s) + properties: + description: + type: string + description: Human readable description of the group + required: true + members: [ tosca.nodes.nfv.Vdu.Compute, tosca.nodes.nfv.VnfVirtualLink ] + +policy_types: + tosca.policies.nfv.InstantiationLevels: + derived_from: tosca.policies.Root + description: The InstantiationLevels type is a policy type representing all the instantiation levels of resources to be instantiated within a deployment flavour and including default instantiation level in term of the number of VNFC instances to be created as defined in ETSI GS NFV-IFA 011 [1]. + properties: + levels: + type: map # key: levelId + description: Describes the various levels of resources that can be used to instantiate the VNF using this flavour. + required: true + entry_schema: + type: tosca.datatypes.nfv.InstantiationLevel + constraints: + - min_length: 1 + default_level: + type: string # levelId + description: The default instantiation level for this flavour. + required: false # required if multiple entries in levels + + tosca.policies.nfv.VduInstantiationLevels: + derived_from: tosca.policies.Root + description: The VduInstantiationLevels type is a policy type representing all the instantiation levels of resources to be instantiated within a deployment flavour in term of the number of VNFC instances to be created from each vdu.Compute. as defined in ETSI GS NFV-IFA 011 [1] + properties: + levels: + type: map # key: levelId + description: Describes the Vdu.Compute levels of resources that can be used to instantiate the VNF using this flavour + required: true + entry_schema: + type: tosca.datatypes.nfv.VduLevel + constraints: + - min_length: 1 + targets: [ tosca.nodes.nfv.Vdu.Compute ] + + tosca.policies.nfv.VirtualLinkInstantiationLevels: + derived_from: tosca.policies.Root + description: The VirtualLinkInstantiationLevels type is a policy type representing all the instantiation levels of virtual link resources to be instantiated within a deployment flavour as defined in ETSI GS NFV-IFA 011 [1]. + properties: + levels: + type: map # key: levelId + description: Describes the virtual link levels of resources that can be used to instantiate the VNF using this flavour. + required: true + entry_schema: + type: tosca.datatypes.nfv.VirtualLinkBitrateLevel + constraints: + - min_length: 1 + targets: [ tosca.nodes.nfv.VnfVirtualLink ] + + tosca.policies.nfv.ScalingAspects: + derived_from: tosca.policies.Root + description: The ScalingAspects type is a policy type representing the scaling aspects used for horizontal scaling as defined in ETSI GS NFV-IFA 011 [1]. + properties: + aspects: + type: map # key: aspectId + description: Describe maximum scale level for total number of scaling steps that can be applied to a particular aspect + required: true + entry_schema: + type: tosca.datatypes.nfv.ScalingAspect + constraints: + - min_length: 1 + + tosca.policies.nfv.VduScalingAspectDeltas: + derived_from: tosca.policies.Root + description: The VduScalingAspectDeltas type is a policy type representing the Vdu.Compute detail of an aspect deltas used for horizontal scaling, as defined in ETSI GS NFV-IFA 011 [1]. + properties: + aspect: + type: string + description: Represents the scaling aspect to which this policy applies + required: true + deltas: + type: map # key: scalingDeltaId + description: Describes the Vdu.Compute scaling deltas to be applied for every scaling steps of a particular aspect. + required: true + entry_schema: + type: tosca.datatypes.nfv.VduLevel + constraints: + - min_length: 1 + targets: [ tosca.nodes.nfv.Vdu.Compute ] + + tosca.policies.nfv.VirtualLinkBitrateScalingAspectDeltas: + derived_from: tosca.policies.Root + description: The VirtualLinkBitrateScalingAspectDeltas type is a policy type representing the VnfVirtualLink detail of an aspect deltas used for horizontal scaling, as defined in ETSI GS NFV-IFA 011 [1]. + properties: + aspect: + type: string + description: Represents the scaling aspect to which this policy applies. + required: true + deltas: + type: map # key: scalingDeltaId + description: Describes the VnfVirtualLink scaling deltas to be applied for every scaling steps of a particular aspect. + required: true + entry_schema: + type: tosca.datatypes.nfv.VirtualLinkBitrateLevel + constraints: + - min_length: 1 + targets: [ tosca.nodes.nfv.VnfVirtualLink ] + + tosca.policies.nfv.VduInitialDelta: + derived_from: tosca.policies.Root + description: The VduInitialDelta type is a policy type representing the Vdu.Compute detail of an initial delta used for horizontal scaling, as defined in ETSI GS NFV-IFA 011 [1]. + properties: + initial_delta: + type: tosca.datatypes.nfv.VduLevel + description: Represents the initial minimum size of the VNF. + required: true + targets: [ tosca.nodes.nfv.Vdu.Compute ] + + tosca.policies.nfv.VirtualLinkBitrateInitialDelta: + derived_from: tosca.policies.Root + description: The VirtualLinkBitrateInitialDelta type is a policy type representing the VnfVirtualLink detail of an initial deltas used for horizontal scaling, as defined in ETSI GS NFV-IFA 011 [1]. + properties: + initial_delta: + type: tosca.datatypes.nfv.VirtualLinkBitrateLevel + description: Represents the initial minimum size of the VNF. + required: true + targets: [ tosca.nodes.nfv.VnfVirtualLink ] + + tosca.policies.nfv.AffinityRule: + derived_from: tosca.policies.Placement + description: The AffinityRule describes the affinity rules applicable for the defined targets + properties: + scope: + type: string + description: scope of the rule is an NFVI_node, an NFVI_PoP, etc. + required: true + constraints: + - valid_values: [ nfvi_node, zone, zone_group, nfvi_pop ] + targets: [ tosca.nodes.nfv.Vdu.Compute, tosca.nodes.nfv.VnfVirtualLink, tosca.groups.nfv.PlacementGroup ] + + tosca.policies.nfv.AntiAffinityRule: + derived_from: tosca.policies.Placement + description: The AntiAffinityRule describes the anti-affinity rules applicable for the defined targets + properties: + scope: + type: string + description: scope of the rule is an NFVI_node, an NFVI_PoP, etc. + required: true + constraints: + - valid_values: [ nfvi_node, zone, zone_group, nfvi_pop ] + targets: [ tosca.nodes.nfv.Vdu.Compute, tosca.nodes.nfv.VnfVirtualLink, tosca.groups.nfv.PlacementGroup ] + + tosca.policies.nfv.SecurityGroupRule: + derived_from: tosca.policies.Root + description: The SecurityGroupRule type is a policy type specified the matching criteria for the ingress and/or egress traffic to/from visited connection points as defined in ETSI GS NFV-IFA 011 [1]. + properties: + description: + type: string + description: Human readable description of the security group rule. + required: false + direction: + type: string + description: The direction in which the security group rule is applied. The direction of 'ingress' or 'egress' is specified against the associated CP. I.e., 'ingress' means the packets entering a CP, while 'egress' means the packets sent out of a CP. + required: false + constraints: + - valid_values: [ ingress, egress ] + default: ingress + ether_type: + type: string + description: Indicates the protocol carried over the Ethernet layer. + required: false + constraints: + - valid_values: [ ipv4, ipv6 ] + default: ipv4 + protocol: + type: string + description: Indicates the protocol carried over the IP layer. Permitted values include any protocol defined in the IANA protocol registry, e.g. TCP, UDP, ICMP, etc. + required: false + constraints: + - valid_values: [ hopopt, icmp, igmp, ggp, ipv4, st, tcp, cbt, egp, igp, bbn_rcc_mon, nvp_ii, pup, argus, emcon, xnet, chaos, udp, mux, dcn_meas, hmp, prm, xns_idp, trunk_1, trunk_2, leaf_1, leaf_2, rdp, irtp, iso_tp4, netblt, mfe_nsp, merit_inp, dccp, 3pc, idpr, xtp, ddp, idpr_cmtp, tp++, il, ipv6, sdrp, ipv6_route, ipv6_frag, idrp, rsvp, gre, dsr, bna, esp, ah, i_nlsp, swipe, narp, mobile, tlsp, skip, ipv6_icmp, ipv6_no_nxt, ipv6_opts, cftp, sat_expak, kryptolan, rvd, ippc, sat_mon, visa, ipcv, cpnx, cphb, wsn, pvp, br_sat_mon, sun_nd, wb_mon, wb_expak, iso_ip, vmtp, secure_vmtp, vines, ttp, iptm, nsfnet_igp, dgp, tcf, eigrp, ospfigp, sprite_rpc, larp, mtp, ax.25, ipip, micp, scc_sp, etherip, encap, gmtp, ifmp, pnni, pim, aris, scps, qnx, a/n, ip_comp, snp, compaq_peer, ipx_in_ip, vrrp, pgm, l2tp, ddx, iatp, stp, srp, uti, smp, sm, ptp, isis, fire, crtp, crudp, sscopmce, iplt, sps, pipe, sctp, fc, rsvp_e2e_ignore, mobility, udp_lite, mpls_in_ip, manet, hip, shim6, wesp, rohc ] + default: tcp + port_range_min: + type: integer + description: Indicates minimum port number in the range that is matched by the security group rule. If a value is provided at design-time, this value may be overridden at run-time based on other deployment requirements or constraints. + required: false + constraints: + - greater_or_equal: 0 + - less_or_equal: 65535 + default: 0 + port_range_max: + type: integer + description: Indicates maximum port number in the range that is matched by the security group rule. If a value is provided at design-time, this value may be overridden at run-time based on other deployment requirements or constraints. + required: false + constraints: + - greater_or_equal: 0 + - less_or_equal: 65535 + default: 65535 + targets: [ tosca.nodes.nfv.VduCp, tosca.nodes.nfv.VnfExtCp ] + + tosca.policies.nfv.SupportedVnfInterface: + derived_from: tosca.policies.Root + description: this policy type represents interfaces produced by a VNF, the details to access them and the applicable connection points to use to access these interfaces + properties: + interface_name: + type: string + description: Identifies an interface produced by the VNF. + required: true + constraints: + - valid_values: [ vnf_indicator, vnf_configuration ] + details: + type: tosca.datatypes.nfv.InterfaceDetails + description: Provide additional data to access the interface endpoint + required: false + targets: [ tosca.nodes.nfv.VnfExtCp, tosca.nodes.nfv.VduCp ] diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/hot_generate_hot_from_tosca.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/hot_generate_hot_from_tosca.yaml new file mode 100644 index 000000000..7983644f2 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/hot_generate_hot_from_tosca.yaml @@ -0,0 +1,65 @@ +heat_template_version: 2013-05-23 +description: 'Template for test _generate_hot_from_tosca(). + + ' +parameters: {} +resources: + VDU1: + type: OS::Nova::Server + properties: + networks: + - port: + get_resource: CP1 + - port: neutron-port-uuid_CP2 + - port: + get_resource: CP3 + - port: + get_resource: CP4 + flavor: + get_resource: VDU1_flavor + name: VDU1 + image: glance-image-uuid_VDU1 + VDU1_flavor: + type: OS::Nova::Flavor + properties: + disk: 1 + ram: 512 + vcpus: 1 + CP1: + type: OS::Neutron::Port + properties: + network: neutron-network-uuid_VL1 + fixed_ips: + - subnet: neutron-subnet-uuid_CP1 + ip_address: 1.1.1.1 + mac_address: fa:16:3e:11:11:11 + CP3: + type: OS::Neutron::Port + properties: + network: neutron-network-uuid_VL3 + CP4: + type: OS::Neutron::Port + properties: + network: + get_resource: VL4 + VL4: + type: OS::Neutron::Net + properties: + qos_policy: + get_resource: VL4_qospolicy + VL4_subnet: + type: OS::Neutron::Subnet + properties: + ip_version: 4 + cidr: 44.44.0.0/24 + network: + get_resource: VL4 + VL4_qospolicy: + type: OS::Neutron::QoSPolicy + VL4_bandwidth: + type: OS::Neutron::QoSBandwidthLimitRule + properties: + policy: + get_resource: VL4_qospolicy + max_kbps: 1024.0 +outputs: {} diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/scaling/hot_generate_hot_from_tosca_with_scaling.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/scaling/hot_generate_hot_from_tosca_with_scaling.yaml new file mode 100644 index 000000000..cf55cc372 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/scaling/hot_generate_hot_from_tosca_with_scaling.yaml @@ -0,0 +1,61 @@ +heat_template_version: 2013-05-23 +description: 'Template for test _generate_hot_from_tosca() with scaling. + + ' +parameters: {} +resources: + worker_instance: + type: OS::Heat::AutoScalingGroup + properties: + desired_capacity: 1 + resource: + properties: + vdu1_flavor_id: + get_resource: VDU1_flavor + vl3_id: neutron-network-uuid_VL3 + vl4_id: + get_resource: VL4 + type: worker_instance.hot.yaml + min_size: 1 + max_size: 3 + worker_instance_scale_out: + type: OS::Heat::ScalingPolicy + properties: + scaling_adjustment: 1 + adjustment_type: change_in_capacity + auto_scaling_group_id: + get_resource: worker_instance + worker_instance_scale_in: + type: OS::Heat::ScalingPolicy + properties: + scaling_adjustment: -1 + adjustment_type: change_in_capacity + auto_scaling_group_id: + get_resource: worker_instance + VDU1_flavor: + type: OS::Nova::Flavor + properties: + disk: 1 + ram: 512 + vcpus: 1 + VL4: + type: OS::Neutron::Net + properties: + qos_policy: + get_resource: VL4_qospolicy + VL4_subnet: + type: OS::Neutron::Subnet + properties: + ip_version: 4 + network: + get_resource: VL4 + cidr: 44.44.0.0/24 + VL4_qospolicy: + type: OS::Neutron::QoSPolicy + VL4_bandwidth: + type: OS::Neutron::QoSBandwidthLimitRule + properties: + max_kbps: 1024.0 + policy: + get_resource: VL4_qospolicy +outputs: {} diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/scaling/worker_instance.hot.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/scaling/worker_instance.hot.yaml new file mode 100644 index 000000000..48aa9b535 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/hot/scaling/worker_instance.hot.yaml @@ -0,0 +1,41 @@ +heat_template_version: 2013-05-23 +description: Scaling template +parameters: + vdu1_flavor_id: + type: string + vl3_id: + type: string + vl4_id: + type: string +resources: + VDU1: + type: OS::Nova::Server + properties: + name: VDU1 + networks: + - port: + get_resource: CP1 + - port: neutron-port-uuid_CP2 + - port: + get_resource: CP3 + - port: + get_resource: CP4 + flavor: + get_param: vdu1_flavor_id + image: glance-image-uuid_VDU1 + CP1: + type: OS::Neutron::Port + properties: + network: neutron-network-uuid_VL1 + fixed_ips: + - subnet: neutron-subnet-uuid_CP1 + CP3: + type: OS::Neutron::Port + properties: + network: + get_param: vl3_id + CP4: + type: OS::Neutron::Port + properties: + network: + get_param: vl4_id diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca.yaml new file mode 100644 index 000000000..939d9b960 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca.yaml @@ -0,0 +1,115 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: > + Template for test _generate_hot_from_tosca(). + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: +topology_template: + node_templates: + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: Software of VDU1 + version: '0.4.0' + checksum: + algorithm: sha-256 + hash: b9c3036539fd7a5f87a1bf38eb05fdde8b556a1a7e664dbeda90ed3cd74b4f9d + container_format: bare + disk_format: qcow2 + min_disk: 1 GiB + size: 1 GiB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: Files/images/cirros-0.4.0-x86_64-disk.img + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MiB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GiB + + CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU1 + + CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU1 + + CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU1 + - virtual_link: VL3 + + CP4: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 3 + requirements: + - virtual_binding: VDU1 + - virtual_link: VL4 + + VL3: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 33.33.0.0/24 + + VL4: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 44.44.0.0/24 diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_parser_error.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_parser_error.yaml new file mode 100644 index 000000000..1e276c48d --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_parser_error.yaml @@ -0,0 +1,16 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: > + Template for test _generate_hot_from_tosca() the case of tosca-parser error. + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: +topology_template: + node_templates: + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_translator_error.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_translator_error.yaml new file mode 100644 index 000000000..5f4330633 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_translator_error.yaml @@ -0,0 +1,48 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: > + Template for test _generate_hot_from_tosca() the case of heat-translator error. + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: +topology_template: + node_templates: + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: Software of VDU1 + version: '0.4.0' + checksum: + algorithm: sha-256 + hash: b9c3036539fd7a5f87a1bf38eb05fdde8b556a1a7e664dbeda90ed3cd74b4f9d + container_format: bare + disk_format: qcow2 + min_disk: 1 GiB + size: 1 GiB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: Files/images/cirros-0.4.0-x86_64-disk.img + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MiB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GiB + + CP1: + type: tosca.nodes.nfv.VnfExtCp + properties: + layer_protocols: [ ipv4 ] diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_params_error.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_params_error.yaml new file mode 100644 index 000000000..7a1783be7 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_params_error.yaml @@ -0,0 +1,197 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: > + Template for test _generate_hot_from_tosca(). + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: + ntt.nslab.VNF: + derived_from: tosca.nodes.nfv.VNF + properties: + descriptor_id: + type: string + constraints: [ valid_values: [ b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 ] ] + default: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 + descriptor_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + provider: + type: string + constraints: [ valid_values: [ 'NTT NS lab' ] ] + default: 'NTT NS lab' + product_name: + type: string + constraints: [ valid_values: [ 'Sample VNF' ] ] + default: 'Sample VNF' + software_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + vnfm_info: + type: list + entry_schema: + type: string + constraints: [ valid_values: [ Tacker ] ] + default: [ Tacker ] + flavour_id: + type: string + constraints: [ valid_values: [ simple ] ] + default: simple + flavour_description: + type: string + default: "" + requirements: + - virtual_link_external: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm + +topology_template: + inputs: + selected_flavour: + type: string + default: simple + description: VNF deployment flavour selected by the consumer. It is provided in the API + + substitution_mappings: + node_type: ntt.nslab.VNF + properties: + flavour_id: simple + requirements: + virtual_link_external: [ CP1, virtual_link ] + + node_templates: + VNF: + type: ntt.nslab.VNF + properties: + flavour_id: { get_input: selected_flavour } + descriptor_id: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 + provider: NTT NS lab + product_name: Sample VNF + software_version: '1.0' + descriptor_version: '1.0' + vnfm_info: + - Tacker + flavour_description: A simple flavour + interfaces: + Vnflcm: + instantiate: [] + instantiate_start: [] + instantiate_end: [] + terminate: [] + terminate_start: [] + terminate_end: [] + modify_information: [] + modify_information_start: [] + modify_information_end: [] + + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: Software of VDU1 + version: '0.4.0' + checksum: + algorithm: sha-256 + hash: b9c3036539fd7a5f87a1bf38eb05fdde8b556a1a7e664dbeda90ed3cd74b4f9d + container_format: bare + disk_format: qcow2 + min_disk: 1 GiB + size: 1 GiB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: Files/images/cirros-0.4.0-x86_64-disk.img + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MiB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GiB + + CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU1 + + CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU1 + + CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU1 + - virtual_link: VL3 + + CP4: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 3 + requirements: + - virtual_binding: VDU1 + - virtual_link: VL4 + + VL3: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 33.33.0.0/24 + + VL4: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 44.44.0.0/24 diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_scaling.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_scaling.yaml new file mode 100644 index 000000000..d9e58a090 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_scaling.yaml @@ -0,0 +1,183 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: > + Template for test _generate_hot_from_tosca() with scaling. + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: +topology_template: + node_templates: + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: Software of VDU1 + version: '0.4.0' + checksum: + algorithm: sha-256 + hash: b9c3036539fd7a5f87a1bf38eb05fdde8b556a1a7e664dbeda90ed3cd74b4f9d + container_format: bare + disk_format: qcow2 + min_disk: 1 GiB + size: 1 GiB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: Files/images/cirros-0.4.0-x86_64-disk.img + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MiB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GiB + + CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU1 + + CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU1 + + CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU1 + - virtual_link: VL3 + + CP4: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 3 + requirements: + - virtual_binding: VDU1 + - virtual_link: VL4 + + VL3: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 33.33.0.0/24 + + VL4: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 44.44.0.0/24 + + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + worker_instance: + name: worker_instance_aspect + description: worker_instance scaling aspect + max_scale_level: 2 + step_deltas: + - delta_1 + + - VDU1_initial_delta: + type: tosca.policies.nfv.VduInitialDelta + properties: + initial_delta: + number_of_instances: 1 + targets: [ VDU1 ] + + - VDU1_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: worker_instance + deltas: + delta_1: + number_of_instances: 1 + targets: [ VDU1 ] + + - instantiation_levels: + type: tosca.policies.nfv.InstantiationLevels + properties: + levels: + instantiation_level_1: + description: Smallest size + scale_info: + worker_instance: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + worker_instance: + scale_level: 2 + default_level: instantiation_level_1 + + - VDU1_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 3 + targets: [ VDU1 ] + + - VL4_instantiation_levels: + type: tosca.policies.nfv.VirtualLinkInstantiationLevels + properties: + levels: + instantiation_level_1: + bitrate_requirements: + root: 1048576 + leaf: 1048576 + instantiation_level_2: + bitrate_requirements: + root: 1048576 + leaf: 1048576 + targets: [ VL4 ] diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_scaling_invalid_inst_req.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_scaling_invalid_inst_req.yaml new file mode 100644 index 000000000..89c66d302 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_scaling_invalid_inst_req.yaml @@ -0,0 +1,105 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: > + Template for test _generate_hot_from_tosca() with scaling the case of invalid inst_req_info. + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: +topology_template: + node_templates: + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: Software of VDU1 + version: '0.4.0' + checksum: + algorithm: sha-256 + hash: b9c3036539fd7a5f87a1bf38eb05fdde8b556a1a7e664dbeda90ed3cd74b4f9d + container_format: bare + disk_format: qcow2 + min_disk: 1 GiB + size: 1 GiB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: Files/images/cirros-0.4.0-x86_64-disk.img + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MiB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GiB + + CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU1 + + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + worker_instance: + name: worker_instance_aspect + description: worker_instance scaling aspect + max_scale_level: 2 + step_deltas: + - delta_1 + + - VDU1_initial_delta: + type: tosca.policies.nfv.VduInitialDelta + properties: + initial_delta: + number_of_instances: 1 + targets: [ VDU1 ] + + - VDU1_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: worker_instance + deltas: + delta_1: + number_of_instances: 1 + targets: [ VDU1 ] + + - instantiation_levels: + type: tosca.policies.nfv.InstantiationLevels + properties: + levels: + instantiation_level_1: + description: Smallest size + scale_info: + worker_instance: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + worker_instance: + scale_level: 2 + default_level: instantiation_level_1 + + - VDU1_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 3 + targets: [ VDU1 ] diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_substitution_mappings_error.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_substitution_mappings_error.yaml new file mode 100644 index 000000000..7a1783be7 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_generate_hot_from_tosca_with_substitution_mappings_error.yaml @@ -0,0 +1,197 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: > + Template for test _generate_hot_from_tosca(). + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: + ntt.nslab.VNF: + derived_from: tosca.nodes.nfv.VNF + properties: + descriptor_id: + type: string + constraints: [ valid_values: [ b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 ] ] + default: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 + descriptor_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + provider: + type: string + constraints: [ valid_values: [ 'NTT NS lab' ] ] + default: 'NTT NS lab' + product_name: + type: string + constraints: [ valid_values: [ 'Sample VNF' ] ] + default: 'Sample VNF' + software_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + vnfm_info: + type: list + entry_schema: + type: string + constraints: [ valid_values: [ Tacker ] ] + default: [ Tacker ] + flavour_id: + type: string + constraints: [ valid_values: [ simple ] ] + default: simple + flavour_description: + type: string + default: "" + requirements: + - virtual_link_external: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm + +topology_template: + inputs: + selected_flavour: + type: string + default: simple + description: VNF deployment flavour selected by the consumer. It is provided in the API + + substitution_mappings: + node_type: ntt.nslab.VNF + properties: + flavour_id: simple + requirements: + virtual_link_external: [ CP1, virtual_link ] + + node_templates: + VNF: + type: ntt.nslab.VNF + properties: + flavour_id: { get_input: selected_flavour } + descriptor_id: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 + provider: NTT NS lab + product_name: Sample VNF + software_version: '1.0' + descriptor_version: '1.0' + vnfm_info: + - Tacker + flavour_description: A simple flavour + interfaces: + Vnflcm: + instantiate: [] + instantiate_start: [] + instantiate_end: [] + terminate: [] + terminate_start: [] + terminate_end: [] + modify_information: [] + modify_information_start: [] + modify_information_end: [] + + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: Software of VDU1 + version: '0.4.0' + checksum: + algorithm: sha-256 + hash: b9c3036539fd7a5f87a1bf38eb05fdde8b556a1a7e664dbeda90ed3cd74b4f9d + container_format: bare + disk_format: qcow2 + min_disk: 1 GiB + size: 1 GiB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: Files/images/cirros-0.4.0-x86_64-disk.img + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MiB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GiB + + CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU1 + + CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU1 + + CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU1 + - virtual_link: VL3 + + CP4: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 3 + requirements: + - virtual_binding: VDU1 + - virtual_link: VL4 + + VL3: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 33.33.0.0/24 + + VL4: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 44.44.0.0/24 diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_params_error.yaml b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_params_error.yaml new file mode 100644 index 000000000..fd9f26768 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_params_error.yaml @@ -0,0 +1,3 @@ +{ + cpus: [, +} diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/client.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/client.py index eec81bc6b..cece93df7 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/client.py +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/client.py @@ -18,10 +18,11 @@ from heatclient import client from keystoneauth1 import fixture from keystoneauth1 import loading from keystoneauth1 import session - +from openstack import connection IDENTITY_URL = 'http://identityserver:5000/v3' HEAT_URL = 'http://heat-api' +GLANCE_URL = 'http://image-api/v2' class ClientFixture(fixtures.Fixture): @@ -37,20 +38,44 @@ class ClientFixture(fixtures.Fixture): self.discovery = fixture.V2Discovery(href=self.identity_url) s = self.token.add_service('orchestration') s.add_endpoint(heat_url) + self.auth_url = '%s/tokens' % self.identity_url def setUp(self): super(ClientFixture, self).setUp() - auth_url = '%s/tokens' % self.identity_url headers = {'X-Content-Type': 'application/json'} - self.requests_mock.post(auth_url, + self.requests_mock.post(self.auth_url, json=self.token, headers=headers) self.requests_mock.get(self.identity_url, json=self.discovery, headers=headers) self.client = self.new_client() - def new_client(self): + def _set_session(self): self.session = session.Session() loader = loading.get_plugin_loader('password') self.session.auth = loader.load_from_options( auth_url=self.identity_url, username='xx', password='xx') + + def new_client(self): + self._set_session() return client.Client("1", session=self.session) + + +class SdkConnectionFixture(ClientFixture): + """Fixture class to access the apis via openstacksdk's Connection object. + + This class is mocking the requests of glance api. + """ + + def __init__(self, requests_mock, glance_url=GLANCE_URL): + super(SdkConnectionFixture, self).__init__(requests_mock) + s = self.token.add_service('image') + s.add_endpoint(glance_url) + + def new_client(self): + self._set_session() + conn = connection.Connection( + region_name=None, + session=self.session, + identity_interface='internal', + image_api_version='2') + return conn diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py index 287b38cfa..3b92d75e9 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py @@ -12,10 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. +import os +import yaml + +from tacker import objects +from tacker.objects import fields from tacker.tests import uuidsentinel -def get_dummy_stack(outputs=True, status='CREATE_COMPELETE'): +def get_dummy_stack(outputs=True, status='CREATE_COMPELETE', attrs=None): outputs_value = [{}] if outputs: outputs_value = [{'output_value': '192.168.120.216', @@ -33,22 +38,26 @@ def get_dummy_stack(outputs=True, status='CREATE_COMPELETE'): 'stack_owner': None, 'updated_time': None, 'id': uuidsentinel.instance_id} + if attrs: + dummy_stack.update(attrs) return dummy_stack -def get_dummy_resource(resource_status='CREATE_COMPLETE'): - return {'resource_name': 'SP1_group', +def get_dummy_resource(resource_status='CREATE_COMPLETE', + resource_name='SP1_group', physical_resource_id=uuidsentinel.stack_id, + resource_type='OS::Heat::AutoScalingGroup'): + return {'resource_name': resource_name, 'logical_resource_id': 'SP1_group', 'creation_time': '2019-03-06T08:57:47Z', 'resource_status_reason': 'state changed', 'updated_time': '2019-03-06T08:57:47Z', 'required_by': ['SP1_scale_out', 'SP1_scale_in'], 'resource_status': resource_status, - 'physical_resource_id': uuidsentinel.stack_id, + 'physical_resource_id': physical_resource_id, 'attributes': {'outputs_list': None, 'refs': None, 'refs_map': None, 'outputs': None, 'current_size': None, 'mgmt_ip-vdu1': 'test1'}, - 'resource_type': 'OS::Heat::AutoScalingGroup'} + 'resource_type': resource_type} def get_dummy_event(resource_status='CREATE_COMPLETE'): @@ -68,3 +77,256 @@ def get_dummy_policy_dict(): 'action': 'out', 'type': 'tosca.policies.tacker.Scaling', 'properties': {}} + + +def get_vnf_instance_object(instantiated_vnf_info=None, + instantiation_state=fields.VnfInstanceState.NOT_INSTANTIATED): + + inst_vnf_info = instantiated_vnf_info or get_vnf_instantiated_info() + + vnf_instance = objects.VnfInstance(id=uuidsentinel.vnf_instance_id, + vnf_instance_name="Test-Vnf-Instance", + vnf_instance_description="vnf instance description", + instantiation_state=instantiation_state, vnfd_id=uuidsentinel.vnfd_id, + vnf_provider="sample provider", vnf_product_name="vnf product name", + vnf_software_version='1.0', vnfd_version="2", + instantiated_vnf_info=inst_vnf_info) + + return vnf_instance + + +def get_virtual_storage_resource_info(desc_id="VirtualStorage", + set_resource_id=True): + + if set_resource_id: + resource_id = uuidsentinel.storage_resource_id + else: + resource_id = "" + + resource_handle = objects.ResourceHandle( + resource_id=resource_id, + vim_level_resource_type="OS::Cinder::Volume") + + storage_resource_info = objects.VirtualStorageResourceInfo( + id=uuidsentinel.storage_id, + virtual_storage_desc_id=desc_id, + storage_resource=resource_handle) + + return storage_resource_info + + +def _get_virtual_link_port(virtual_link_port_id, cp_instance_id, + set_resource_id=False): + + if set_resource_id: + resource_id = uuidsentinel.virtual_link_port_resource_id + else: + resource_id = "" + + resource_handle = objects.ResourceHandle( + resource_id=resource_id, + vim_level_resource_type="OS::Neutron::Port") + + v_l_port = objects.VnfLinkPortInfo( + id=virtual_link_port_id, cp_instance_id=cp_instance_id, + resource_handle=resource_handle) + + return v_l_port + + +def get_virtual_link_resource_info(virtual_link_port_id, cp_instance_id, + desc_id="internalVL1", set_resource_id=True): + + network_resource = objects.ResourceHandle( + resource_id=uuidsentinel.virtual_link_resource_id, + vim_level_resource_type="OS::Neutron::Network") + + v_l_link_port = _get_virtual_link_port(virtual_link_port_id, + cp_instance_id=cp_instance_id, set_resource_id=set_resource_id) + + v_l_resource_info = objects.VnfVirtualLinkResourceInfo( + id=uuidsentinel.v_l_resource_info_id, + vnf_virtual_link_desc_id=desc_id, + network_resource=network_resource, vnf_link_ports=[v_l_link_port]) + + return v_l_resource_info + + +def _get_ext_virtual_link_port(ext_v_l_port_id, cp_instance_id, + set_resource_id=False): + if set_resource_id: + resource_id = uuidsentinel.ext_virtual_link_port_resource_id + else: + resource_id = "" + + resource_handle = objects.ResourceHandle( + resource_id=resource_id, + vim_level_resource_type="OS::Neutron::Port") + + ext_v_l_port = objects.VnfLinkPortInfo( + id=ext_v_l_port_id, cp_instance_id=cp_instance_id, + resource_handle=resource_handle) + + return ext_v_l_port + + +def get_ext_managed_virtual_link_resource_info(virtual_link_port_id, + cp_instance_id, desc_id="externalVL1", set_resource_id=True): + network_resource = objects.ResourceHandle( + resource_id=uuidsentinel.ext_managed_virtual_link_resource_id) + + ext_v_l_link_port = _get_ext_virtual_link_port(virtual_link_port_id, + cp_instance_id=cp_instance_id, set_resource_id=set_resource_id) + + ext_managed_v_l_resource_info = objects.ExtManagedVirtualLinkInfo( + id=uuidsentinel.v_l_resource_info_id, + vnf_virtual_link_desc_id=desc_id, + network_resource=network_resource, + vnf_link_ports=[ext_v_l_link_port]) + + return ext_managed_v_l_resource_info + + +def _get_vnfc_cp_info(virtual_link_port_id, cpd_id="CP1"): + vnfc_cp_info = objects.VnfcCpInfo( + id=uuidsentinel.vnfc_cp_info_id, + cpd_id=cpd_id, + cp_protocol_info=[], + vnf_link_port_id=virtual_link_port_id) + + return vnfc_cp_info + + +def get_vnfc_resource_info(vdu_id="VDU1", storage_resource_ids=None, + set_resource_id=True): + storage_resource_ids = storage_resource_ids or [] + + if set_resource_id: + resource_id = uuidsentinel.vdu_resource_id + else: + resource_id = "" + + resource_handle = objects.ResourceHandle( + resource_id=resource_id, + vim_level_resource_type="OS::Nova::Server") + + vnfc_cp_info = _get_vnfc_cp_info(uuidsentinel.virtual_link_port_id) + + vnfc_resource_info = objects.VnfcResourceInfo( + id=uuidsentinel.vnfc_resource_id, vdu_id=vdu_id, + compute_resource=resource_handle, vnfc_cp_info=[vnfc_cp_info], + storage_resource_ids=storage_resource_ids) + + return vnfc_resource_info + + +def get_vnf_instantiated_info(flavour_id='simple', + instantiation_level_id=None, vnfc_resource_info=None, + virtual_storage_resource_info=None, + vnf_virtual_link_resource_info=None, + ext_managed_virtual_link_info=None): + + vnfc_resource_info = vnfc_resource_info or [] + vnf_virtual_link_resource_info = vnf_virtual_link_resource_info or [] + virtual_storage_resource_info = virtual_storage_resource_info or [] + ext_managed_virtual_link_info = ext_managed_virtual_link_info or [] + + inst_vnf_info = objects.InstantiatedVnfInfo(flavour_id=flavour_id, + instantiation_level_id=instantiation_level_id, + instance_id=uuidsentinel.instance_id, + vnfc_resource_info=vnfc_resource_info, + vnf_virtual_link_resource_info=vnf_virtual_link_resource_info, + virtual_storage_resource_info=virtual_storage_resource_info, + ext_managed_virtual_link_info=ext_managed_virtual_link_info) + + return inst_vnf_info + + +def get_vnf_software_image_object(image_path=None): + image_path = image_path or ("http://download.cirros-cloud.net/0.4.0/" + "cirros-0.4.0-x86_64-disk.img") + vnf_software_image = objects.VnfSoftwareImage( + name='test-image', image_path=image_path, + min_disk=10, min_ram=4, disk_format="qcow2", + container_format="bare", hash="hash") + + return vnf_software_image + + +def get_fake_glance_image_dict(image_path=None, status='pending_create', + hash_value='hash'): + """Create a fake glance image. + + :return: + Glance image dict with id, name, etc. + """ + + if not image_path: + image_path = "http://localhost/cirros.img" + + image_attrs = {"name": 'test-image', "image_path": image_path, + "id": uuidsentinel.image_id, + "min_disk": "fake_description", + "min_ram": "0", + "disk_format": "qcow2", + "container_format": "bare", + "hash_value": hash_value, + "status": status} + + return image_attrs + + +def get_vnf_resource_object(resource_name="VDU1", + resource_type="OS::Nova::Server"): + vnf_resource = objects.VnfResource( + resource_identifier=uuidsentinel.resource_identifier, + id=uuidsentinel.vnf_resource_id, + resource_name=resource_name, + resource_type=resource_type, + vnf_instance_id=uuidsentinel.vnf_instance_id) + + return vnf_resource + + +def get_vim_connection_info_object(): + access_info = {'auth_url': 'http://127.0.1.0/identity/v3', + 'cert_verify': True, + 'password': 'devstack', + 'project_name': 'nfv', + 'username': 'nfv_user'} + + vim_connection = objects.VimConnectionInfo( + id=uuidsentinel.vim_connection_id, vim_id=uuidsentinel.vim_id, + vim_type='openstack', access_info=access_info) + + return vim_connection + + +def get_vnfd_dict(): + filename = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../data/", + 'test_tosca_image.yaml') + with open(filename) as f: + vnfd_dict = {'vnfd': {'attributes': {'vnfd': str(yaml.safe_load(f))}}} + vnfd_dict.update({'id': '7ed39362-c551-4ce7-9ad2-17a98a6cee3d', + 'name': None, 'attributes': {'param_values': "", + 'stack_name': 'vnflcm_7ed39362-c551-4ce7-9ad2-17a98a6cee3d'}, + 'placement_attr': {'region_name': None}}) + + return vnfd_dict + + +def get_instantiate_vnf_request(): + inst_vnf_req = objects.InstantiateVnfRequest( + flavour_id='simple') + + return inst_vnf_req + + +def get_grant_response_dict(): + grant_response_dict = { + 'VDU1': [get_vnf_resource_object(resource_name='VDU1')], + 'VirtualStorage': [get_vnf_resource_object( + resource_name='VirtualStorage')]} + + return grant_response_dict diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_etsi_translate_template.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_etsi_translate_template.py new file mode 100644 index 000000000..8aeaa46f7 --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_etsi_translate_template.py @@ -0,0 +1,297 @@ +# Copyright 2015 Brocade Communications System, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import codecs +import os +import yaml + +from tacker import context +from tacker.extensions import vnfm +from tacker import objects +from tacker.tests.unit import base +from tacker.tests import uuidsentinel +from tacker.vnfm.infra_drivers.openstack.translate_template import TOSCAToHOT + + +class TestEtsiTranslateTemplate(base.TestCase): + + def setUp(self): + super(TestEtsiTranslateTemplate, self).setUp() + self.tth = TOSCAToHOT(None, None) + self.tth.fields = {} + self.tth.vnf = {} + self.tth.vnf['attributes'] = {} + + def _get_template(self, name): + filename = os.path.join( + os.path.dirname(os.path.abspath(__file__)), name) + with codecs.open(filename, encoding='utf-8', errors='strict') as f: + return f.read() + + def _load_yaml(self, yaml_name, update_import=False): + filename = os.path.join( + os.path.dirname(os.path.abspath(__file__)), yaml_name) + file_abspath = os.path.dirname(os.path.abspath(filename)) + with open(filename, 'r') as f: + heat_yaml = f.read() + heat_dict = yaml.safe_load(heat_yaml) + if update_import: + self._update_imports(heat_dict, file_abspath) + return heat_dict + + def _update_imports(self, yaml_dict, file_abspath): + imports = yaml_dict['imports'] + new_imports = [] + for i in imports: + new_imports.append(file_abspath + '/' + i) + yaml_dict['imports'] = new_imports + + def test_generate_hot_from_tosca(self): + tosca_file = './data/etsi_nfv/' \ + 'tosca_generate_hot_from_tosca.yaml' + hot_file = './data/etsi_nfv/hot/' \ + 'hot_generate_hot_from_tosca.yaml' + vnfd_dict = self._load_yaml(tosca_file, update_import=True) + + # Input params + dev_attrs = {} + + data = [{ + "id": 'VL1', + "resource_id": 'neutron-network-uuid_VL1', + "ext_cps": [{ + "cpd_id": "CP1", + "cp_config": [{ + "cp_protocol_data": [{ + "layer_protocol": "IP_OVER_ETHERNET", + "ip_over_ethernet": { + "mac_address": 'fa:16:3e:11:11:11', + "ip_addresses": [{ + 'type': 'IPV4', + 'fixed_addresses': ['1.1.1.1'], + 'subnet_id': 'neutron-subnet-uuid_CP1'}]} + }] + }]}]}, + { + "id": 'VL2', + "resource_id": 'neutron-network-uuid_VL2', + "ext_cps": [{ + "cpd_id": 'CP2', + "cp_config": [{ + "link_port_id": uuidsentinel.link_port_id, + "cp_protocol_data": [{ + "layer_protocol": "IP_OVER_ETHERNET"}]}] + }], + "ext_link_ports": [{ + "id": uuidsentinel.link_port_id, + "resource_handle": { + "resource_id": 'neutron-port-uuid_CP2'} + }]}] + + ext_mg_vl = [{'id': 'VL3', 'vnf_virtual_link_desc_id': 'VL3', + 'resource_id': 'neutron-network-uuid_VL3'}] + request = {'ext_managed_virtual_links': ext_mg_vl, + 'ext_virtual_links': data, 'flavour_id': 'simple'} + ctxt = context.get_admin_context() + inst_req_info = objects.InstantiateVnfRequest.obj_from_primitive( + request, ctxt) + + # image and info + grant_info = { + 'VDU1': [objects.VnfResource(id=uuidsentinel.id, + vnf_instance_id=uuidsentinel.vnf_instance_id, + resource_type='image', + resource_identifier='glance-image-uuid_VDU1')]} + + self.tth._generate_hot_from_tosca(vnfd_dict, dev_attrs, + inst_req_info, grant_info) + + expected_hot_tpl = self._load_yaml(hot_file) + actual_hot_tpl = yaml.safe_load(self.tth.heat_template_yaml) + self.assertEqual(expected_hot_tpl, actual_hot_tpl) + + def test_generate_hot_from_tosca_with_scaling(self): + tosca_file = './data/etsi_nfv/' \ + 'tosca_generate_hot_from_tosca_with_scaling.yaml' + hot_file = './data/etsi_nfv/hot/' \ + 'scaling/' \ + 'hot_generate_hot_from_tosca_with_scaling.yaml' + hot_aspect_file = './data/etsi_nfv/hot/' \ + 'scaling/' \ + 'worker_instance.hot.yaml' + vnfd_dict = self._load_yaml(tosca_file, update_import=True) + + # Input params + dev_attrs = {} + + data = [{ + "id": 'VL1', + "resource_id": 'neutron-network-uuid_VL1', + "ext_cps": [{ + "cpd_id": "CP1", + "cp_config": [{ + "cp_protocol_data": [{ + "layer_protocol": "IP_OVER_ETHERNET", + "ip_over_ethernet": { + "ip_addresses": [{ + 'type': 'IPV4', + 'subnet_id': 'neutron-subnet-uuid_CP1'}]}}] + }]}]}, + { + "id": 'VL2', + "resource_id": 'neutron-network-uuid_VL2', + "ext_cps": [{ + "cpd_id": 'CP2', + "cp_config": [{ + "link_port_id": uuidsentinel.link_port_id, + "cp_protocol_data": [{ + "layer_protocol": "IP_OVER_ETHERNET"}]}] + }], + "ext_link_ports": [{ + "id": uuidsentinel.link_port_id, + "resource_handle": { + "resource_id": 'neutron-port-uuid_CP2'} + }]}] + + ext_mg_vl = [{'id': 'VL3', 'vnf_virtual_link_desc_id': 'VL3', + 'resource_id': 'neutron-network-uuid_VL3'}] + request = {'ext_managed_virtual_links': ext_mg_vl, + 'ext_virtual_links': data, 'flavour_id': 'simple', + 'instantiation_level_id': 'instantiation_level_1'} + ctxt = context.get_admin_context() + inst_req_info = objects.InstantiateVnfRequest.obj_from_primitive( + request, ctxt) + + # image and info + grant_info = { + 'VDU1': [objects.VnfResource(id=uuidsentinel.id, + vnf_instance_id=uuidsentinel.vnf_instance_id, + resource_type='image', + resource_identifier='glance-image-uuid_VDU1')]} + + self.tth._generate_hot_from_tosca(vnfd_dict, dev_attrs, + inst_req_info, grant_info) + + expected_hot_tpl = self._load_yaml(hot_file) + actual_hot_tpl = yaml.safe_load(self.tth.heat_template_yaml) + self.assertEqual(expected_hot_tpl, actual_hot_tpl) + + expected_hot_aspect_tpl = self._load_yaml(hot_aspect_file) + actual_hot_aspect_tpl = \ + yaml.safe_load( + self.tth.nested_resources['worker_instance.hot.yaml']) + self.assertEqual(expected_hot_aspect_tpl, actual_hot_aspect_tpl) + + def test_generate_hot_from_tosca_with_substitution_mappings_error(self): + tosca_file = './data/etsi_nfv/' \ + 'tosca_generate_hot_from_tosca_' \ + 'with_substitution_mappings_error.yaml' + vnfd_dict = self._load_yaml(tosca_file, update_import=True) + + dev_attrs = {} + + self.assertRaises(vnfm.InvalidParamsForSM, + self.tth._generate_hot_from_tosca, + vnfd_dict, + dev_attrs, + None, + None) + + def test_generate_hot_from_tosca_with_params_error(self): + tosca_file = './data/etsi_nfv/' \ + 'tosca_generate_hot_from_tosca_with_params_error.yaml' + param_file = './data/etsi_nfv/' \ + 'tosca_params_error.yaml' + vnfd_dict = self._load_yaml(tosca_file, update_import=True) + + param_yaml = self._get_template(param_file) + dev_attrs = { + u'param_values': param_yaml + } + + self.assertRaises(vnfm.ParamYAMLNotWellFormed, + self.tth._generate_hot_from_tosca, + vnfd_dict, + dev_attrs, + None, + None) + + def test_generate_hot_from_tosca_parser_error(self): + tosca_file = './data/etsi_nfv/' \ + 'tosca_generate_hot_from_tosca_parser_error.yaml' + vnfd_dict = self._load_yaml(tosca_file, update_import=True) + + # Input params + dev_attrs = {} + + self.assertRaises(vnfm.ToscaParserFailed, + self.tth._generate_hot_from_tosca, + vnfd_dict, + dev_attrs, + None, + None) + + def test_generate_hot_from_tosca_translator_error(self): + tosca_file = './data/etsi_nfv/' \ + 'tosca_generate_hot_from_tosca_translator_error.yaml' + vnfd_dict = self._load_yaml(tosca_file, update_import=True) + + # Input params + dev_attrs = {} + + self.assertRaises(vnfm.HeatTranslatorFailed, + self.tth._generate_hot_from_tosca, + vnfd_dict, + dev_attrs, + None, + None) + + def test_generate_hot_from_tosca_with_scaling_invalid_inst_req(self): + tosca_file = './data/etsi_nfv/' \ + 'tosca_generate_hot_from_tosca_with_scaling_invalid_inst_req.yaml' + vnfd_dict = self._load_yaml(tosca_file, update_import=True) + + # Input params + dev_attrs = {} + + data = [{ + "id": 'VL1', + "resource_id": 'neutron-network-uuid_VL1', + "ext_cps": [{ + "cpd_id": "CP1", + "cp_config": [{ + "cp_protocol_data": [{ + "layer_protocol": "IP_OVER_ETHERNET", + "ip_over_ethernet": { + "ip_addresses": [{ + 'type': 'IPV4', + 'fixed_addresses': ['1.1.1.1'], + 'subnet_id': 'neutron-subnet-uuid_CP1'}]} + }] + }] + }]} + ] + + request = {'ext_virtual_links': data, 'flavour_id': 'simple'} + ctxt = context.get_admin_context() + inst_req_info = objects.InstantiateVnfRequest.obj_from_primitive( + request, ctxt) + + self.assertRaises(vnfm.InvalidInstReqInfoForScaling, + self.tth._generate_hot_from_tosca, + vnfd_dict, + dev_attrs, + inst_req_info, + None) diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack.py index 6c0c8d667..26d8f48c9 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack.py +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack.py @@ -14,8 +14,9 @@ # under the License. import codecs -import mock import os + +import mock import yaml from oslo_serialization import jsonutils @@ -96,8 +97,8 @@ class FakeHeatClient(mock.Mock): def _get_template(name): filename = os.path.join( os.path.dirname(os.path.abspath(__file__)), "data/", name) - f = codecs.open(filename, encoding='utf-8', errors='strict') - return f.read() + with codecs.open(filename, encoding='utf-8', errors='strict') as f: + return f.read() class TestOpenStack(base.TestCase): @@ -119,6 +120,9 @@ class TestOpenStack(base.TestCase): self._cos_db_plugin = \ common_services_db_plugin.CommonServicesPluginDb() self.addCleanup(mock.patch.stopall) + yaml.SafeLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + lambda loader, node: dict(loader.construct_pairs(node))) def _mock_heat_client(self): self.heat_client = mock.Mock(wraps=FakeHeatClient()) diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py index 186cf6723..06a6e73da 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py @@ -15,7 +15,11 @@ import ddt import mock +import os +import requests +import tempfile +from tacker.common import exceptions from tacker import context from tacker.extensions import vnfm from tacker.tests.common import helpers @@ -31,18 +35,22 @@ from tacker.vnfm.infra_drivers.openstack import openstack @ddt.ddt class TestOpenStack(base.FixturedTestCase): client_fixture_class = client.ClientFixture + sdk_connection_fixure_class = client.SdkConnectionFixture def setUp(self): super(TestOpenStack, self).setUp() self.openstack = openstack.OpenStack() self.context = context.get_admin_context() - self.url = client.HEAT_URL + self.heat_url = client.HEAT_URL + self.glance_url = client.GLANCE_URL self.instance_uuid = uuidsentinel.instance_id self.stack_id = uuidsentinel.stack_id self.json_headers = {'content-type': 'application/json', 'location': 'http://heat-api/stacks/' + self.instance_uuid + '/myStack/60f83b5e'} self._mock('tacker.common.clients.OpenstackClients.heat', self.cs) + mock.patch('tacker.common.clients.OpenstackSdkConnection.' + 'openstack_connection', return_value=self.sdk_conn).start() self.mock_log = mock.patch('tacker.vnfm.infra_drivers.openstack.' 'openstack.LOG').start() mock.patch('time.sleep', return_value=None).start() @@ -51,7 +59,7 @@ class TestOpenStack(base.FixturedTestCase): stack_outputs=True): # response for heat_client's get() for status in status_list: - url = self.url + '/stacks/' + self.instance_uuid + url = self.heat_url + '/stacks/' + self.instance_uuid json = {'stack': fd_utils.get_dummy_stack(stack_outputs, status=status)} self.requests_mock.register_uri('GET', url, json=json, @@ -60,10 +68,10 @@ class TestOpenStack(base.FixturedTestCase): def _response_in_resource_get(self, id, res_name=None): # response for heat_client's resource_get() if res_name: - url = self.url + '/stacks/' + id + ('/myStack/60f83b5e/' + url = self.heat_url + '/stacks/' + id + ('/myStack/60f83b5e/' 'resources/') + res_name else: - url = self.url + '/stacks/' + id + url = self.heat_url + '/stacks/' + id json = {'resource': fd_utils.get_dummy_resource()} self.requests_mock.register_uri('GET', url, json=json, @@ -96,7 +104,7 @@ class TestOpenStack(base.FixturedTestCase): "CREATE_COMPLETE"]) self._response_in_resource_get(self.instance_uuid, res_name='SP1_group') - url = self.url + '/stacks/' + self.stack_id + '/resources' + url = self.heat_url + '/stacks/' + self.stack_id + '/resources' json = {'resources': [fd_utils.get_dummy_resource()]} self.requests_mock.register_uri('GET', url, json=json, headers=self.json_headers) @@ -123,7 +131,7 @@ class TestOpenStack(base.FixturedTestCase): None, None, vnf_dict, self.instance_uuid, {}) def _exception_response(self): - url = self.url + '/stacks/' + self.instance_uuid + url = self.heat_url + '/stacks/' + self.instance_uuid body = {"error": Exception("any stuff")} self.requests_mock.register_uri('GET', url, body=body, status_code=404, headers=self.json_headers) @@ -202,13 +210,13 @@ class TestOpenStack(base.FixturedTestCase): def _responses_in_resource_event_list(self, dummy_event): # response for heat_client's resource_event_list() - url = self.url + '/stacks/' + self.instance_uuid + url = self.heat_url + '/stacks/' + self.instance_uuid json = {'stack': [fd_utils.get_dummy_stack()]} self.requests_mock.register_uri('GET', url, json=json, headers=self.json_headers) - url = self.url + '/stacks/' + self.instance_uuid + ('/myStack/60f83b5e' - '/resources/SP1_scale_out/events?limit=1&sort_dir=desc&sort_keys=' - 'event_time') + url = self.heat_url + '/stacks/' + self.instance_uuid + ( + '/myStack/60f83b5e/resources/SP1_scale_out/events?limit=1&sort_dir' + '=desc&sort_keys=event_time') json = {'events': [dummy_event]} self.requests_mock.register_uri('GET', url, json=json, headers=self.json_headers) @@ -217,8 +225,8 @@ class TestOpenStack(base.FixturedTestCase): dummy_event = fd_utils.get_dummy_event() self._responses_in_resource_event_list(dummy_event) # response for heat_client's resource_signal() - url = self.url + '/stacks/' + self.instance_uuid + ('/myStack/60f83b5e' - '/resources/SP1_scale_out/signal') + url = self.heat_url + '/stacks/' + self.instance_uuid + ( + '/myStack/60f83b5e/resources/SP1_scale_out/signal') self.requests_mock.register_uri('POST', url, json={}, headers=self.json_headers) event_id = self.openstack.scale(plugin=self, context=self.context, @@ -229,7 +237,7 @@ class TestOpenStack(base.FixturedTestCase): def _response_in_resource_get_list(self): # response for heat_client's resource_get_list() - url = self.url + '/stacks/' + self.stack_id + '/resources' + url = self.heat_url + '/stacks/' + self.stack_id + '/resources' json = {'resources': [fd_utils.get_dummy_resource()]} self.requests_mock.register_uri('GET', url, json=json, headers=self.json_headers) @@ -279,10 +287,10 @@ class TestOpenStack(base.FixturedTestCase): def _response_in_resource_metadata(self, metadata=None): # response for heat_client's resource_metadata() - url = self.url + '/stacks/' + self.instance_uuid + \ + url = self.heat_url + '/stacks/' + self.instance_uuid + \ '/myStack/60f83b5e/resources/SP1_scale_out/metadata' json = {'metadata': {'scaling_in_progress': metadata}} - self.requests_mock.register_uri('GET', url, json=json, + return self.requests_mock.register_uri('GET', url, json=json, headers=self.json_headers) def test_scale_wait_failed_with_stack_retries_0(self): @@ -317,3 +325,441 @@ class TestOpenStack(base.FixturedTestCase): 'so ignore it') self.mock_log.warning.assert_called_once_with(error_reason) self.assertEqual(b'{"vdu1": ["test1"]}', mgmt_ip) + + def _responses_in_create_image(self, multiple_responses=False): + # response for glance_client's create() + json = fd_utils.get_fake_glance_image_dict() + url = os.path.join(self.glance_url, 'images') + if multiple_responses: + return self.requests_mock.register_uri( + 'POST', url, [{'json': json, 'status_code': 201, + 'headers': self.json_headers}, + {'exc': requests.exceptions.ConnectTimeout}]) + else: + return self.requests_mock.register_uri('POST', url, json=json, + headers=self.json_headers) + + def _responses_in_import_image(self, raise_exception=False): + # response for glance_client's import() + json = fd_utils.get_fake_glance_image_dict() + url = os.path.join( + self.glance_url, 'images', uuidsentinel.image_id, 'import') + + if raise_exception: + return self.requests_mock.register_uri('POST', url, + exc=requests.exceptions.ConnectTimeout) + else: + return self.requests_mock.register_uri('POST', url, json=json, + headers=self.json_headers) + + def _responses_in_get_image(self, image_path=None, status='active', + hash_value='hash'): + # response for glance_client's import() + json = fd_utils.get_fake_glance_image_dict(image_path=image_path, + status=status, + hash_value=hash_value) + url = os.path.join( + self.glance_url, 'images', uuidsentinel.image_id) + return self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + + def _responses_in_upload_image(self, image_path=None, status='active', + hash_value='hash'): + # response for glance_client's upload() + json = fd_utils.get_fake_glance_image_dict(image_path=image_path, + status=status, + hash_value=hash_value) + url = os.path.join( + self.glance_url, 'images', uuidsentinel.image_id, 'file') + return self.requests_mock.register_uri('PUT', url, json=json, + headers=self.json_headers) + + def test_pre_instantiation_vnf_image_with_file(self): + vnf_instance = fd_utils.get_vnf_instance_object() + + # Create a temporary file as the openstacksdk will access it for + # calculating the hash value. + image_fd, image_path = tempfile.mkstemp() + vnf_software_image = fd_utils.get_vnf_software_image_object( + image_path=image_path) + vnf_software_images = {'node_name': vnf_software_image} + + upload_image_url = self._responses_in_upload_image(image_path) + create_image_url = self._responses_in_create_image() + get_image_url = self._responses_in_get_image(image_path) + + vnf_resources = self.openstack.pre_instantiation_vnf( + self.context, vnf_instance, None, vnf_software_images) + + image_resource = vnf_resources['node_name'][0] + + os.close(image_fd) + os.remove(image_path) + + # Asserting the response as per the data given in the fake objects. + self.assertEqual(image_resource.resource_name, + 'test-image') + self.assertEqual(image_resource.resource_status, + 'CREATED') + self.assertEqual(image_resource.resource_type, + 'image') + self.assertEqual(image_resource.vnf_instance_id, + vnf_instance.id) + self.assertEqual(upload_image_url.call_count, 1) + self.assertEqual(create_image_url.call_count, 1) + self.assertEqual(get_image_url.call_count, 2) + + @mock.patch('tacker.common.utils.is_url', mock.MagicMock( + return_value=True)) + def test_pre_instantiation_vnf_image_with_url(self): + image_path = "http://fake-url.net" + vnf_instance = fd_utils.get_vnf_instance_object() + + vnf_software_image = fd_utils.get_vnf_software_image_object( + image_path=image_path) + vnf_software_images = {'node_name': vnf_software_image} + create_image_url = self._responses_in_create_image(image_path) + import_image_url = self._responses_in_import_image() + get_image_url = self._responses_in_get_image(image_path) + + vnf_resources = self.openstack.pre_instantiation_vnf( + self.context, vnf_instance, None, vnf_software_images) + + image_resource = vnf_resources['node_name'][0] + + # Asserting the response as per the data given in the fake objects. + self.assertEqual(image_resource.resource_name, + 'test-image') + self.assertEqual(image_resource.resource_status, + 'CREATED') + self.assertEqual(image_resource.resource_type, + 'image') + self.assertEqual(image_resource.vnf_instance_id, + vnf_instance.id) + self.assertEqual(create_image_url.call_count, 1) + self.assertEqual(import_image_url.call_count, 1) + self.assertEqual(get_image_url.call_count, 1) + + @ddt.data(False, True) + def test_pre_instantiation_vnf_failed_in_image_creation( + self, exception_in_delete_image): + vnf_instance = fd_utils.get_vnf_instance_object() + + vnf_software_image = fd_utils.get_vnf_software_image_object() + vnf_software_images = {'node_name1': vnf_software_image, + 'node_name2': vnf_software_image} + # exception will occur in second iteration of image creation. + create_image_url = self._responses_in_create_image( + multiple_responses=True) + import_image_url = self._responses_in_import_image() + get_image_url = self._responses_in_get_image() + delete_image_url = self._response_in_delete_image( + uuidsentinel.image_id, exception=exception_in_delete_image) + self.assertRaises(exceptions.VnfPreInstantiationFailed, + self.openstack.pre_instantiation_vnf, + self.context, vnf_instance, None, + vnf_software_images) + self.assertEqual(create_image_url.call_count, 3) + self.assertEqual(import_image_url.call_count, 1) + self.assertEqual(get_image_url.call_count, 1) + + delete_call_count = 2 if exception_in_delete_image else 1 + self.assertEqual(delete_image_url.call_count, delete_call_count) + + @ddt.data(False, True) + def test_pre_instantiation_vnf_failed_in_image_upload( + self, exception_in_delete_image): + vnf_instance = fd_utils.get_vnf_instance_object() + image_path = '/non/existent/file' + software_image_update = {'image_path': image_path} + vnf_software_image = fd_utils.get_vnf_software_image_object( + **software_image_update) + vnf_software_images = {'node_name1': vnf_software_image, + 'node_name2': vnf_software_image} + + # exception will occur in second iteration of image creation. + + # No urls are accessed in this case because openstacksdk fails to + # access the file when it wants to calculate the hash. + self._responses_in_create_image(multiple_responses=True) + self._responses_in_upload_image(image_path) + self._responses_in_get_image() + self._response_in_delete_image(uuidsentinel.image_id, + exception=exception_in_delete_image) + self.assertRaises(exceptions.VnfPreInstantiationFailed, + self.openstack.pre_instantiation_vnf, + self.context, vnf_instance, None, + vnf_software_images) + + def test_pre_instantiation_vnf_failed_with_mismatch_in_hash_value(self): + vnf_instance = fd_utils.get_vnf_instance_object() + + vnf_software_image = fd_utils.get_vnf_software_image_object() + vnf_software_images = {'node_name1': vnf_software_image, + 'node_name2': vnf_software_image} + # exception will occur in second iteration of image creation. + create_image_url = self._responses_in_create_image( + multiple_responses=True) + import_image_url = self._responses_in_import_image() + get_image_url = self._responses_in_get_image( + hash_value='diff-hash-value') + delete_image_url = self._response_in_delete_image( + uuidsentinel.image_id) + self.assertRaises(exceptions.VnfPreInstantiationFailed, + self.openstack.pre_instantiation_vnf, + self.context, vnf_instance, None, + vnf_software_images) + self.assertEqual(create_image_url.call_count, 1) + self.assertEqual(import_image_url.call_count, 1) + self.assertEqual(get_image_url.call_count, 1) + self.assertEqual(delete_image_url.call_count, 1) + + def test_pre_instantiation_vnf_with_image_create_wait_failed(self): + vnf_instance = fd_utils.get_vnf_instance_object() + + vnf_software_image = fd_utils.get_vnf_software_image_object() + vnf_software_images = {'node_name1': vnf_software_image, + 'node_name2': vnf_software_image} + # exception will occurs in second iteration of image creation. + create_image_url = self._responses_in_create_image() + import_image_url = self._responses_in_import_image() + get_image_url = self._responses_in_get_image(status='pending_create') + self.assertRaises(exceptions.VnfPreInstantiationFailed, + self.openstack.pre_instantiation_vnf, + self.context, vnf_instance, None, + vnf_software_images) + self.assertEqual(create_image_url.call_count, 1) + self.assertEqual(import_image_url.call_count, 1) + self.assertEqual(get_image_url.call_count, 10) + + def _exception_response_in_import_image(self): + url = os.path.join(self.glance_url, 'images', uuidsentinel.image_id, + 'import') + return self.requests_mock.register_uri( + 'POST', url, exc=requests.exceptions.ConnectTimeout) + + def _response_in_delete_image(self, resource_id, exception=False): + # response for glance_client's delete() + url = os.path.join( + self.glance_url, 'images', resource_id) + if exception: + return self.requests_mock.register_uri( + 'DELETE', url, exc=requests.exceptions.ConnectTimeout) + else: + return self.requests_mock.register_uri('DELETE', url, json={}, + status_code=200, + headers=self.json_headers) + + @ddt.data(True, False) + def test_pre_instantiation_vnf_failed_in_image_import( + self, exception_in_delete): + vnf_instance = fd_utils.get_vnf_instance_object() + + vnf_software_image = fd_utils.get_vnf_software_image_object() + vnf_software_images = {'node_name': vnf_software_image} + + create_image_url = self._responses_in_create_image() + import_image_exc_url = self._responses_in_import_image( + raise_exception=True) + delete_image_url = self._response_in_delete_image( + uuidsentinel.image_id, exception_in_delete) + self.assertRaises(exceptions.VnfPreInstantiationFailed, + self.openstack.pre_instantiation_vnf, + self.context, vnf_instance, None, + vnf_software_images) + self.assertEqual(create_image_url.call_count, 1) + self.assertEqual(import_image_exc_url.call_count, 2) + delete_call_count = 2 if exception_in_delete else 1 + self.assertEqual(delete_image_url.call_count, delete_call_count) + + @mock.patch('tacker.vnfm.infra_drivers.openstack.openstack.LOG') + def test_delete_vnf_instance_resource(self, mock_log): + vnf_instance = fd_utils.get_vnf_instance_object() + vnf_resource = fd_utils.get_vnf_resource_object() + + delete_image_url = self._response_in_delete_image( + vnf_resource.resource_identifier) + self.openstack.delete_vnf_instance_resource( + self.context, vnf_instance, None, vnf_resource) + mock_log.info.assert_called() + self.assertEqual(delete_image_url.call_count, 1) + + @mock.patch('tacker.vnfm.infra_drivers.openstack.openstack.LOG') + def test_delete_vnf_instance_resource_failed_with_exception( + self, mock_log): + vnf_instance = fd_utils.get_vnf_instance_object() + vnf_resource = fd_utils.get_vnf_resource_object() + + delete_image_url = self._response_in_delete_image( + vnf_resource.resource_identifier, exception=True) + self.openstack.delete_vnf_instance_resource( + self.context, vnf_instance, None, vnf_resource) + mock_log.info.assert_called() + self.assertEqual(delete_image_url.call_count, 2) + + @mock.patch('tacker.vnfm.infra_drivers.openstack.translate_template.' + 'TOSCAToHOT._get_unsupported_resource_props') + def test_instantiate_vnf(self, mock_get_unsupported_resource_props): + vim_connection_info = fd_utils.get_vim_connection_info_object() + inst_req_info = fd_utils.get_instantiate_vnf_request() + vnfd_dict = fd_utils.get_vnfd_dict() + grant_response = fd_utils.get_grant_response_dict() + + url = os.path.join(self.heat_url, 'stacks') + self.requests_mock.register_uri( + 'POST', url, json={'stack': fd_utils.get_dummy_stack()}, + headers=self.json_headers) + + instance_id = self.openstack.instantiate_vnf( + self.context, None, vnfd_dict, vim_connection_info, + inst_req_info, grant_response) + + self.assertEqual(uuidsentinel.instance_id, instance_id) + + def _responses_in_stack_list(self, instance_id, resources=None): + + resources = resources or [] + url = os.path.join(self.heat_url, 'stacks', instance_id, 'resources') + self.requests_mock.register_uri('GET', url, + json={'resources': resources}, headers=self.json_headers) + + response_list = [{'json': {'stacks': [fd_utils.get_dummy_stack( + attrs={'parent': uuidsentinel.instance_id})]}}, + {'json': {'stacks': [fd_utils.get_dummy_stack()]}}] + + url = os.path.join(self.heat_url, 'stacks?owner_id=' + + instance_id + '&show_nested=True') + self.requests_mock.register_uri('GET', url, response_list) + + def test_post_vnf_instantiation(self): + v_s_resource_info = fd_utils.get_virtual_storage_resource_info( + desc_id="storage1", set_resource_id=False) + + storage_resource_ids = [v_s_resource_info.id] + vnfc_resource_info = fd_utils.get_vnfc_resource_info(vdu_id="VDU_VNF", + storage_resource_ids=storage_resource_ids, set_resource_id=False) + + v_l_resource_info = fd_utils.get_virtual_link_resource_info( + vnfc_resource_info.vnfc_cp_info[0].vnf_link_port_id, + vnfc_resource_info.vnfc_cp_info[0].id) + + inst_vnf_info = fd_utils.get_vnf_instantiated_info( + virtual_storage_resource_info=[v_s_resource_info], + vnf_virtual_link_resource_info=[v_l_resource_info], + vnfc_resource_info=[vnfc_resource_info]) + + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + resources = [{'resource_name': vnfc_resource_info.vdu_id, + 'resource_type': vnfc_resource_info.compute_resource. + vim_level_resource_type, + 'physical_resource_id': uuidsentinel.vdu_resource_id}, + {'resource_name': v_s_resource_info.virtual_storage_desc_id, + 'resource_type': v_s_resource_info.storage_resource. + vim_level_resource_type, + 'physical_resource_id': uuidsentinel.storage_resource_id}, + {'resource_name': vnfc_resource_info.vnfc_cp_info[0].cpd_id, + 'resource_type': inst_vnf_info.vnf_virtual_link_resource_info[0]. + vnf_link_ports[0].resource_handle.vim_level_resource_type, + 'physical_resource_id': uuidsentinel.cp1_resource_id}] + + self._responses_in_stack_list(inst_vnf_info.instance_id, + resources=resources) + self.openstack.post_vnf_instantiation( + self.context, vnf_instance, vim_connection_info) + self.assertEqual(vnf_instance.instantiated_vnf_info. + vnfc_resource_info[0].metadata['stack_id'], + inst_vnf_info.instance_id) + + # Check if vnfc resource "VDU_VNF" is set with resource_id + self.assertEqual(uuidsentinel.vdu_resource_id, + vnf_instance.instantiated_vnf_info.vnfc_resource_info[0]. + compute_resource.resource_id) + + # Check if virtual storage resource "storage1" is set with resource_id + self.assertEqual(uuidsentinel.storage_resource_id, + vnf_instance.instantiated_vnf_info. + virtual_storage_resource_info[0].storage_resource.resource_id) + + # Check if virtual link port "CP1" is set with resource_id + self.assertEqual(uuidsentinel.cp1_resource_id, + vnf_instance.instantiated_vnf_info. + vnf_virtual_link_resource_info[0].vnf_link_ports[0]. + resource_handle.resource_id) + + def test_post_vnf_instantiation_with_ext_managed_virtual_link(self): + v_s_resource_info = fd_utils.get_virtual_storage_resource_info( + desc_id="storage1", set_resource_id=False) + + storage_resource_ids = [v_s_resource_info.id] + vnfc_resource_info = fd_utils.get_vnfc_resource_info(vdu_id="VDU_VNF", + storage_resource_ids=storage_resource_ids, set_resource_id=False) + + v_l_resource_info = fd_utils.get_virtual_link_resource_info( + vnfc_resource_info.vnfc_cp_info[0].vnf_link_port_id, + vnfc_resource_info.vnfc_cp_info[0].id, + desc_id='ExternalVL1') + + ext_managed_v_l_resource_info = \ + fd_utils.get_ext_managed_virtual_link_resource_info( + uuidsentinel.virtual_link_port_id, + uuidsentinel.vnfc_cp_info_id, + desc_id='ExternalVL1') + + inst_vnf_info = fd_utils.get_vnf_instantiated_info( + virtual_storage_resource_info=[v_s_resource_info], + vnf_virtual_link_resource_info=[v_l_resource_info], + vnfc_resource_info=[vnfc_resource_info], + ext_managed_virtual_link_info=[ext_managed_v_l_resource_info]) + + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vim_connection_info = fd_utils.get_vim_connection_info_object() + resources = [{'resource_name': vnfc_resource_info.vdu_id, + 'resource_type': vnfc_resource_info.compute_resource. + vim_level_resource_type, + 'physical_resource_id': uuidsentinel.vdu_resource_id}, + {'resource_name': v_s_resource_info.virtual_storage_desc_id, + 'resource_type': v_s_resource_info.storage_resource. + vim_level_resource_type, + 'physical_resource_id': uuidsentinel.storage_resource_id}, + {'resource_name': vnfc_resource_info.vnfc_cp_info[0].cpd_id, + 'resource_type': inst_vnf_info.vnf_virtual_link_resource_info[0]. + vnf_link_ports[0].resource_handle.vim_level_resource_type, + 'physical_resource_id': uuidsentinel.cp1_resource_id}, + {'resource_name': v_l_resource_info.vnf_virtual_link_desc_id, + 'resource_type': v_l_resource_info.network_resource. + vim_level_resource_type, + 'physical_resource_id': uuidsentinel.v_l_resource_info_id}] + self._responses_in_stack_list(inst_vnf_info.instance_id, + resources=resources) + self.openstack.post_vnf_instantiation( + self.context, vnf_instance, vim_connection_info) + self.assertEqual(vnf_instance.instantiated_vnf_info. + vnfc_resource_info[0].metadata['stack_id'], + inst_vnf_info.instance_id) + + # Check if vnfc resource "VDU_VNF" is set with resource_id + self.assertEqual(uuidsentinel.vdu_resource_id, + vnf_instance.instantiated_vnf_info.vnfc_resource_info[0]. + compute_resource.resource_id) + + # Check if virtual storage resource "storage1" is set with resource_id + self.assertEqual(uuidsentinel.storage_resource_id, + vnf_instance.instantiated_vnf_info. + virtual_storage_resource_info[0].storage_resource.resource_id) + + # Check if virtual link port "CP1" is set with resource_id + self.assertEqual(uuidsentinel.cp1_resource_id, + vnf_instance.instantiated_vnf_info. + vnf_virtual_link_resource_info[0].vnf_link_ports[0]. + resource_handle.resource_id) + + # Check if ext managed virtual link port is set with resource_id + self.assertEqual(uuidsentinel.cp1_resource_id, + vnf_instance.instantiated_vnf_info. + ext_managed_virtual_link_info[0].vnf_link_ports[0]. + resource_handle.resource_id) diff --git a/tacker/tosca/utils.py b/tacker/tosca/utils.py index 7435db7ae..c2aa84263 100644 --- a/tacker/tosca/utils.py +++ b/tacker/tosca/utils.py @@ -44,6 +44,10 @@ BLOCKSTORAGE_ATTACHMENT = 'tosca.nodes.BlockStorageAttachment' TOSCA_BINDS_TO = 'tosca.relationships.network.BindsTo' VDU = 'tosca.nodes.nfv.VDU' IMAGE = 'tosca.artifacts.Deployment.Image.VM' +ETSI_INST_LEVEL = 'tosca.policies.nfv.InstantiationLevels' +ETSI_SCALING_ASPECT = 'tosca.policies.nfv.ScalingAspects' +ETSI_SCALING_ASPECT_DELTA = 'tosca.policies.nfv.VduScalingAspectDeltas' +ETSI_INITIAL_DELTA = 'tosca.policies.nfv.VduInitialDelta' HEAT_SOFTWARE_CONFIG = 'OS::Heat::SoftwareConfig' OS_RESOURCES = { 'flavor': 'get_flavor_dict', @@ -106,7 +110,7 @@ def updateimports(template): else: template['imports'] = [defsfile] - if 'nfv' in template['tosca_definitions_version']: + if 'nfv' in template.get('tosca_definitions_version', {}): nfvfile = path + 'tacker_nfv_defs.yaml' template['imports'].append(nfvfile) @@ -482,7 +486,9 @@ def represent_odict(dump, tag, mapping, flow_style=None): @log.log def post_process_heat_template(heat_tpl, mgmt_ports, metadata, alarm_resources, res_tpl, vol_res={}, - unsupported_res_prop=None, unique_id=None): + unsupported_res_prop=None, unique_id=None, + inst_req_info=None, grant_info=None, + tosca=None): # # TODO(bobh) - remove when heat-translator can support literal strings. # @@ -547,6 +553,15 @@ def post_process_heat_template(heat_tpl, mgmt_ports, metadata, add_volume_resources(heat_dict, vol_res) if unsupported_res_prop: convert_unsupported_res_prop(heat_dict, unsupported_res_prop) + if grant_info: + convert_grant_info(heat_dict, grant_info) + if inst_req_info: + convert_inst_req_info(heat_dict, inst_req_info, tosca) + + if heat_dict.get('parameters') and heat_dict.get( + 'parameters', {}).get('vnfm_info'): + heat_dict.get('parameters').get('vnfm_info').update( + {'type': 'comma_delimited_list'}) yaml.SafeDumper.add_representer(OrderedDict, lambda dumper, value: represent_odict(dumper, @@ -555,6 +570,344 @@ def post_process_heat_template(heat_tpl, mgmt_ports, metadata, return yaml.safe_dump(heat_dict) +@log.log +def post_process_heat_template_for_scaling( + heat_tpl, mgmt_ports, metadata, + alarm_resources, res_tpl, vol_res={}, + unsupported_res_prop=None, unique_id=None, + inst_req_info=None, grant_info=None, + tosca=None): + heat_dict = yamlparser.simple_ordered_parse(heat_tpl) + if inst_req_info: + check_inst_req_info_for_scaling(heat_dict, inst_req_info) + convert_inst_req_info(heat_dict, inst_req_info, tosca) + if grant_info: + convert_grant_info(heat_dict, grant_info) + + yaml.SafeDumper.add_representer(OrderedDict, + lambda dumper, value: represent_odict(dumper, + u'tag:yaml.org,2002:map', value)) + return yaml.safe_dump(heat_dict) + + +@log.log +def check_inst_req_info_for_scaling(heat_dict, inst_req_info): + # Check whether fixed ip_address or mac_address is set in CP, + # because CP with fixed IP address cannot be scaled. + if not inst_req_info.ext_virtual_links: + return + + def _get_mac_ip(exp_cp): + mac = None + ip = None + for cp_conf in ext_cp.cp_config: + if cp_conf.cp_protocol_data is None: + continue + + for cp_protocol in cp_conf.cp_protocol_data: + if cp_protocol.ip_over_ethernet is None: + continue + + mac = cp_protocol.ip_over_ethernet.mac_address + for ip_address in \ + cp_protocol.ip_over_ethernet.ip_addresses: + if ip_address.fixed_addresses: + ip = ip_address.fixed_addresses + + return mac, ip + + for ext_vl in inst_req_info.ext_virtual_links: + ext_cps = ext_vl.ext_cps + for ext_cp in ext_cps: + if not ext_cp.cp_config: + continue + + mac, ip = _get_mac_ip(ext_cp) + + cp_resource = heat_dict['resources'].get(ext_cp.cpd_id) + if cp_resource is not None: + if mac or ip: + raise vnfm.InvalidInstReqInfoForScaling() + + +@log.log +def convert_inst_req_info(heat_dict, inst_req_info, tosca): + # Case which extVls is defined. + ext_vl_infos = inst_req_info.ext_virtual_links + if ext_vl_infos is not None: + for ext_vl in ext_vl_infos: + _convert_ext_vls(heat_dict, ext_vl) + + # Case which extMngVls is defined. + ext_mng_vl_infos = inst_req_info.ext_managed_virtual_links + + if ext_mng_vl_infos is not None: + for ext_mng_vl in ext_mng_vl_infos: + _convert_ext_mng_vl( + heat_dict, ext_mng_vl.vnf_virtual_link_desc_id, + ext_mng_vl.resource_id) + + # Check whether instLevelId is defined. + # Extract the initial number of scalable VDUs from the instantiation + # policy. + inst_level_id = inst_req_info.instantiation_level_id + + # The format of dict required to calculate desired_capacity are + # shown below. + # { aspectId: { deltaId: deltaNum }} + aspect_delta_dict = {} + # { aspectId: [ vduId ]} + aspect_vdu_dict = {} + # { instLevelId: { aspectId: levelNum }} + inst_level_dict = {} + # { aspectId: deltaId } + aspect_id_dict = {} + # { vduId: initialDelta } + vdu_delta_dict = {} + + tosca_policies = tosca.topology_template.policies + default_inst_level_id = _extract_policy_info( + tosca_policies, inst_level_dict, + aspect_delta_dict, aspect_id_dict, + aspect_vdu_dict, vdu_delta_dict) + + if inst_level_id is not None: + # Case which instLevelId is defined. + _convert_desired_capacity(inst_level_id, inst_level_dict, + aspect_delta_dict, aspect_id_dict, + aspect_vdu_dict, vdu_delta_dict, + heat_dict) + elif inst_level_id is None and default_inst_level_id is not None: + # Case which instLevelId is not defined. + # In this case, use the default instLevelId. + _convert_desired_capacity(default_inst_level_id, inst_level_dict, + aspect_delta_dict, aspect_id_dict, + aspect_vdu_dict, vdu_delta_dict, + heat_dict) + else: + LOG.info('Because instLevelId is not defined and ' + 'there is no default level in TOSCA, ' + 'the conversion of desired_capacity is skipped.') + + +@log.log +def convert_grant_info(heat_dict, grant_info): + # Case which grant_info is defined. + if not grant_info: + return + + for vdu_name, vnf_resources in grant_info.items(): + _convert_grant_info_vdu(heat_dict, vdu_name, vnf_resources) + + +def _convert_ext_vls(heat_dict, ext_vl): + ext_cps = ext_vl.ext_cps + vl_id = ext_vl.resource_id + defined_ext_link_ports = [ext_link_port.resource_handle.resource_id + for ext_link_port in ext_vl.ext_link_ports] + + def _replace_external_network_port(link_port_id, cpd_id): + for ext_link_port in ext_vl.ext_link_ports: + if ext_link_port.id == link_port_id: + if heat_dict['resources'].get(cpd_id) is not None: + _convert_ext_link_port(heat_dict, cpd_id, + ext_link_port.resource_handle.resource_id) + + for ext_cp in ext_cps: + cp_resource = heat_dict['resources'].get(ext_cp.cpd_id) + + if cp_resource is None: + return + # Update CP network properties to NEUTRON NETWORK-UUID + # defined in extVls. + cp_resource['properties']['network'] = vl_id + + # Check whether extLinkPorts is defined. + for cp_config in ext_cp.cp_config: + for cp_protocol in cp_config.cp_protocol_data: + # Update the following CP properties to the values defined + # in extVls. + # - subnet + # - ip_address + # - mac_address + ip_over_ethernet = cp_protocol.ip_over_ethernet + if ip_over_ethernet: + if ip_over_ethernet.mac_address or\ + ip_over_ethernet.ip_addresses: + if ip_over_ethernet.mac_address: + cp_resource['properties']['mac_address'] =\ + ip_over_ethernet.mac_address + if ip_over_ethernet.ip_addresses: + _convert_fixed_ips_list( + 'ip_address', + ip_over_ethernet.ip_addresses, + cp_resource) + elif defined_ext_link_ports: + _replace_external_network_port(cp_config.link_port_id, + ext_cp.cpd_id) + + +def _convert_fixed_ips_list(cp_key, cp_val, cp_resource): + for val in cp_val: + new_dict = {} + if val.fixed_addresses: + new_dict['ip_address'] = ''.join(val.fixed_addresses) + if val.subnet_id: + new_dict['subnet'] = val.subnet_id + + fixed_ips_list = cp_resource['properties'].get('fixed_ips') + + # Add if it doesn't exist yet. + if fixed_ips_list is None: + cp_resource['properties']['fixed_ips'] = [new_dict] + # Update if it already exists. + else: + for index, fixed_ips in enumerate(fixed_ips_list): + if fixed_ips.get(cp_key) is not None: + fixed_ips_list[index] = new_dict + else: + fixed_ips_list.append(new_dict) + sorted_list = sorted(fixed_ips_list) + cp_resource['properties']['fixed_ips'] = sorted_list + + +def _convert_ext_link_port(heat_dict, cp_name, ext_link_port): + # Delete CP resource and update VDU's properties + # related to CP defined in extLinkPorts. + del heat_dict['resources'][cp_name] + for rsrc_info in heat_dict['resources'].values(): + if rsrc_info['type'] == 'OS::Nova::Server': + vdu_networks = rsrc_info['properties']['networks'] + for index, vdu_network in enumerate(vdu_networks): + if isinstance(vdu_network['port'], dict) and\ + vdu_network['port'].get('get_resource') == cp_name: + new_dict = {'port': ext_link_port} + rsrc_info['properties']['networks'][index] = new_dict + + +def _convert_ext_mng_vl(heat_dict, vl_name, vl_id): + # Delete resources related to VL defined in extMngVLs. + if heat_dict['resources'].get(vl_name) is not None: + del heat_dict['resources'][vl_name] + del heat_dict['resources'][vl_name + '_subnet'] + del heat_dict['resources'][vl_name + '_qospolicy'] + del heat_dict['resources'][vl_name + '_bandwidth'] + + for rsrc_info in heat_dict['resources'].values(): + # Update CP's properties related to VL defined in extMngVls. + if rsrc_info['type'] == 'OS::Neutron::Port': + cp_network = rsrc_info['properties']['network'] + if isinstance(cp_network, dict) and\ + cp_network.get('get_resource') == vl_name: + rsrc_info['properties']['network'] = vl_id + # Update AutoScalingGroup's properties related to VL defined + # in extMngVls. + elif rsrc_info['type'] == 'OS::Heat::AutoScalingGroup': + asg_rsrc_props = \ + rsrc_info['properties']['resource'].get('properties') + for vl_key, vl_val in asg_rsrc_props.items(): + if vl_val.get('get_resource') == vl_name: + asg_rsrc_props[vl_key] = vl_id + + +def _extract_policy_info(tosca_policies, inst_level_dict, + aspect_delta_dict, aspect_id_dict, + aspect_vdu_dict, vdu_delta_dict): + default_inst_level_id = None + if tosca_policies is not []: + for p in tosca_policies: + if p.type == ETSI_SCALING_ASPECT_DELTA: + vdu_list = p.targets + aspect_id = p.properties['aspect'] + deltas = p.properties['deltas'] + delta_id_dict = {} + for delta_id, delta_val in deltas.items(): + delta_num = delta_val['number_of_instances'] + delta_id_dict[delta_id] = delta_num + aspect_delta_dict[aspect_id] = delta_id_dict + aspect_vdu_dict[aspect_id] = vdu_list + + elif p.type == ETSI_INST_LEVEL: + inst_levels = p.properties['levels'] + for level_id, inst_val in inst_levels.items(): + scale_info = inst_val['scale_info'] + aspect_level_dict = {} + for aspect_id, scale_level in scale_info.items(): + aspect_level_dict[aspect_id] = \ + scale_level['scale_level'] + inst_level_dict[level_id] = aspect_level_dict + default_inst_level_id = p.properties.get('default_level') + + # On TOSCA definitions, step_deltas is list and + # multiple description is possible, + # but only single description is supported. + # (first win) + # Like heat-translator. + elif p.type == ETSI_SCALING_ASPECT: + aspects = p.properties['aspects'] + for aspect_id, aspect_val in aspects.items(): + delta_names = aspect_val['step_deltas'] + delta_name = delta_names[0] + aspect_id_dict[aspect_id] = delta_name + + elif p.type == ETSI_INITIAL_DELTA: + vdus = p.targets + initial_delta = \ + p.properties['initial_delta']['number_of_instances'] + for vdu in vdus: + vdu_delta_dict[vdu] = initial_delta + return default_inst_level_id + + +def _convert_desired_capacity(inst_level_id, inst_level_dict, + aspect_delta_dict, aspect_id_dict, + aspect_vdu_dict, vdu_delta_dict, + heat_dict): + al_dict = inst_level_dict.get(inst_level_id) + if al_dict is not None: + # Get level_num. + for aspect_id, level_num in al_dict.items(): + delta_id = aspect_id_dict.get(aspect_id) + + # Get delta_num. + if delta_id is not None: + delta_num = \ + aspect_delta_dict.get(aspect_id).get(delta_id) + + # Get initial_delta. + vdus = aspect_vdu_dict.get(aspect_id) + initial_delta = None + for vdu in vdus: + initial_delta = vdu_delta_dict.get(vdu) + + if initial_delta is not None: + # Calculate desired_capacity. + desired_capacity = initial_delta + delta_num * level_num + # Convert desired_capacity on HOT. + for rsrc_key, rsrc_info in heat_dict['resources'].items(): + if rsrc_info['type'] == 'OS::Heat::AutoScalingGroup' and \ + rsrc_key == aspect_id: + rsrc_info['properties']['desired_capacity'] = \ + desired_capacity + else: + LOG.warning('Because target instLevelId is not defined in TOSCA, ' + 'the conversion of desired_capacity is skipped.') + pass + + +def _convert_grant_info_vdu(heat_dict, vdu_name, vnf_resources): + for vnf_resource in vnf_resources: + if vnf_resource.resource_type == "image": + # Update VDU's properties related to + # image defined in grant_info. + vdu_info = heat_dict.get('resources').get(vdu_name) + if vdu_info is not None: + vdu_props = vdu_info.get('properties') + if vdu_props.get('image') is None: + vdu_props.update({'image': + vnf_resource.resource_identifier}) + + @log.log def add_volume_resources(heat_dict, vol_res): # Add cinder volumes @@ -821,34 +1174,41 @@ def get_scaling_group_dict(ht_template, scaling_policy_names): return scaling_group_dict -def get_nested_resources_name(template): - for policy in template.policies: - if (policy.type_definition.is_derived_from(SCALING)): - nested_resource_name = policy.name + '_res.yaml' - return nested_resource_name +def get_nested_resources_name(hot): + nested_resource_names = [] + hot_yaml = yaml.safe_load(hot) + for r_key, r_val in hot_yaml.get('resources').items(): + if r_val.get('type') == 'OS::Heat::AutoScalingGroup': + nested_resource_name = r_val.get('properties', {}).get( + 'resource', {}).get('type', None) + nested_resource_names.append(nested_resource_name) + return nested_resource_names -def get_sub_heat_tmpl_name(template): - for policy in template.policies: - if (policy.type_definition.is_derived_from(SCALING)): - sub_heat_tmpl_name = policy.name + '_' + \ - uuidutils.generate_uuid() + '_res.yaml' - return sub_heat_tmpl_name +def get_sub_heat_tmpl_name(tmpl_name): + return uuidutils.generate_uuid() + tmpl_name def update_nested_scaling_resources(nested_resources, mgmt_ports, metadata, - res_tpl, unsupported_res_prop=None): + res_tpl, unsupported_res_prop=None, + grant_info=None, inst_req_info=None): nested_tpl = dict() - if nested_resources: - nested_resource_name, nested_resources_yaml =\ - list(nested_resources.items())[0] + for nested_resource_name, nested_resources_yaml in \ + nested_resources.items(): nested_resources_dict =\ yamlparser.simple_ordered_parse(nested_resources_yaml) if metadata.get('vdus'): for vdu_name, metadata_dict in metadata['vdus'].items(): if nested_resources_dict['resources'].get(vdu_name): - nested_resources_dict['resources'][vdu_name]['properties']['metadata'] = \ - metadata_dict + vdu_dict = nested_resources_dict['resources'][vdu_name] + vdu_dict['properties']['metadata'] = metadata_dict + convert_grant_info(nested_resources_dict, grant_info) + + # Replace external virtual links if specified in the inst_req_info + if inst_req_info is not None: + for ext_vl in inst_req_info.ext_virtual_links: + _convert_ext_vls(nested_resources_dict, ext_vl) + add_resources_tpl(nested_resources_dict, res_tpl) for res in nested_resources_dict["resources"].values(): if not res['type'] == HEAT_SOFTWARE_CONFIG: @@ -861,17 +1221,20 @@ def update_nested_scaling_resources(nested_resources, mgmt_ports, metadata, convert_unsupported_res_prop(nested_resources_dict, unsupported_res_prop) - for outputname, portname in mgmt_ports.items(): - ipval = {'get_attr': [portname, 'fixed_ips', 0, 'ip_address']} - output = {outputname: {'value': ipval}} - if 'outputs' in nested_resources_dict: - nested_resources_dict['outputs'].update(output) - else: - nested_resources_dict['outputs'] = output - LOG.debug(_('Added output for %s'), outputname) - yaml.SafeDumper.add_representer( - OrderedDict, lambda dumper, value: represent_odict( - dumper, u'tag:yaml.org,2002:map', value)) - nested_tpl[nested_resource_name] =\ - yaml.safe_dump(nested_resources_dict) + if mgmt_ports: + for outputname, portname in mgmt_ports.items(): + ipval = {'get_attr': [portname, 'fixed_ips', 0, 'ip_address']} + output = {outputname: {'value': ipval}} + if 'outputs' in nested_resources_dict: + nested_resources_dict['outputs'].update(output) + else: + nested_resources_dict['outputs'] = output + LOG.debug(_('Added output for %s'), outputname) + + yaml.SafeDumper.add_representer( + OrderedDict, lambda dumper, value: represent_odict( + dumper, u'tag:yaml.org,2002:map', value)) + nested_tpl[nested_resource_name] =\ + yaml.safe_dump(nested_resources_dict) + return nested_tpl diff --git a/tacker/vnflcm/__init__.py b/tacker/vnflcm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/vnflcm/abstract_driver.py b/tacker/vnflcm/abstract_driver.py new file mode 100644 index 000000000..9dc1a7633 --- /dev/null +++ b/tacker/vnflcm/abstract_driver.py @@ -0,0 +1,33 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class VnfInstanceAbstractDriver(object): + + @abc.abstractmethod + def instantiate_vnf(self, context, vnf_instance_id, instantiate_vnf_req): + """instantiate vnf request. + + :param context: context + :param vnf_instance_id: uuid of vnf_instance + :param instantiate_vnf_req: object of InstantiateVnfRequest + :return: None + """ + pass diff --git a/tacker/vnflcm/utils.py b/tacker/vnflcm/utils.py new file mode 100644 index 000000000..27e934875 --- /dev/null +++ b/tacker/vnflcm/utils.py @@ -0,0 +1,731 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import io +import os +import six +import yaml + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import uuidutils +from toscaparser import tosca_template + +from tacker.common import exceptions +from tacker.common import utils +from tacker.extensions import nfvo +from tacker import objects +from tacker.objects import fields +from tacker.tosca import utils as toscautils +from tacker.vnfm import vim_client + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +def _get_vim(context, vim_connection_info): + vim_client_obj = vim_client.VimClient() + + if vim_connection_info: + vim_id = vim_connection_info[0].vim_id + access_info = vim_connection_info[0].access_info + if access_info: + region_name = access_info.get('region') + else: + region_name = None + else: + vim_id = None + region_name = None + + try: + vim_res = vim_client_obj.get_vim( + context, vim_id, region_name=region_name) + except nfvo.VimNotFoundException: + raise exceptions.VimConnectionNotFound(vim_id=vim_id) + + vim_res['vim_auth'].update({'region': region_name}) + vim_info = {'id': vim_res['vim_id'], 'vim_id': vim_res['vim_id'], + 'vim_type': vim_res['vim_type'], + 'access_info': vim_res['vim_auth']} + + return vim_info + + +def _get_vnfd_dict(context, vnfd_id, flavour_id): + vnf_package_id = _get_vnf_package_id(context, vnfd_id) + vnf_package_base_path = cfg.CONF.vnf_package.vnf_package_csar_path + vnf_package_csar_path = vnf_package_base_path + '/' + vnf_package_id + vnfd_dict = _get_flavour_based_vnfd(vnf_package_csar_path, flavour_id) + + # Remove requirements from substitution mapping + vnfd_dict.get('topology_template').get( + 'substitution_mappings').pop('requirements') + return vnfd_dict + + +def _get_vnf_package_id(context, vnfd_id): + vnf_package = objects.VnfPackageVnfd.get_by_id(context, vnfd_id) + return vnf_package.package_uuid + + +def _create_grant_request(vnfd_dict, package_uuid): + node_templates = vnfd_dict.get('topology_template', + {}).get('node_templates', {}) + vnf_software_images = {} + if not node_templates: + return vnf_software_images + + def _build_vnf_software_image(sw_image_data, artifact_image_path): + vnf_sw_image = objects.VnfSoftwareImage() + vnf_sw_image.image_path = artifact_image_path + vnf_sw_image.name = sw_image_data.get('name') + vnf_sw_image.version = sw_image_data.get('version') + if sw_image_data.get('checksum'): + checksum = sw_image_data.get('checksum') + if checksum.get('algorithm'): + vnf_sw_image.algorithm = checksum.get('algorithm') + if checksum.get('hash'): + vnf_sw_image.hash = checksum.get('hash') + + vnf_sw_image.container_format = sw_image_data.get( + 'container_format') + vnf_sw_image.disk_format = sw_image_data.get('disk_format') + if sw_image_data.get('min_disk'): + min_disk = utils.MemoryUnit.convert_unit_size_to_num( + sw_image_data.get('min_disk'), 'GB') + vnf_sw_image.min_disk = min_disk + else: + vnf_sw_image.min_disk = 0 + + if sw_image_data.get('min_ram'): + min_ram = utils.MemoryUnit.convert_unit_size_to_num( + sw_image_data.get('min_ram'), 'MB') + vnf_sw_image.min_ram = min_ram + else: + vnf_sw_image.min_ram = 0 + + return vnf_sw_image + + def _get_image_path(artifact_image_path, package_uuid): + vnf_package_path = cfg.CONF.vnf_package.vnf_package_csar_path + artifact_image_path = os.path.join( + vnf_package_path, package_uuid, + artifact_image_path.split('../')[-1]) + return artifact_image_path + + for node, value in node_templates.items(): + if not value.get( + 'type') in ['tosca.nodes.nfv.Vdu.Compute', + 'tosca.nodes.nfv.Vdu.VirtualBlockStorage']: + continue + + sw_image_data = value.get('properties', {}).get('sw_image_data') + artifacts = value.get('artifacts', {}) + for artifact, sw_image in artifacts.items(): + artifact_image_path = None + if isinstance(sw_image, six.string_types): + artifact_image_path = sw_image + elif sw_image.get('type') == 'tosca.artifacts.nfv.SwImage': + artifact_image_path = sw_image.get('file', {}) + if sw_image_data and artifact_image_path: + is_url = utils.is_url(artifact_image_path) + if not is_url: + artifact_image_path = _get_image_path(artifact_image_path, + package_uuid) + + vnf_software_image = _build_vnf_software_image( + sw_image_data, artifact_image_path) + vnf_software_images[node] = vnf_software_image + break + + return vnf_software_images + + +def _make_final_vnf_dict(vnfd_dict, id, name, param_values): + return {'vnfd': { + 'attributes': { + 'vnfd': str(vnfd_dict)}}, + 'id': id, + 'name': name, + 'attributes': { + 'param_values': str(param_values), + 'stack_name': name or ("vnflcm_" + id)}} + + +def _get_flavour_based_vnfd(csar_path, flavour_id): + ext = [".yaml", ".yml"] + file_path_and_data = {} + imp_list = [] + for item in os.listdir(csar_path): + src_path = os.path.join(csar_path, item) + if os.path.isdir(src_path): + for file in os.listdir(src_path): + if file.endswith(tuple(ext)): + source_file_path = os.path.join(src_path, file) + with open(source_file_path) as file_obj: + data = yaml.safe_load(file_obj) + substitution_map = data.get( + 'topology_template', + {}).get('substitution_mappings', {}) + if substitution_map.get( + 'properties', {}).get('flavour_id') == flavour_id: + if data.get('imports'): + for imp in data.get('imports'): + imp_path = os.path.join(src_path, imp) + imp_list.append(imp_path) + data.update({'imports': imp_list}) + + return data + + elif src_path.endswith(tuple(ext)): + file_data = yaml.safe_load(io.open(src_path)) + substitution_map = file_data.get( + 'topology_template', {}).get('substitution_mappings', {}) + if substitution_map.get( + 'properties', {}).get('flavour_id') == flavour_id: + if file_data.get('imports'): + for imp in file_data.get('imports'): + imp_list.append(os.path.join(src_path, imp)) + file_data.update({'imports': imp_list}) + return file_data + + return file_path_and_data + + +def _get_param_data(vnfd_dict, instantiate_vnf_req): + param_value = {} + additional_param = instantiate_vnf_req.additional_params + if additional_param is None: + additional_param = {} + substitution_map = vnfd_dict.get('topology_template', + {}).get('substitution_mappings', {}) + input_attributes = vnfd_dict.get('topology_template', {}).get('inputs') + if substitution_map is not None: + subs_map_node_type = substitution_map.get('node_type') + import_paths = vnfd_dict.get('imports') + for imp_path in import_paths: + with open(imp_path) as file_obj: + import_data = yaml.safe_load(file_obj) + imp_node_type = import_data.get('node_types') + if imp_node_type: + for key, value in imp_node_type.items(): + if key == subs_map_node_type: + properties = value.get('properties') + for key, prop in properties.items(): + if additional_param.get(key): + param_value.update({ + key: additional_param.get(key)}) + else: + param_value.update({key: prop.get('default')}) + + for input_attr, value in input_attributes.items(): + if additional_param.get(input_attr): + param_value.update({input_attr: additional_param.get( + input_attr)}) + + return param_value + + +def _get_vim_connection_info_from_vnf_req(vnf_instance, instantiate_vnf_req): + vim_connection_obj_list = [] + + if not instantiate_vnf_req.vim_connection_info: + return vim_connection_obj_list + + for vim_connection in instantiate_vnf_req.vim_connection_info: + vim_conn = objects.VimConnectionInfo(id=vim_connection.id, + vim_id=vim_connection.vim_id, vim_type=vim_connection.vim_type, + access_info=vim_connection.access_info) + + vim_connection_obj_list.append(vim_conn) + + return vim_connection_obj_list + + +def _build_instantiated_vnf_info(vnfd_dict, instantiate_vnf_req, + vnf_instance, vim_id): + inst_vnf_info = vnf_instance.instantiated_vnf_info + inst_vnf_info.vnf_state = fields.VnfOperationalStateType.STARTED + inst_vnf_info.ext_cp_info = _set_ext_cp_info(instantiate_vnf_req) + inst_vnf_info.ext_virtual_link_info = _set_ext_virtual_link_info( + instantiate_vnf_req, inst_vnf_info.ext_cp_info) + + node_templates = vnfd_dict.get( + 'topology_template', {}).get('node_templates') + + vnfc_resource_info, virtual_storage_resource_info = \ + _get_vnfc_resource_info(vnfd_dict, instantiate_vnf_req, vim_id) + + inst_vnf_info.vnfc_resource_info = vnfc_resource_info + inst_vnf_info.virtual_storage_resource_info = \ + virtual_storage_resource_info + inst_vnf_info.vnf_virtual_link_resource_info = \ + _build_vnf_virtual_link_resource_info( + node_templates, instantiate_vnf_req, + inst_vnf_info.vnfc_resource_info, vim_id) + + inst_vnf_info.ext_managed_virtual_link_info = \ + _build_ext_managed_virtual_link_info(instantiate_vnf_req, + inst_vnf_info) + vnf_instance.instantiated_vnf_info = inst_vnf_info + + +def _get_compute_nodes(vnfd_dict, instantiate_vnf_req): + """Read the node templates and prepare VDU data in below format + + { + 'VDU1': { + 'CP': [CP1, CP2], + 'VIRTUAL_STORAGE': [virtual_storage1] + }, + } + """ + + node_templates = vnfd_dict.get( + 'topology_template', {}).get('node_templates') + + vdu_resources = {} + for key, value in node_templates.items(): + if value.get('type') != 'tosca.nodes.nfv.Vdu.Compute': + continue + + desired_capacity = _convert_desired_capacity( + instantiate_vnf_req.instantiation_level_id, vnfd_dict, key) + + cp_list = _get_cp_for_vdu(key, node_templates) + + virtual_storages = [] + requirements = value.get('requirements', []) + for requirement in requirements: + if requirement.get('virtual_storage'): + virtual_storages.append( + requirement.get('virtual_storage')) + + vdu_resources[key] = {"CP": cp_list, + "VIRTUAL_STORAGE": virtual_storages, + "COUNT": desired_capacity} + + return vdu_resources + + +def _get_virtual_link_nodes(node_templates): + virtual_link_nodes = {} + + for key, value in node_templates.items(): + if value.get('type') == 'tosca.nodes.nfv.VnfVirtualLink': + cp_list = _get_cp_for_vl(key, node_templates) + virtual_link_nodes[key] = cp_list + + return virtual_link_nodes + + +def _get_cp_for_vdu(vdu, node_templates): + cp_list = [] + for key, value in node_templates.items(): + if value.get('type') != 'tosca.nodes.nfv.VduCp': + continue + + requirements = value.get('requirements', []) + for requirement in requirements: + if requirement.get('virtual_binding') and vdu == \ + requirement.get('virtual_binding'): + cp_list.append(key) + + return cp_list + + +def _get_cp_for_vl(vl, node_templates): + cp_list = [] + for key, value in node_templates.items(): + if value.get('type') != 'tosca.nodes.nfv.VduCp': + continue + + requirements = value.get('requirements', []) + for requirement in requirements: + if requirement.get('virtual_link') and vl == \ + requirement.get('virtual_link'): + cp_list.append(key) + + return cp_list + + +def _build_vnf_virtual_link_resource_info(node_templates, instantiate_vnf_req, + vnfc_resource_info, vim_id): + virtual_link_nodes_with_cp = _get_virtual_link_nodes(node_templates) + + # Read the external networks and extcps from InstantiateVnfRequest + for ext_virt_link in instantiate_vnf_req.ext_virtual_links: + virtual_link_nodes_with_cp[ext_virt_link.id] = [extcp.cpd_id for extcp + in ext_virt_link.ext_cps] + + virtual_link_resource_info_list = [] + + def _get_network_resource(vl_node): + resource_handle = objects.ResourceHandle() + found = False + for ext_mg_vl in instantiate_vnf_req.ext_managed_virtual_links: + if ext_mg_vl.vnf_virtual_link_desc_id == vl_node: + resource_handle.resource_id = ext_mg_vl.resource_id + # TODO(tpatil): This cannot be set here. + resource_handle.vim_level_resource_type = \ + 'OS::Neutron::Net' + found = True + break + + if not found: + # check if it exists in the ext_virtual_links + for ext_virt_link in instantiate_vnf_req.ext_virtual_links: + if ext_virt_link.id == vl_node: + resource_handle.resource_id = ext_virt_link.resource_id + # TODO(tpatil): This cannot be set here. + resource_handle.vim_level_resource_type = \ + 'OS::Neutron::Net' + found = True + break + + return resource_handle + + def _get_vnf_link_port_info(cp): + vnf_link_port_info = objects.VnfLinkPortInfo() + vnf_link_port_info.id = uuidutils.generate_uuid() + + resource_handle = objects.ResourceHandle() + for ext_virt_link in instantiate_vnf_req.ext_virtual_links: + for extcp in ext_virt_link.ext_cps: + if extcp.cpd_id == cp: + for cpconfig in extcp.cp_config: + if cpconfig.link_port_id: + resource_handle.resource_id = \ + cpconfig.link_port_id + # TODO(tpatil): This shouldn't be set here. + resource_handle.vim_level_resource_type = \ + 'OS::Neutron::Port' + break + + vnf_link_port_info.resource_handle = resource_handle + + return vnf_link_port_info + + for node, cp_list in virtual_link_nodes_with_cp.items(): + vnf_vl_resource_info = objects.VnfVirtualLinkResourceInfo() + vnf_vl_resource_info.id = uuidutils.generate_uuid() + vnf_vl_resource_info.vnf_virtual_link_desc_id = node + vnf_vl_resource_info.network_resource = _get_network_resource(node) + + vnf_link_port_info_list = [] + for cp in cp_list: + for vnfc_resource in vnfc_resource_info: + for vnfc_cp in vnfc_resource.vnfc_cp_info: + if vnfc_cp.cpd_id == cp: + vnf_link_port_info = _get_vnf_link_port_info(cp) + vnf_link_port_info.cp_instance_id = vnfc_cp.id + # Identifier of the "vnfLinkPorts" structure in the + # "vnfVirtualLinkResourceInfo" structure. + vnfc_cp.vnf_link_port_id = vnf_link_port_info.id + vnf_link_port_info_list.append(vnf_link_port_info) + + vnf_vl_resource_info.vnf_link_ports = vnf_link_port_info_list + + virtual_link_resource_info_list.append(vnf_vl_resource_info) + + return virtual_link_resource_info_list + + +def _build_vnf_cp_info(instantiate_vnf_req, cp_list): + vnfc_cp_info_list = [] + + if not cp_list: + return vnfc_cp_info_list + + def _set_vnf_exp_cp_id_protocol_data(vnfc_cp_info): + for ext_virt_link in instantiate_vnf_req.ext_virtual_links: + for extcp in ext_virt_link.ext_cps: + if extcp.cpd_id == cp: + vnfc_cp_info.cp_protocol_info = \ + _set_cp_protocol_info(extcp) + for cpconfig in extcp.cp_config: + vnfc_cp_info.vnf_ext_cp_id = cpconfig.link_port_id + break + + for cp in cp_list: + vnfc_cp_info = objects.VnfcCpInfo() + vnfc_cp_info.id = uuidutils.generate_uuid() + vnfc_cp_info.cpd_id = cp + _set_vnf_exp_cp_id_protocol_data(vnfc_cp_info) + vnfc_cp_info_list.append(vnfc_cp_info) + + return vnfc_cp_info_list + + +def _build_virtual_storage_info(virtual_storages): + + for storage_node in virtual_storages: + virtual_storage = objects.VirtualStorageResourceInfo() + virtual_storage.id = uuidutils.generate_uuid() + virtual_storage.virtual_storage_desc_id = storage_node + + virtual_storage.storage_resource = objects.ResourceHandle() + + yield virtual_storage + + +def _get_vnfc_resource_info(vnfd_dict, instantiate_vnf_req, vim_id): + vdu_resources = _get_compute_nodes(vnfd_dict, instantiate_vnf_req) + vnfc_resource_info_list = [] + virtual_storage_resource_info_list = [] + + def _build_vnfc_resource_info(vdu, vdu_resource): + vnfc_resource_info = objects.VnfcResourceInfo() + vnfc_resource_info.id = uuidutils.generate_uuid() + vnfc_resource_info.vdu_id = vdu + + vnfc_resource_info.compute_resource = objects.ResourceHandle() + + vnfc_cp_info_list = _build_vnf_cp_info(instantiate_vnf_req, + vdu_resource.get("CP")) + vnfc_resource_info.vnfc_cp_info = vnfc_cp_info_list + + virtual_storages = vdu_resource.get("VIRTUAL_STORAGE") + vdu_storages = [] + for storage in _build_virtual_storage_info(virtual_storages): + vdu_storages.append(storage) + virtual_storage_resource_info_list.append(storage) + + storage_resource_ids = [info.id for info in vdu_storages] + vnfc_resource_info.storage_resource_ids = storage_resource_ids + return vnfc_resource_info + + for vdu, vdu_resource in vdu_resources.items(): + count = vdu_resource.get('COUNT', 1) + for num_instance in range(count): + vnfc_resource_info = _build_vnfc_resource_info(vdu, vdu_resource) + vnfc_resource_info_list.append(vnfc_resource_info) + + return vnfc_resource_info_list, virtual_storage_resource_info_list + + +def _set_ext_cp_info(instantiate_vnf_req): + ext_cp_info_list = [] + + if not instantiate_vnf_req.ext_virtual_links: + return ext_cp_info_list + + for ext_virt_link in instantiate_vnf_req.ext_virtual_links: + if not ext_virt_link.ext_cps: + continue + + for ext_cp in ext_virt_link.ext_cps: + ext_cp_info = objects.VnfExtCpInfo( + id=uuidutils.generate_uuid(), + cpd_id=ext_cp.cpd_id, + cp_protocol_info=_set_cp_protocol_info(ext_cp), + ext_link_port_id=_get_ext_link_port_id(ext_virt_link, + ext_cp.cpd_id)) + + ext_cp_info_list.append(ext_cp_info) + + return ext_cp_info_list + + +def _get_ext_link_port_id(ext_virtual_link, cpd_id): + if not ext_virtual_link.ext_link_ports: + return + + for ext_link in ext_virtual_link.ext_link_ports: + if ext_link.id == cpd_id: + return ext_link.id + + +def _build_ip_over_ethernet_address_info(cp_protocol_data): + """Convert IpOverEthernetAddressData to IpOverEthernetAddressInfo""" + + if not cp_protocol_data.ip_over_ethernet: + return + + ip_over_ethernet_add_info = objects.IpOverEthernetAddressInfo() + ip_over_ethernet_add_info.mac_address = \ + cp_protocol_data.ip_over_ethernet.mac_address + + if not cp_protocol_data.ip_over_ethernet.ip_addresses: + return ip_over_ethernet_add_info + + ip_address_list = [] + for ip_address in cp_protocol_data.ip_over_ethernet.ip_addresses: + ip_address_info = objects.vnf_instantiated_info.IpAddress( + type=ip_address.type, + addresses=ip_address.fixed_addresses, + is_dynamic=(False if ip_address.fixed_addresses else True), + subnet_id=ip_address.subnet_id) + + ip_address_list.append(ip_address_info) + + ip_over_ethernet_add_info.ip_addresses = ip_address_list + + return ip_over_ethernet_add_info + + +def _build_cp_protocol_info(cp_protocol_data): + ip_over_ethernet_add_info = _build_ip_over_ethernet_address_info( + cp_protocol_data) + cp_protocol_info = objects.CpProtocolInfo( + layer_protocol=cp_protocol_data.layer_protocol, + ip_over_ethernet=ip_over_ethernet_add_info) + + return cp_protocol_info + + +def _set_cp_protocol_info(ext_cp): + """Convert CpProtocolData to CpProtocolInfo""" + + cp_protocol_info_list = [] + if not ext_cp.cp_config: + return cp_protocol_info_list + + for cp_config in ext_cp.cp_config: + for cp_protocol_data in cp_config.cp_protocol_data: + cp_protocol_info = _build_cp_protocol_info(cp_protocol_data) + cp_protocol_info_list.append(cp_protocol_info) + + return cp_protocol_info_list + + +def _set_ext_virtual_link_info(instantiate_vnf_req, ext_cp_info): + ext_virtual_link_list = [] + + if not instantiate_vnf_req.ext_virtual_links: + return ext_virtual_link_list + + for ext_virtual_link in instantiate_vnf_req.ext_virtual_links: + res_handle = objects.ResourceHandle() + res_handle.resource_id = ext_virtual_link.resource_id + + ext_virtual_link_info = objects.ExtVirtualLinkInfo( + id=ext_virtual_link.id, + resource_handle=res_handle, + ext_link_ports=_set_ext_link_port(ext_virtual_link, + ext_cp_info)) + + ext_virtual_link_list.append(ext_virtual_link_info) + + return ext_virtual_link_list + + +def _set_ext_link_port(ext_virtual_links, ext_cp_info): + ext_link_port_list = [] + + if not ext_virtual_links.ext_link_ports: + return ext_link_port_list + + for ext_link_port in ext_virtual_links.ext_link_ports: + resource_handle = ext_link_port.resource_handle.obj_clone() + cp_instance_id = None + if ext_virtual_links.ext_cps: + for ext_cp in ext_cp_info: + cp_instance_id = ext_cp.id + + ext_link_port_info = objects.ExtLinkPortInfo(id=ext_link_port.id, + resource_handle=resource_handle, cp_instance_id=cp_instance_id) + + ext_link_port_list.append(ext_link_port_info) + + return ext_link_port_list + + +def _build_ext_managed_virtual_link_info(instantiate_vnf_req, inst_vnf_info): + + def _network_resource(ext_managed_vl): + resource_handle = objects.ResourceHandle( + resource_id=ext_managed_vl.resource_id) + # TODO(tpatil): Remove hard coding of resource type as + # OS::Neutron::Net resource type is specific to OpenStack infra + # driver. It could be different for other infra drivers like + # Kubernetes. + resource_handle.vim_level_resource_type = 'OS::Neutron::Net' + + return resource_handle + + ext_managed_virtual_link_info = [] + ext_managed_virt_link_from_req = \ + instantiate_vnf_req.ext_managed_virtual_links + for ext_managed_vl in ext_managed_virt_link_from_req: + ext_managed_virt_info = objects.ExtManagedVirtualLinkInfo() + ext_managed_virt_info.id = ext_managed_vl.id + ext_managed_virt_info.vnf_virtual_link_desc_id =\ + ext_managed_vl.vnf_virtual_link_desc_id + + ext_managed_virt_info.network_resource =\ + _network_resource(ext_managed_vl) + + # Populate the vnf_link_ports from vnf_virtual_link_resource_info + # of instantiated_vnf_info. + for vnf_vl_res_info in inst_vnf_info.vnf_virtual_link_resource_info: + if ext_managed_vl.vnf_virtual_link_desc_id ==\ + vnf_vl_res_info.vnf_virtual_link_desc_id: + vnf_link_ports = [] + for vnf_lp in vnf_vl_res_info.vnf_link_ports: + vnf_link_ports.append(vnf_lp.obj_clone()) + ext_managed_virt_info.vnf_link_ports = vnf_link_ports + + ext_managed_virtual_link_info.append(ext_managed_virt_info) + return ext_managed_virtual_link_info + + +def _convert_desired_capacity(inst_level_id, vnfd_dict, vdu): + aspect_delta_dict = {} + aspect_vdu_dict = {} + inst_level_dict = {} + aspect_id_dict = {} + vdu_delta_dict = {} + desired_capacity = 1 + + tosca = tosca_template.ToscaTemplate(parsed_params={}, a_file=False, + yaml_dict_tpl=vnfd_dict) + tosca_policies = tosca.topology_template.policies + default_inst_level_id = toscautils._extract_policy_info( + tosca_policies, inst_level_dict, + aspect_delta_dict, aspect_id_dict, + aspect_vdu_dict, vdu_delta_dict) + + if vdu_delta_dict.get(vdu) is None: + return desired_capacity + + if inst_level_id: + instantiation_level = inst_level_id + elif default_inst_level_id: + instantiation_level = default_inst_level_id + else: + return desired_capacity + + al_dict = inst_level_dict.get(instantiation_level) + + if not al_dict: + return desired_capacity + + for aspect_id, level_num in al_dict.items(): + delta_id = aspect_id_dict.get(aspect_id) + + if delta_id is not None: + delta_num = \ + aspect_delta_dict.get(aspect_id).get(delta_id) + + vdus = aspect_vdu_dict.get(aspect_id) + initial_delta = None + for vdu in vdus: + initial_delta = vdu_delta_dict.get(vdu) + + if initial_delta is not None: + desired_capacity = initial_delta + delta_num * level_num + + return desired_capacity diff --git a/tacker/vnflcm/vnflcm_driver.py b/tacker/vnflcm/vnflcm_driver.py new file mode 100644 index 000000000..6e278887c --- /dev/null +++ b/tacker/vnflcm/vnflcm_driver.py @@ -0,0 +1,153 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import encodeutils +from oslo_utils import excutils + +from tacker.common import log + +from tacker.common import driver_manager +from tacker.common import exceptions +from tacker import objects +from tacker.objects import fields +from tacker.vnflcm import abstract_driver +from tacker.vnflcm import utils as vnflcm_utils + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): + + def __init__(self): + super(VnfLcmDriver, self).__init__() + self._vnf_manager = driver_manager.DriverManager( + 'tacker.tacker.vnfm.drivers', + cfg.CONF.tacker.infra_driver) + + def _vnf_instance_update(self, context, vnf_instance, **kwargs): + """Update vnf instance in the database using kwargs as value.""" + + for k, v in kwargs.items(): + setattr(vnf_instance, k, v) + vnf_instance.save() + + def _instantiate_vnf(self, context, vnf_instance, vim_connection_info, + instantiate_vnf_req): + vnfd_dict = vnflcm_utils._get_vnfd_dict(context, vnf_instance.vnfd_id, + instantiate_vnf_req.flavour_id) + + param_for_subs_map = vnflcm_utils._get_param_data(vnfd_dict, + instantiate_vnf_req) + + package_uuid = vnflcm_utils._get_vnf_package_id(context, + vnf_instance.vnfd_id) + vnf_software_images = vnflcm_utils._create_grant_request(vnfd_dict, + package_uuid) + vnf_resources = self._vnf_manager.invoke( + vim_connection_info.vim_type, 'pre_instantiation_vnf', + context=context, vnf_instance=vnf_instance, + vim_connection_info=vim_connection_info, + vnf_software_images=vnf_software_images) + + # save the vnf resources in the db + for _, resources in vnf_resources.items(): + for vnf_resource in resources: + vnf_resource.create() + + vnfd_dict_to_create_final_dict = copy.deepcopy(vnfd_dict) + final_vnf_dict = vnflcm_utils._make_final_vnf_dict( + vnfd_dict_to_create_final_dict, vnf_instance.id, + vnf_instance.vnf_instance_name, param_for_subs_map) + + try: + instance_id = self._vnf_manager.invoke( + vim_connection_info.vim_type, 'instantiate_vnf', + context=context, vnf_instance=vnf_instance, + vnfd_dict=final_vnf_dict, grant_response=vnf_resources, + vim_connection_info=vim_connection_info, + instantiate_vnf_req=instantiate_vnf_req) + except Exception as exp: + with excutils.save_and_reraise_exception(): + exp.reraise = False + LOG.error("Unable to instantiate vnf instance " + "%(id)s due to error : %(error)s", + {"id": vnf_instance.id, "error": + encodeutils.exception_to_unicode(exp)}) + raise exceptions.VnfInstantiationFailed( + id=vnf_instance.id, + error=encodeutils.exception_to_unicode(exp)) + + vnf_instance.instantiated_vnf_info = objects.InstantiatedVnfInfo( + flavour_id=instantiate_vnf_req.flavour_id, + instantiation_level_id=instantiate_vnf_req.instantiation_level_id, + vnf_instance_id=vnf_instance.id, + instance_id=instance_id, + ext_cp_info=[]) + + try: + self._vnf_manager.invoke( + vim_connection_info.vim_type, 'create_wait', + plugin=self, context=context, + vnf_dict=final_vnf_dict, + vnf_id=final_vnf_dict['instance_id'], + auth_attr=vim_connection_info.access_info) + + except Exception as exp: + with excutils.save_and_reraise_exception(): + exp.reraise = False + LOG.error("Vnf creation wait failed for vnf instance " + "%(id)s due to error : %(error)s", + {"id": vnf_instance.id, "error": + encodeutils.exception_to_unicode(exp)}) + raise exceptions.VnfInstantiationWaitFailed( + id=vnf_instance.id, + error=encodeutils.exception_to_unicode(exp)) + + vnflcm_utils._build_instantiated_vnf_info(vnfd_dict, + instantiate_vnf_req, vnf_instance, vim_connection_info.vim_id) + + self._vnf_manager.invoke(vim_connection_info.vim_type, + 'post_vnf_instantiation', context=context, + vnf_instance=vnf_instance, + vim_connection_info=vim_connection_info) + + @log.log + def instantiate_vnf(self, context, vnf_instance, instantiate_vnf_req): + + vim_connection_info_list = vnflcm_utils.\ + _get_vim_connection_info_from_vnf_req(vnf_instance, + instantiate_vnf_req) + + self._vnf_instance_update(context, vnf_instance, + vim_connection_info=vim_connection_info_list) + + vim_info = vnflcm_utils._get_vim(context, + instantiate_vnf_req.vim_connection_info) + + vim_connection_info = objects.VimConnectionInfo.obj_from_primitive( + vim_info, context) + + self._instantiate_vnf(context, vnf_instance, vim_connection_info, + instantiate_vnf_req) + + self._vnf_instance_update(context, vnf_instance, + instantiation_state=fields.VnfInstanceState.INSTANTIATED, + task_state=None) diff --git a/tacker/vnfm/infra_drivers/abstract_driver.py b/tacker/vnfm/infra_drivers/abstract_driver.py index 514b5aac3..8c1daed9b 100644 --- a/tacker/vnfm/infra_drivers/abstract_driver.py +++ b/tacker/vnfm/infra_drivers/abstract_driver.py @@ -73,3 +73,32 @@ class VnfAbstractDriver(extensions.PluginInterface): @abc.abstractmethod def heal_vdu(self, plugin, context, vnf_dict, heal_request_data): pass + + @abc.abstractmethod + def pre_instantiation_vnf(self, context, vnf_instance, + vim_connection_info, vnf_software_images): + """Create resources required for instantiating Vnf. + + :param context: A RequestContext + :param vnf_instance: Object tacker.objects.VnfInstance + :vim_info: Credentials to initialize Vim connection + :vnf_software_images: Dict of key:value pair, + :tacker.objects.VnfSoftwareImage. + """ + pass + + @abc.abstractmethod + def delete_vnf_instance_resource(self, context, vnf_instance, + vim_connection_info, vnf_resource): + pass + + @abc.abstractmethod + def instantiate_vnf(self, context, vnf_instance, vnfd_dict, + vim_connection_info, instantiate_vnf_req, + grant_response): + pass + + @abc.abstractmethod + def post_vnf_instantiation(self, context, vnf_instance, + vim_connection_info): + pass diff --git a/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py b/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py index c1adc1eaa..9f0d0a93b 100644 --- a/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py +++ b/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py @@ -550,3 +550,20 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, def heal_vdu(self, plugin, context, vnf_dict, heal_request_data): pass + + def pre_instantiation_vnf(self, context, vnf_instance, + vim_connection_info, image_data): + raise NotImplementedError() + + def delete_vnf_instance_resource(self, context, vnf_instance, + vim_connection_info, vnf_resource): + raise NotImplementedError() + + def instantiate_vnf(self, context, vnf_instance, vnfd_dict, + vim_connection_info, instantiate_vnf_req, + grant_response): + raise NotImplementedError() + + def post_vnf_instantiation(self, context, vnf_instance, + vim_connection_info): + raise NotImplementedError() diff --git a/tacker/vnfm/infra_drivers/noop.py b/tacker/vnfm/infra_drivers/noop.py index aebda22d6..10afccf37 100644 --- a/tacker/vnfm/infra_drivers/noop.py +++ b/tacker/vnfm/infra_drivers/noop.py @@ -76,3 +76,20 @@ class VnfNoop(abstract_driver.VnfAbstractDriver): def heal_vdu(self, plugin, context, vnf_dict, heal_request_data): pass + + def pre_instantiation_vnf(self, context, vnf_instance, + vim_connection_info, image_data): + pass + + def delete_vnf_instance_resource(self, context, vnf_instance, + vim_connection_info, vnf_resource): + pass + + def instantiate_vnf(self, context, vnf_instance, vnfd_dict, + vim_connection_info, instantiate_vnf_req, + grant_response): + pass + + def post_vnf_instantiation(self, context, vnf_instance, + vim_connection_info): + pass diff --git a/tacker/vnfm/infra_drivers/openstack/glance_client.py b/tacker/vnfm/infra_drivers/openstack/glance_client.py new file mode 100644 index 000000000..a8e535c3b --- /dev/null +++ b/tacker/vnfm/infra_drivers/openstack/glance_client.py @@ -0,0 +1,63 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from oslo_log import log as logging + +from glanceclient import exc +from tacker.common import clients +from tacker.extensions import vnflcm + + +LOG = logging.getLogger(__name__) + + +class GlanceClient(object): + + def __init__(self, vim_connection_info, version=None): + super(GlanceClient, self).__init__() + self.connection = clients.OpenstackSdkConnection( + vim_connection_info, version).connection + + def create(self, name, **fields): + try: + return self.connection.image.create_image( + name, allow_duplicates=True, **fields) + except exc.HTTPException: + type_, value, tb = sys.exc_info() + raise vnflcm.GlanceClientException(msg=value) + + def delete(self, image_id): + try: + self.connection.image.delete_image(image_id) + except exc.HTTPNotFound: + LOG.warning("Image %(image)s created not found " + "at cleanup", {'image': image_id}) + + def import_image(self, image, web_path): + try: + self.connection.image.import_image( + image, method='web-download', uri=web_path) + except exc.HTTPException: + type_, value, tb = sys.exc_info() + raise vnflcm.GlanceClientException(msg=value) + + def get(self, image_id): + try: + return self.connection.image.get_image(image_id) + except exc.HTTPNotFound: + LOG.warning("Image %(image)s created not found ", + {'image': image_id}) diff --git a/tacker/vnfm/infra_drivers/openstack/heat_client.py b/tacker/vnfm/infra_drivers/openstack/heat_client.py index 13e0fe3ab..77ceaf576 100644 --- a/tacker/vnfm/infra_drivers/openstack/heat_client.py +++ b/tacker/vnfm/infra_drivers/openstack/heat_client.py @@ -29,6 +29,16 @@ class HeatClient(object): self.resource_types = self.heat.resource_types self.resources = self.heat.resources + def _stack_ids(self, stack_id): + filters = {"owner_id": stack_id, + "show_nested": True} + + for stack in self.stacks.list(**{"filters": filters}): + yield stack.id + if stack.parent and stack.parent == stack_id: + for x in self._stack_ids(stack.id): + yield x + def create(self, fields): fields = fields.copy() fields.update({ @@ -53,6 +63,13 @@ class HeatClient(object): def get(self, stack_id): return self.stacks.get(stack_id) + def get_stack_nested_depth(self, stack_id): + stack_ids = self._stack_ids(stack_id) + if stack_ids: + return len(list(stack_ids)) + + return 0 + def update(self, stack_id, **kwargs): try: return self.stacks.update(stack_id, **kwargs) diff --git a/tacker/vnfm/infra_drivers/openstack/openstack.py b/tacker/vnfm/infra_drivers/openstack/openstack.py index 2fc10e554..d106c550a 100644 --- a/tacker/vnfm/infra_drivers/openstack/openstack.py +++ b/tacker/vnfm/infra_drivers/openstack/openstack.py @@ -14,19 +14,26 @@ # License for the specific language governing permissions and limitations # under the License. + import time from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils +from oslo_utils import encodeutils +from oslo_utils import excutils import yaml from tacker._i18n import _ +from tacker.common import exceptions from tacker.common import log from tacker.common import utils +from tacker.extensions import vnflcm from tacker.extensions import vnfm +from tacker import objects from tacker.vnfm.infra_drivers import abstract_driver from tacker.vnfm.infra_drivers.openstack import constants as infra_cnst +from tacker.vnfm.infra_drivers.openstack import glance_client as gc from tacker.vnfm.infra_drivers.openstack import heat_client as hc from tacker.vnfm.infra_drivers.openstack import translate_template from tacker.vnfm.infra_drivers.openstack import vdu @@ -85,6 +92,8 @@ class OpenStack(abstract_driver.VnfAbstractDriver, super(OpenStack, self).__init__() self.STACK_RETRIES = cfg.CONF.openstack_vim.stack_retries self.STACK_RETRY_WAIT = cfg.CONF.openstack_vim.stack_retry_wait + self.IMAGE_RETRIES = 10 + self.IMAGE_RETRY_WAIT = 10 def get_type(self): return 'openstack' @@ -96,13 +105,14 @@ class OpenStack(abstract_driver.VnfAbstractDriver, return 'Openstack infra driver' @log.log - def create(self, plugin, context, vnf, auth_attr): + def create(self, plugin, context, vnf, auth_attr, + inst_req_info=None, grant_info=None): LOG.debug('vnf %s', vnf) - region_name = vnf.get('placement_attr', {}).get('region_name', None) heatclient = hc.HeatClient(auth_attr, region_name) - tth = translate_template.TOSCAToHOT(vnf, heatclient) + tth = translate_template.TOSCAToHOT(vnf, heatclient, + inst_req_info, grant_info) tth.generate_hot() stack = self._create_stack(heatclient, tth.vnf, tth.fields) return stack['stack']['id'] @@ -442,3 +452,347 @@ class OpenStack(abstract_driver.VnfAbstractDriver, except Exception: LOG.error("VNF '%s' failed to heal", vnf_dict['id']) raise vnfm.VNFHealFailed(vnf_id=vnf_dict['id']) + + @log.log + def pre_instantiation_vnf(self, context, vnf_instance, + vim_connection_info, vnf_software_images): + glance_client = gc.GlanceClient(vim_connection_info) + vnf_resources = {} + + def _roll_back_images(): + # Delete all previously created images for vnf + for key, resources in vnf_resources.items(): + for vnf_resource in resources: + try: + glance_client.delete(vnf_resource.resource_identifier) + except Exception: + LOG.error("Failed to delete image %(uuid)s " + "for vnf %(id)s", + {"uuid": vnf_resource.resource_identifier, + "id": vnf_instance.id}) + + for node_name, vnf_sw_image in vnf_software_images.items(): + name = vnf_sw_image.name + image_path = vnf_sw_image.image_path + is_url = utils.is_url(image_path) + + if not is_url: + filename = image_path + else: + filename = None + + try: + LOG.info("Creating image %(name)s for vnf %(id)s", + {"name": name, "id": vnf_instance.id}) + + image_data = {"min_disk": vnf_sw_image.min_disk, + "min_ram": vnf_sw_image.min_ram, + "disk_format": vnf_sw_image.disk_format, + "container_format": vnf_sw_image.container_format, + "visibility": "private"} + + if filename: + image_data.update({"filename": filename}) + + image = glance_client.create(name, **image_data) + + LOG.info("Image %(name)s created successfully for vnf %(id)s", + {"name": name, "id": vnf_instance.id}) + except Exception as exp: + with excutils.save_and_reraise_exception(): + exp.reraise = False + LOG.error("Failed to create image %(name)s for vnf %(id)s" + "due to error: %(error)s", + {"name": name, "id": vnf_instance.id, + "error": encodeutils.exception_to_unicode(exp)}) + + # Delete previously created images + _roll_back_images() + + raise exceptions.VnfPreInstantiationFailed( + id=vnf_instance.id, + error=encodeutils.exception_to_unicode(exp)) + try: + if is_url: + glance_client.import_image(image, image_path) + + self._image_create_wait(image.id, vnf_sw_image.hash, + glance_client, 'active', vnflcm.ImageCreateWaitFailed) + + vnf_resource = objects.VnfResource(context=context, + vnf_instance_id=vnf_instance.id, + resource_name=name, resource_type="image", + resource_status="CREATED", resource_identifier=image.id) + vnf_resources[node_name] = [vnf_resource] + except Exception as exp: + with excutils.save_and_reraise_exception(): + exp.reraise = False + LOG.error("Image %(name)s not active for vnf %(id)s" + "error: %(error)s", + {"name": name, "id": vnf_instance.id, + "error": encodeutils.exception_to_unicode(exp)}) + + err_msg = "Failed to delete image %(uuid)s for vnf %(id)s" + # Delete the image + try: + glance_client.delete(image.id) + except Exception: + LOG.error(err_msg, {"uuid": image.id, + "id": vnf_instance.id}) + + # Delete all previously created images for vnf + _roll_back_images() + + raise exceptions.VnfPreInstantiationFailed( + id=vnf_instance.id, + error=encodeutils.exception_to_unicode(exp)) + + return vnf_resources + + def _image_create_wait(self, image_uuid, hash_value, glance_client, + expected_status, exception_class): + retries = self.IMAGE_RETRIES + + while retries > 0: + retries = retries - 1 + image = glance_client.get(image_uuid) + status = image.status + if status == expected_status: + # NOTE(tpatil): If image is uploaded using import_image + # ,sdk doesn't validate checksum. So, verify checksum/hash + # for both scenarios upload from file and URL here. + if hash_value != image.hash_value: + msg = 'Image %s checksum verification failed' + raise Exception(msg % image_uuid) + + LOG.debug('Image status: %(image_uuid)s %(status)s', + {'image_uuid': image_uuid, 'status': status}) + return True + time.sleep(self.IMAGE_RETRY_WAIT) + LOG.debug('Image %(image_uuid)s status: %(status)', + {"image_uuid": image_uuid, "status": status}) + + if retries == 0 and image.status != expected_status: + error_reason = ("Image {image_uuid} could not get active " + "within {wait} seconds").format( + wait=(self.IMAGE_RETRIES * + self.IMAGE_RETRY_WAIT), + image_uuid=image_uuid) + raise exception_class(reason=error_reason) + + @log.log + def delete_vnf_instance_resource(self, context, vnf_instance, + vim_connection_info, vnf_resource): + LOG.info("Deleting resource '%(name)s' of type ' %(type)s' for vnf" + "%(id)s", {"type": vnf_resource.resource_type, + "name": vnf_resource.resource_name, + "id": vnf_instance.id}) + glance_client = gc.GlanceClient(vim_connection_info) + try: + glance_client.delete(vnf_resource.resource_identifier) + LOG.info("Deleted resource '%(name)s' of type ' %(type)s' for vnf" + "%(id)s", {"type": vnf_resource.resource_type, + "name": vnf_resource.resource_name, + "id": vnf_instance.id}) + except Exception: + LOG.info("Failed to delete resource '%(name)s' of type" + " %(type)s' for vnf %(id)s", + {"type": vnf_resource.resource_type, + "name": vnf_resource.resource_name, + "id": vnf_instance.id}) + + def instantiate_vnf(self, context, vnf_instance, vnfd_dict, + vim_connection_info, instantiate_vnf_req, + grant_response): + access_info = vim_connection_info.access_info + region_name = access_info.get('region') + placement_attr = vnfd_dict.get('placement_attr', {}) + placement_attr.update({'region_name': region_name}) + vnfd_dict['placement_attr'] = placement_attr + + instance_id = self.create(None, context, vnfd_dict, + access_info, inst_req_info=instantiate_vnf_req, + grant_info=grant_response) + vnfd_dict['instance_id'] = instance_id + return instance_id + + @log.log + def post_vnf_instantiation(self, context, vnf_instance, + vim_connection_info): + inst_vnf_info = vnf_instance.instantiated_vnf_info + access_info = vim_connection_info.access_info + + heatclient = hc.HeatClient(access_info, + region_name=access_info.get('region')) + stack_resources = self._get_stack_resources( + inst_vnf_info.instance_id, heatclient) + + self._update_vnfc_resources(vnf_instance, stack_resources) + + def _update_resource_handle(self, vnf_instance, resource_handle, + stack_resources, resource_name): + if not stack_resources: + LOG.warning("Failed to set resource handle for resource " + "%(resource)s for vnf %(id)s", {"resource": resource_name, + "id": vnf_instance.id}) + return + + resource_data = stack_resources.pop(resource_name, None) + if not resource_data: + LOG.warning("Failed to set resource handle for resource " + "%(resource)s for vnf %(id)s", + {"resource": resource_name, "id": vnf_instance.id}) + return + + resource_handle.resource_id = resource_data.get( + 'physical_resource_id') + resource_handle.vim_level_resource_type = resource_data.get( + 'resource_type') + + def _update_vnfc_resource_info(self, vnf_instance, vnfc_res_info, + stack_resources, update_network_resource=True): + inst_vnf_info = vnf_instance.instantiated_vnf_info + + def _pop_stack_resources(resource_name): + for stack_id, resources in stack_resources.items(): + if resource_name in resources.keys(): + return stack_id, resources + return None, {} + + def _populate_virtual_link_resource_info(vnf_virtual_link_desc_id, + pop_resources): + vnf_virtual_link_resource_info = \ + inst_vnf_info.vnf_virtual_link_resource_info + for vnf_vl_resource_info in vnf_virtual_link_resource_info: + if (vnf_vl_resource_info.vnf_virtual_link_desc_id != + vnf_virtual_link_desc_id): + continue + + vl_resource_data = pop_resources.pop( + vnf_virtual_link_desc_id, None) + if not vl_resource_data: + _, resources = _pop_stack_resources( + vnf_virtual_link_desc_id) + if not resources: + # NOTE(tpatil): network_resource is already set + # from the instantiatevnfrequest during instantiation. + continue + vl_resource_data = resources.get( + vnf_virtual_link_desc_id) + + resource_handle = vnf_vl_resource_info.network_resource + resource_handle.resource_id = \ + vl_resource_data.get('physical_resource_id') + resource_handle.vim_level_resource_type = \ + vl_resource_data.get('resource_type') + + def _populate_virtual_link_port(vnfc_cp_info, pop_resources): + vnf_virtual_link_resource_info = \ + inst_vnf_info.vnf_virtual_link_resource_info + for vnf_vl_resource_info in vnf_virtual_link_resource_info: + vl_link_port_found = False + for vl_link_port in vnf_vl_resource_info.vnf_link_ports: + if vl_link_port.cp_instance_id == vnfc_cp_info.id: + vl_link_port_found = True + self._update_resource_handle(vnf_instance, + vl_link_port.resource_handle, pop_resources, + vnfc_cp_info.cpd_id) + + if vl_link_port_found: + yield vnf_vl_resource_info.vnf_virtual_link_desc_id + + def _populate_virtual_storage(vnfc_resource_info, pop_resources): + virtual_storage_resource_info = inst_vnf_info. \ + virtual_storage_resource_info + for storage_id in vnfc_resource_info.storage_resource_ids: + for vir_storage_res_info in virtual_storage_resource_info: + if vir_storage_res_info.id == storage_id: + self._update_resource_handle(vnf_instance, + vir_storage_res_info.storage_resource, + pop_resources, + vir_storage_res_info.virtual_storage_desc_id) + break + + stack_id, pop_resources = _pop_stack_resources( + vnfc_res_info.vdu_id) + + self._update_resource_handle(vnf_instance, + vnfc_res_info.compute_resource, pop_resources, + vnfc_res_info.vdu_id) + + vnfc_res_info.metadata.update({"stack_id": stack_id}) + _populate_virtual_storage(vnfc_res_info, pop_resources) + + # Find out associated VLs, and CP used by vdu_id + virtual_links = set() + for vnfc_cp_info in vnfc_res_info.vnfc_cp_info: + for vl_desc_id in _populate_virtual_link_port(vnfc_cp_info, + pop_resources): + virtual_links.add(vl_desc_id) + + if update_network_resource: + for vl_desc_id in virtual_links: + _populate_virtual_link_resource_info(vl_desc_id, + pop_resources) + + def _update_ext_managed_virtual_link_ports(self, inst_vnf_info, + ext_managed_vl_info): + vnf_virtual_link_resource_info = \ + inst_vnf_info.vnf_virtual_link_resource_info + + def _update_link_port(vl_port): + for ext_vl_port in ext_managed_vl_info.vnf_link_ports: + if vl_port.id == ext_vl_port.id: + # Update the resource_id + ext_vl_port.resource_handle.resource_id =\ + vl_port.resource_handle.resource_id + ext_vl_port.resource_handle.vim_level_resource_type =\ + vl_port.resource_handle.vim_level_resource_type + break + + for vnf_vl_resource_info in vnf_virtual_link_resource_info: + if (vnf_vl_resource_info.vnf_virtual_link_desc_id != + ext_managed_vl_info.vnf_virtual_link_desc_id): + continue + + for vl_port in vnf_vl_resource_info.vnf_link_ports: + _update_link_port(vl_port) + + def _update_vnfc_resources(self, vnf_instance, stack_resources): + inst_vnf_info = vnf_instance.instantiated_vnf_info + for vnfc_res_info in inst_vnf_info.vnfc_resource_info: + self._update_vnfc_resource_info(vnf_instance, vnfc_res_info, + stack_resources) + + # update vnf_link_ports of ext_managed_virtual_link_info using already + # populated vnf_link_ports from vnf_virtual_link_resource_info. + for ext_mng_vl_info in inst_vnf_info.ext_managed_virtual_link_info: + self._update_ext_managed_virtual_link_ports(inst_vnf_info, + ext_mng_vl_info) + + def _get_stack_resources(self, stack_id, heatclient): + def _stack_ids(stack_id): + filters = { + "owner_id": stack_id, + "show_nested": True + } + yield stack_id + for stack in heatclient.stacks.list(**{"filters": filters}): + if stack.parent and stack.parent == stack_id: + for x in _stack_ids(stack.id): + yield x + + resource_details = {} + for id in _stack_ids(stack_id): + resources = {} + child_stack = False if id == stack_id else True + for stack_resource in heatclient.resources.list(id): + resource_data = {"resource_type": + stack_resource.resource_type, + "physical_resource_id": + stack_resource.physical_resource_id} + resources[stack_resource.resource_name] = resource_data + resource_details[id] = resources + resource_details[id].update({'child_stack': child_stack}) + + return resource_details diff --git a/tacker/vnfm/infra_drivers/openstack/translate_template.py b/tacker/vnfm/infra_drivers/openstack/translate_template.py index cb9db9097..d4754ae82 100644 --- a/tacker/vnfm/infra_drivers/openstack/translate_template.py +++ b/tacker/vnfm/infra_drivers/openstack/translate_template.py @@ -53,7 +53,7 @@ SCALING_POLICY = 'tosca.policies.tacker.Scaling' class TOSCAToHOT(object): """Convert TOSCA template to HOT template.""" - def __init__(self, vnf, heatclient): + def __init__(self, vnf, heatclient, inst_req_info=None, grant_info=None): self.vnf = vnf self.heatclient = heatclient self.attributes = {} @@ -65,18 +65,20 @@ class TOSCAToHOT(object): self.fields = None self.STACK_FLAVOR_EXTRA = cfg.CONF.openstack_vim.flavor_extra_specs self.appmonitoring_dict = None + self.grant_info = grant_info + self.inst_req_info = inst_req_info @log.log def generate_hot(self): self._get_vnfd() dev_attrs = self._update_fields() - vnfd_dict = yamlparser.simple_ordered_parse(self.vnfd_yaml) LOG.debug('vnfd_dict %s', vnfd_dict) self._get_unsupported_resource_props(self.heatclient) - self._generate_hot_from_tosca(vnfd_dict, dev_attrs) + self._generate_hot_from_tosca(vnfd_dict, dev_attrs, + self.inst_req_info, self.grant_info) self.fields['template'] = self.heat_template_yaml if not self.vnf['attributes'].get('heat_template'): self.vnf['attributes']['heat_template'] = self.fields['template'] @@ -249,7 +251,9 @@ class TOSCAToHOT(object): self.unsupported_props = unsupported_resource_props @log.log - def _generate_hot_from_tosca(self, vnfd_dict, dev_attrs): + def _generate_hot_from_tosca(self, vnfd_dict, dev_attrs, + inst_req_info=None, + grant_info=None): parsed_params = {} if 'param_values' in dev_attrs and dev_attrs['param_values'] != "": try: @@ -291,23 +295,28 @@ class TOSCAToHOT(object): self.vnf, tosca, metadata, unique_id=unique_id) monitoring_dict = toscautils.get_vdu_monitoring(tosca) mgmt_ports = toscautils.get_mgmt_ports(tosca) - nested_resource_name = toscautils.get_nested_resources_name(tosca) - sub_heat_tmpl_name = toscautils.get_sub_heat_tmpl_name(tosca) res_tpl = toscautils.get_resources_dict(tosca, self.STACK_FLAVOR_EXTRA) toscautils.post_process_template(tosca) scaling_policy_names = toscautils.get_scaling_policy(tosca) try: translator = tosca_translator.TOSCATranslator(tosca, parsed_params) + heat_template_yaml = translator.translate() - if nested_resource_name: - sub_heat_template_yaml =\ - translator.translate_to_yaml_files_dict(sub_heat_tmpl_name) - nested_resource_yaml =\ - sub_heat_template_yaml[nested_resource_name] - LOG.debug("nested_resource_yaml: %s", nested_resource_yaml) - self.nested_resources[nested_resource_name] =\ - nested_resource_yaml + nested_resource_names = toscautils.get_nested_resources_name( + heat_template_yaml) + if nested_resource_names: + for nested_resource_name in nested_resource_names: + sub_heat_tmpl_name = \ + toscautils.get_sub_heat_tmpl_name(nested_resource_name) + sub_heat_template_yaml =\ + translator.translate_to_yaml_files_dict( + sub_heat_tmpl_name) + nested_resource_yaml = \ + sub_heat_template_yaml[nested_resource_name] + LOG.debug("nested_resource_yaml: %s", nested_resource_yaml) + self.nested_resources[nested_resource_name] = \ + nested_resource_yaml except Exception as e: LOG.debug("heat-translator error: %s", str(e)) @@ -315,11 +324,13 @@ class TOSCAToHOT(object): if self.nested_resources: nested_tpl = toscautils.update_nested_scaling_resources( - self.nested_resources, mgmt_ports, metadata, - res_tpl, self.unsupported_props) + self.nested_resources, + mgmt_ports, metadata, res_tpl, self.unsupported_props, + grant_info=grant_info, inst_req_info=inst_req_info) self.fields['files'] = nested_tpl - self.vnf['attributes'][nested_resource_name] =\ - nested_tpl[nested_resource_name] + for nested_resource_name in nested_tpl.keys(): + self.vnf['attributes'][nested_resource_name] =\ + nested_tpl[nested_resource_name] mgmt_ports.clear() if scaling_policy_names: @@ -327,11 +338,25 @@ class TOSCAToHOT(object): heat_template_yaml, scaling_policy_names) self.vnf['attributes']['scaling_group_names'] =\ jsonutils.dump_as_bytes(scaling_group_dict) - heat_template_yaml = toscautils.post_process_heat_template( heat_template_yaml, mgmt_ports, metadata, alarm_resources, res_tpl, block_storage_details, self.unsupported_props, - unique_id=unique_id) + unique_id=unique_id, inst_req_info=inst_req_info, + grant_info=grant_info, tosca=tosca) + + try: + for nested_resource_name in self.nested_resources.keys(): + self.nested_resources[nested_resource_name] = \ + toscautils.post_process_heat_template_for_scaling( + self.nested_resources[nested_resource_name], + mgmt_ports, metadata, alarm_resources, + res_tpl, block_storage_details, self.unsupported_props, + unique_id=unique_id, inst_req_info=inst_req_info, + grant_info=grant_info, tosca=tosca) + except Exception as e: + LOG.debug("post_process_heat_template_for_scaling " + "error: %s", str(e)) + raise self.heat_template_yaml = heat_template_yaml self.monitoring_dict = monitoring_dict diff --git a/tacker/wsgi.py b/tacker/wsgi.py index bf765aa97..0981c3ce3 100644 --- a/tacker/wsgi.py +++ b/tacker/wsgi.py @@ -130,6 +130,12 @@ def expected_errors(errors): # Handle an authorized exception, will be # automatically converted to a HTTP 401. raise + elif isinstance(exc, exception.Conflict): + # Note(tpatil): Handle a conflict error, which + # happens due to resources in wrong state. + # ResourceExceptionHandler silently converts Conflict + # to HTTPConflict + raise LOG.exception("Unexpected exception in API method") msg = _('Unexpected API Error. Please report this at ' @@ -861,6 +867,9 @@ class ResourceExceptionHandler(object): raise Fault(exception.ConvertedException( code=ex_value.code, explanation=ex_value.format_message())) + elif isinstance(ex_value, exception.Conflict): + raise Fault(webob.exc.HTTPConflict( + explanation=ex_value.format_message())) elif isinstance(ex_value, TypeError): exc_info = (ex_type, ex_value, ex_traceback) LOG.error('Exception handling resource: %s', ex_value,