diff --git a/releasenotes/notes/support-individual-vnfc-management-7a942395ad7ca7de.yaml b/releasenotes/notes/support-individual-vnfc-management-7a942395ad7ca7de.yaml new file mode 100644 index 000000000..e3fbf7a4c --- /dev/null +++ b/releasenotes/notes/support-individual-vnfc-management-7a942395ad7ca7de.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Support individual VNFC management using OpenStack Heat. + New sample BaseHOT, corresponding UserData script, + and utility functions are added to UserData class + for v2 VNF Lifecycle management API. \ No newline at end of file diff --git a/tacker/sol_refactored/infra_drivers/openstack/heat_utils.py b/tacker/sol_refactored/infra_drivers/openstack/heat_utils.py index fbd65596b..cb34a5f85 100644 --- a/tacker/sol_refactored/infra_drivers/openstack/heat_utils.py +++ b/tacker/sol_refactored/infra_drivers/openstack/heat_utils.py @@ -94,7 +94,7 @@ class HeatClient(object): LOG.info("%s %s done.", operation, stack_name) raise loopingcall.LoopingCallDone() elif status in failed_status: - LOG.error("% %s failed.", operation, stack_name) + LOG.error("%s %s failed.", operation, stack_name) sol_title = "%s failed" % operation raise sol_ex.StackOperationFailed(sol_title=sol_title, sol_detail=status_reason) diff --git a/tacker/sol_refactored/infra_drivers/openstack/openstack.py b/tacker/sol_refactored/infra_drivers/openstack/openstack.py index c8e1f7507..14874f5e9 100644 --- a/tacker/sol_refactored/infra_drivers/openstack/openstack.py +++ b/tacker/sol_refactored/infra_drivers/openstack/openstack.py @@ -18,6 +18,7 @@ import json import os import pickle import subprocess +import yaml from dateutil import parser import eventlet @@ -67,6 +68,19 @@ def _make_combination_id(a, b): return '{}-{}'.format(a, b) +def _get_vdu_idx(vdu_with_idx): + part = vdu_with_idx.rpartition('-') + if part[1] == '': + return None + return int(part[2]) + + +def _vdu_with_idx(vdu, vdu_idx): + if vdu_idx is None: + return vdu + return f'{vdu}-{vdu_idx}' + + class Openstack(object): def __init__(self): @@ -75,7 +89,6 @@ class Openstack(object): def instantiate(self, req, inst, grant_req, grant, vnfd): # make HOT fields = self._make_hot(req, inst, grant_req, grant, vnfd) - LOG.debug("stack fields: %s", fields) # create or update stack vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) @@ -113,21 +126,27 @@ class Openstack(object): stack_name = heat_utils.get_stack_name(inst) heat_client.delete_stack(stack_name) - def _update_nfv_dict(self, heat_client, stack_name, fields): - parameters = heat_client.get_parameters(stack_name) - LOG.debug("ORIG parameters: %s", parameters) - # NOTE: parameters['nfv'] is string - orig_nfv_dict = json.loads(parameters.get('nfv', '{}')) - if 'nfv' in fields['parameters']: + def _update_fields(self, heat_client, stack_name, fields): + if 'nfv' in fields.get('parameters', {}): + parameters = heat_client.get_parameters(stack_name) + LOG.debug("ORIG parameters: %s", parameters) + # NOTE: parameters['nfv'] is string + orig_nfv_dict = json.loads(parameters.get('nfv', '{}')) fields['parameters']['nfv'] = inst_utils.json_merge_patch( orig_nfv_dict, fields['parameters']['nfv']) - LOG.debug("NEW parameters: %s", fields['parameters']) + + if 'template' in fields: + orig_template = heat_client.get_template(stack_name) + template = inst_utils.json_merge_patch( + orig_template, yaml.safe_load(fields['template'])) + fields['template'] = yaml.safe_dump(template) + + LOG.debug("NEW fields: %s", fields) return fields def scale(self, req, inst, grant_req, grant, vnfd): # make HOT fields = self._make_hot(req, inst, grant_req, grant, vnfd) - LOG.debug("stack fields: %s", fields) vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) heat_client = heat_utils.HeatClient(vim_info) @@ -139,17 +158,19 @@ class Openstack(object): if res_def.type == 'COMPUTE'] for vnfc in inst.instantiatedVnfInfo.vnfcResourceInfo: if vnfc.computeResource.resourceId in vnfc_res_ids: - if 'parent_stack_id' not in vnfc.metadata: + if 'parent_stack_id' in vnfc.metadata: + # AutoScalingGroup + heat_client.mark_unhealthy( + vnfc.metadata['parent_stack_id'], + vnfc.metadata['parent_resource_name']) + elif 'vdu_idx' not in vnfc.metadata: # It means definition of VDU in the BaseHOT # is inappropriate. raise sol_ex.UnexpectedParentResourceDefinition() - heat_client.mark_unhealthy( - vnfc.metadata['parent_stack_id'], - vnfc.metadata['parent_resource_name']) # update stack stack_name = heat_utils.get_stack_name(inst) - fields = self._update_nfv_dict(heat_client, stack_name, fields) + fields = self._update_fields(heat_client, stack_name, fields) heat_client.update_stack(stack_name, fields) # make instantiated_vnf_info @@ -158,6 +179,10 @@ class Openstack(object): def scale_rollback(self, req, inst, grant_req, grant, vnfd): # NOTE: rollback is supported for scale out only + # make HOT + fields = self._make_hot(req, inst, grant_req, grant, vnfd, + is_rollback=True) + vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) heat_client = heat_utils.HeatClient(vim_info) stack_name = heat_utils.get_stack_name(inst) @@ -169,19 +194,18 @@ class Openstack(object): for res in heat_utils.get_server_reses(heat_reses): if res['physical_resource_id'] not in vnfc_ids: metadata = self._make_vnfc_metadata(res, heat_reses) - if 'parent_stack_id' not in metadata: + if 'parent_stack_id' in metadata: + # AutoScalingGroup + heat_client.mark_unhealthy( + metadata['parent_stack_id'], + metadata['parent_resource_name']) + elif 'vdu_idx' not in metadata: # It means definition of VDU in the BaseHOT # is inappropriate. raise sol_ex.UnexpectedParentResourceDefinition() - heat_client.mark_unhealthy( - metadata['parent_stack_id'], - metadata['parent_resource_name']) # update (put back) 'desired_capacity' parameter - fields = self._update_nfv_dict(heat_client, stack_name, - userdata_default.DefaultUserData.scale_rollback( - req, inst, grant_req, grant, vnfd.csar_dir)) - + fields = self._update_fields(heat_client, stack_name, fields) heat_client.update_stack(stack_name, fields) # NOTE: instantiatedVnfInfo is not necessary to update since it @@ -190,13 +214,12 @@ class Openstack(object): def change_ext_conn(self, req, inst, grant_req, grant, vnfd): # make HOT fields = self._make_hot(req, inst, grant_req, grant, vnfd) - LOG.debug("stack fields: %s", fields) # update stack vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) heat_client = heat_utils.HeatClient(vim_info) stack_name = heat_utils.get_stack_name(inst) - fields = self._update_nfv_dict(heat_client, stack_name, fields) + fields = self._update_fields(heat_client, stack_name, fields) heat_client.update_stack(stack_name, fields) # make instantiated_vnf_info @@ -204,31 +227,33 @@ class Openstack(object): heat_client) def change_ext_conn_rollback(self, req, inst, grant_req, grant, vnfd): + # make HOT + fields = self._make_hot(req, inst, grant_req, grant, vnfd, + is_rollback=True) + + # update stack vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) heat_client = heat_utils.HeatClient(vim_info) stack_name = heat_utils.get_stack_name(inst) - fields = self._update_nfv_dict(heat_client, stack_name, - userdata_default.DefaultUserData.change_ext_conn_rollback( - req, inst, grant_req, grant, vnfd.csar_dir)) + fields = self._update_fields(heat_client, stack_name, fields) heat_client.update_stack(stack_name, fields) # NOTE: it is necessary to re-create instantiatedVnfInfo because # ports may be changed. self._make_instantiated_vnf_info(req, inst, grant_req, grant, vnfd, - heat_client) + heat_client, is_rollback=True) def heal(self, req, inst, grant_req, grant, vnfd): # make HOT # NOTE: _make_hot() is called as other operations, but it returns - # empty 'nfv' dict by default. Therefore _update_nfv_dict() returns + # empty 'nfv' dict by default. Therefore _update_fields() returns # current heat parameters as is. fields = self._make_hot(req, inst, grant_req, grant, vnfd) - LOG.debug("stack fields: %s", fields) vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) heat_client = heat_utils.HeatClient(vim_info) stack_name = heat_utils.get_stack_name(inst) - fields = self._update_nfv_dict(heat_client, stack_name, fields) + fields = self._update_fields(heat_client, stack_name, fields) # "re_create" is set to True only when SOL003 heal(without # vnfcInstanceId) and "all=True" in additionalParams. @@ -284,7 +309,6 @@ class Openstack(object): def change_vnfpkg(self, req, inst, grant_req, grant, vnfd): # make HOT fields = self._make_hot(req, inst, grant_req, grant, vnfd) - LOG.debug("stack fields: %s", fields) vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) heat_client = heat_utils.HeatClient(vim_info) @@ -298,13 +322,35 @@ class Openstack(object): # update stack stack_name = heat_utils.get_stack_name(inst) - fields = self._update_nfv_dict(heat_client, stack_name, fields) + fields = self._update_fields(heat_client, stack_name, fields) heat_client.update_stack(stack_name, fields) # make instantiated_vnf_info self._make_instantiated_vnf_info(req, inst, grant_req, grant, vnfd, heat_client) + def _get_flavor_from_vdu_dict(self, vnfc, vdu_dict): + vdu_name = _vdu_with_idx(vnfc.vduId, vnfc.metadata.get('vdu_idx')) + return vdu_dict.get(vdu_name, {}).get('computeFlavourId') + + def _get_images_from_vdu_dict(self, vnfc, storage_infos, vdu_dict): + vdu_idx = vnfc.metadata.get('vdu_idx') + vdu_name = _vdu_with_idx(vnfc.vduId, vdu_idx) + vdu_names = [vdu_name] + if vnfc.obj_attr_is_set('storageResourceIds'): + vdu_names += [ + _vdu_with_idx(storage_info.virtualStorageDescId, vdu_idx) + for storage_info in storage_infos + if storage_info.id in vnfc.storageResourceIds + ] + images = {} + for vdu_name in vdu_names: + image = vdu_dict.get(vdu_name, {}).get('vcImageId') + if image: + images[vdu_name] = image + + return images + def _change_vnfpkg_rolling_update(self, req, inst, grant_req, grant, vnfd, fields, heat_client, is_rollback): if not grant_req.obj_attr_is_set('removeResources'): @@ -348,30 +394,25 @@ class Openstack(object): LOG.debug("stack fields: %s", vdu_fields) heat_client.update_stack(parent_stack_id, vdu_fields) else: - # handle single VM - # pickup 'vcImageId' and 'computeFlavourId' + # pickup 'vcImageId' and 'computeFlavourId' from vdu_dict + vdu_name = _vdu_with_idx(vnfc.vduId, + vnfc.metadata.get('vdu_idx')) params = {} - if vdu_dict[vnfc.vduId].get('computeFlavourId'): - params[vnfc.vduId] = {'computeFlavourId': - vdu_dict[vnfc.vduId]['computeFlavourId']} - vdu_names = [vnfc.vduId] - if vnfc.obj_attr_is_set('storageResourceIds'): - storage_infos = ( - inst.instantiatedVnfInfo.virtualStorageResourceInfo) - vdu_names += [ - storage_info.virtualStorageDescId - for storage_info in storage_infos - if storage_info.id in vnfc.storageResourceIds - ] - for vdu_name in vdu_names: - image = vdu_dict.get(vdu_name, {}).get('vcImageId') - if image: - params.setdefault(vdu_name, {}) - params[vdu_name]['vcImageId'] = image + flavor = self._get_flavor_from_vdu_dict(vnfc, vdu_dict) + if flavor: + params[vdu_name] = {'computeFlavourId': flavor} + storage_infos = ( + inst.instantiatedVnfInfo.virtualStorageResourceInfo + if vnfc.obj_attr_is_set('storageResourceIds') else []) + images = self._get_images_from_vdu_dict(vnfc, + storage_infos, vdu_dict) + for vdu_name, image in images.items(): + params.setdefault(vdu_name, {}) + params[vdu_name]['vcImageId'] = image update_nfv_dict = {'nfv': {'VDU': params}} update_fields = {'parameters': update_nfv_dict} - update_fields = self._update_nfv_dict(heat_client, stack_name, + update_fields = self._update_fields(heat_client, stack_name, update_fields) LOG.debug("stack fields: %s", update_fields) heat_client.update_stack(stack_name, update_fields) @@ -429,8 +470,9 @@ class Openstack(object): def change_vnfpkg_rollback(self, req, inst, grant_req, grant, vnfd, lcmocc): - fields = userdata_default.DefaultUserData.change_vnfpkg_rollback( - req, inst, grant_req, grant, vnfd.csar_dir) + # make HOT + fields = self._make_hot(req, inst, grant_req, grant, vnfd, + is_rollback=True) vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) heat_client = heat_utils.HeatClient(vim_info) @@ -444,15 +486,15 @@ class Openstack(object): # stack update stack_name = heat_utils.get_stack_name(inst) - fields = self._update_nfv_dict(heat_client, stack_name, fields) + fields = self._update_fields(heat_client, stack_name, fields) heat_client.update_stack(stack_name, fields) # NOTE: it is necessary to re-create instantiatedVnfInfo because # some resources may be changed. self._make_instantiated_vnf_info(req, inst, grant_req, grant, vnfd, - heat_client) + heat_client, is_rollback=True) - def _make_hot(self, req, inst, grant_req, grant, vnfd): + def _make_hot(self, req, inst, grant_req, grant, vnfd, is_rollback=False): if grant_req.operation == v2fields.LcmOperationType.INSTANTIATE: flavour_id = req.flavourId else: @@ -470,10 +512,12 @@ class Openstack(object): 'lcm-operation-user-data-class') if userdata is None and userdata_class is None: - LOG.debug("Processing default userdata %s", grant_req.operation) + operation = grant_req.operation.lower() + if is_rollback: + operation = operation + '_rollback' + LOG.debug("Processing default userdata %s", operation) # NOTE: objects used here are dict compat. - method = getattr(userdata_default.DefaultUserData, - grant_req.operation.lower()) + method = getattr(userdata_default.DefaultUserData, operation) fields = method(req, inst, grant_req, grant, vnfd.csar_dir) elif userdata is None or userdata_class is None: # Both must be specified. @@ -488,7 +532,8 @@ class Openstack(object): 'vnf_instance': inst.to_dict(), 'grant_request': grant_req.to_dict(), 'grant_response': grant.to_dict(), - 'tmp_csar_dir': tmp_csar_dir + 'tmp_csar_dir': tmp_csar_dir, + 'is_rollback': is_rollback } script_path = os.path.join( os.path.dirname(__file__), "userdata_main.py") @@ -510,6 +555,8 @@ class Openstack(object): fields['timeout_mins'] = ( CONF.v2_vnfm.openstack_vim_stack_create_timeout) + LOG.debug("stack fields: %s", fields) + return fields def _get_checked_reses(self, nodes, reses): @@ -895,9 +942,16 @@ class Openstack(object): } parent_res = heat_utils.get_parent_resource(server_res, heat_reses) if parent_res: - metadata['parent_stack_id'] = ( - heat_utils.get_resource_stack_id(parent_res)) - metadata['parent_resource_name'] = parent_res['resource_name'] + parent_parent_res = heat_utils.get_parent_resource(parent_res, + heat_reses) + if (parent_parent_res and + parent_parent_res['resource_type'] == + 'OS::Heat::AutoScalingGroup'): + metadata['parent_stack_id'] = ( + heat_utils.get_resource_stack_id(parent_res)) + metadata['parent_resource_name'] = parent_res['resource_name'] + else: + metadata['vdu_idx'] = _get_vdu_idx(parent_res['resource_name']) return metadata @@ -917,22 +971,16 @@ class Openstack(object): if k.startswith('image-'): metadata[k] = v else: - properties = nfv_dict['VDU'][vnfc_res_info.vduId] - metadata['flavor'] = properties['computeFlavourId'] - vdu_names = [vnfc_res_info.vduId] - if vnfc_res_info.obj_attr_is_set('storageResourceIds'): - vdu_names += [ - storage_info.virtualStorageDescId - for storage_info in storage_infos - if storage_info.id in vnfc_res_info.storageResourceIds - ] - for vdu_name in vdu_names: - image = nfv_dict['VDU'].get(vdu_name, {}).get('vcImageId') - if image: - metadata[f'image-{vdu_name}'] = image + # assume it is found + metadata['flavor'] = self._get_flavor_from_vdu_dict( + vnfc_res_info, nfv_dict['VDU']) + images = self._get_images_from_vdu_dict(vnfc_res_info, + storage_infos, nfv_dict['VDU']) + for vdu_name, image in images.items(): + metadata[f'image-{vdu_name}'] = image def _make_instantiated_vnf_info(self, req, inst, grant_req, grant, vnfd, - heat_client): + heat_client, is_rollback=False): # get heat resources stack_name = heat_utils.get_stack_name(inst) heat_reses = heat_client.get_resources(stack_name) @@ -1029,7 +1077,8 @@ class Openstack(object): flavour_id, req, grant) else: old_inst_vnf_info = inst.instantiatedVnfInfo - if op == v2fields.LcmOperationType.CHANGE_EXT_CONN: + if (op == v2fields.LcmOperationType.CHANGE_EXT_CONN and + not is_rollback): ext_vl_infos = self._make_ext_vl_info_from_req_and_inst( req, grant, old_inst_vnf_info, ext_cp_infos) else: @@ -1107,7 +1156,9 @@ class Openstack(object): # grant request. def _get_key(vnfc): - return parser.isoparse(vnfc.metadata['creation_time']) + vdu_idx = vnfc.metadata.get('vdu_idx', 0) + creation_time = parser.isoparse(vnfc.metadata['creation_time']) + return (vdu_idx, creation_time) sorted_vnfc_res_infos = sorted(vnfc_res_infos, key=_get_key, reverse=True) diff --git a/tacker/sol_refactored/infra_drivers/openstack/userdata_main.py b/tacker/sol_refactored/infra_drivers/openstack/userdata_main.py index 76acb4317..f3d2b0de8 100644 --- a/tacker/sol_refactored/infra_drivers/openstack/userdata_main.py +++ b/tacker/sol_refactored/infra_drivers/openstack/userdata_main.py @@ -39,7 +39,10 @@ def main(): module = importlib.import_module(class_module) klass = getattr(module, userdata_class) - method = getattr(klass, grant_req['operation'].lower()) + operation = grant_req['operation'].lower() + if script_dict['is_rollback']: + operation = operation + '_rollback' + method = getattr(klass, operation) stack_dict = method(req, inst, grant_req, grant, tmp_csar_dir) pickle.dump(stack_dict, sys.stdout.buffer) diff --git a/tacker/sol_refactored/infra_drivers/openstack/userdata_standard.py b/tacker/sol_refactored/infra_drivers/openstack/userdata_standard.py new file mode 100644 index 000000000..a01429485 --- /dev/null +++ b/tacker/sol_refactored/infra_drivers/openstack/userdata_standard.py @@ -0,0 +1,544 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 +import yaml + +from tacker.sol_refactored.common import common_script_utils +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored.infra_drivers.openstack import userdata_utils + + +def add_idx(name, index): + return f'{name}-{index}' + + +def rm_idx(name_idx): + return name_idx.rpartition('-')[0] + + +def add_idx_to_vdu_template(vdu_template, vdu_idx): + """Add index to the third element of get_param + + ex. input VDU template: + --- + VDU1: + type: VDU1.yaml + properties: + flavor: { get_param: [ nfv, VDU, VDU1, computeFlavourId ] } + image-VDU1: { get_param: [ nfv, VDU, VDU1, vcImageId ] } + net1: { get_param: [ nfv, CP, VDU1_CP1, network ] } + --- + + output VDU template: + --- + VDU1: + type: VDU1.yaml + properties: + flavor: { get_param: [ nfv, VDU, VDU1-1, computeFlavourId ] } + image-VDU1: { get_param: [ nfv, VDU, VDU1-1, vcImageId ] } + net1: { get_param: [ nfv, CP, VDU1_CP1-1, network ] } + --- + """ + res = copy.deepcopy(vdu_template) + for prop_value in res.get('properties', {}).values(): + get_param = prop_value.get('get_param') + if (get_param is not None and + isinstance(get_param, list) and len(get_param) >= 4): + get_param[2] = add_idx(get_param[2], vdu_idx) + return res + + +class StandardUserData(userdata_utils.AbstractUserData): + + @staticmethod + def instantiate(req, inst, grant_req, grant, tmp_csar_dir): + vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) + flavour_id = req['flavourId'] + + hot_dict = vnfd.get_base_hot(flavour_id) + top_hot = hot_dict['template'] + + # first modify VDU resources + poped_vdu = {} + vdu_idxes = {} + for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): + poped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) + vdu_idxes[vdu_name] = 0 + + for res in grant_req['addResources']: + if res['type'] != 'COMPUTE': + continue + vdu_name = res['resourceTemplateId'] + if vdu_name not in poped_vdu: + continue + vdu_idx = vdu_idxes[vdu_name] + vdu_idxes[vdu_name] += 1 + res = add_idx_to_vdu_template(poped_vdu[vdu_name], vdu_idx) + top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res + + nfv_dict = common_script_utils.init_nfv_dict(top_hot) + + vdus = nfv_dict.get('VDU', {}) + for vdu_name, vdu_value in vdus.items(): + vdu_name = rm_idx(vdu_name) + if 'computeFlavourId' in vdu_value: + vdu_value['computeFlavourId'] = ( + common_script_utils.get_param_flavor( + vdu_name, flavour_id, vnfd, grant)) + if 'vcImageId' in vdu_value: + vdu_value['vcImageId'] = common_script_utils.get_param_image( + vdu_name, flavour_id, vnfd, grant) + if 'locationConstraints' in vdu_value: + vdu_value['locationConstraints'] = ( + common_script_utils.get_param_zone( + vdu_name, grant_req, grant)) + + cps = nfv_dict.get('CP', {}) + for cp_name, cp_value in cps.items(): + cp_name = rm_idx(cp_name) + if 'network' in cp_value: + cp_value['network'] = common_script_utils.get_param_network( + cp_name, grant, req) + if 'fixed_ips' in cp_value: + ext_fixed_ips = common_script_utils.get_param_fixed_ips( + cp_name, grant, req) + fixed_ips = [] + for i in range(len(ext_fixed_ips)): + if i not in cp_value['fixed_ips']: + break + ips_i = cp_value['fixed_ips'][i] + if 'subnet' in ips_i: + ips_i['subnet'] = ext_fixed_ips[i].get('subnet') + if 'ip_address' in ips_i: + ips_i['ip_address'] = ext_fixed_ips[i].get( + 'ip_address') + fixed_ips.append(ips_i) + cp_value['fixed_ips'] = fixed_ips + + common_script_utils.apply_ext_managed_vls(top_hot, req, grant) + + if 'nfv' in req.get('additionalParams', {}): + nfv_dict = inst_utils.json_merge_patch(nfv_dict, + req['additionalParams']['nfv']) + if 'nfv' in grant.get('additionalParams', {}): + nfv_dict = inst_utils.json_merge_patch(nfv_dict, + grant['additionalParams']['nfv']) + + fields = { + 'template': yaml.safe_dump(top_hot), + 'parameters': {'nfv': nfv_dict}, + 'files': {} + } + for key, value in hot_dict.get('files', {}).items(): + fields['files'][key] = yaml.safe_dump(value) + + return fields + + @staticmethod + def scale(req, inst, grant_req, grant, tmp_csar_dir): + if req['type'] == 'SCALE_OUT': + return StandardUserData._scale_out(req, inst, grant_req, grant, + tmp_csar_dir) + else: + return StandardUserData._scale_in(req, inst, grant_req, grant, + tmp_csar_dir) + + @staticmethod + def _scale_out(req, inst, grant_req, grant, tmp_csar_dir): + vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) + flavour_id = inst['instantiatedVnfInfo']['flavourId'] + + hot_dict = vnfd.get_base_hot(flavour_id) + top_hot = hot_dict['template'] + + # first modify VDU resources + poped_vdu = {} + vdu_idxes = {} + for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): + poped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) + vdu_idxes[vdu_name] = common_script_utils.get_current_capacity( + vdu_name, inst) + + for res in grant_req['addResources']: + if res['type'] != 'COMPUTE': + continue + vdu_name = res['resourceTemplateId'] + if vdu_name not in poped_vdu: + continue + vdu_idx = vdu_idxes[vdu_name] + vdu_idxes[vdu_name] += 1 + res = add_idx_to_vdu_template(poped_vdu[vdu_name], vdu_idx) + top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res + + nfv_dict = common_script_utils.init_nfv_dict(top_hot) + + vdus = nfv_dict.get('VDU', {}) + for vdu_name, vdu_value in vdus.items(): + vdu_name = rm_idx(vdu_name) + if 'computeFlavourId' in vdu_value: + vdu_value['computeFlavourId'] = ( + common_script_utils.get_param_flavor( + vdu_name, flavour_id, vnfd, grant)) + if 'vcImageId' in vdu_value: + vdu_value['vcImageId'] = common_script_utils.get_param_image( + vdu_name, flavour_id, vnfd, grant) + if 'locationConstraints' in vdu_value: + vdu_value['locationConstraints'] = ( + common_script_utils.get_param_zone( + vdu_name, grant_req, grant)) + + cps = nfv_dict.get('CP', {}) + for cp_name, cp_value in cps.items(): + cp_name = rm_idx(cp_name) + if 'network' in cp_value: + cp_value['network'] = ( + common_script_utils.get_param_network_from_inst( + cp_name, inst)) + if 'fixed_ips' in cp_value: + ext_fixed_ips = ( + common_script_utils.get_param_fixed_ips_from_inst( + cp_name, inst)) + fixed_ips = [] + for i in range(len(ext_fixed_ips)): + if i not in cp_value['fixed_ips']: + break + ips_i = cp_value['fixed_ips'][i] + if 'subnet' in ips_i: + ips_i['subnet'] = ext_fixed_ips[i].get('subnet') + if 'ip_address' in ips_i: + ips_i['ip_address'] = ext_fixed_ips[i].get( + 'ip_address') + fixed_ips.append(ips_i) + cp_value['fixed_ips'] = fixed_ips + + fields = { + 'template': yaml.safe_dump(top_hot), + 'parameters': {'nfv': nfv_dict} + } + + return fields + + @staticmethod + def _scale_in(req, inst, grant_req, grant, tmp_csar_dir): + vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) + flavour_id = inst['instantiatedVnfInfo']['flavourId'] + + vdu_nodes = vnfd.get_vdu_nodes(flavour_id) + template = {'resources': {}} + vdus = {} + cps = {} + for res in grant_req['removeResources']: + if res['type'] != 'COMPUTE': + continue + for inst_vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: + if (inst_vnfc['computeResource']['resourceId'] == + res['resource']['resourceId']): + # must be found + vdu_idx = inst_vnfc['metadata']['vdu_idx'] + break + vdu_name = res['resourceTemplateId'] + template['resources'][add_idx(vdu_name, vdu_idx)] = None + vdus[add_idx(vdu_name, vdu_idx)] = None + + for str_name in vnfd.get_vdu_storages(vdu_nodes[vdu_name]): + # may overspec, but no problem + vdus[add_idx(str_name, vdu_idx)] = None + + for cp_name in vnfd.get_vdu_cps(flavour_id, vdu_name): + # may overspec, but no problem + cps[add_idx(cp_name, vdu_idx)] = None + + fields = { + 'template': yaml.safe_dump(template), + 'parameters': {'nfv': {'VDU': vdus, 'CP': cps}} + } + + return fields + + @staticmethod + def scale_rollback(req, inst, grant_req, grant, tmp_csar_dir): + vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) + flavour_id = inst['instantiatedVnfInfo']['flavourId'] + + vdu_nodes = vnfd.get_vdu_nodes(flavour_id) + vdu_idxes = {} + for vdu_name in vdu_nodes.keys(): + vdu_idxes[vdu_name] = common_script_utils.get_current_capacity( + vdu_name, inst) + + template = {'resources': {}} + vdus = {} + cps = {} + for res in grant_req['addResources']: + if res['type'] != 'COMPUTE': + continue + vdu_name = res['resourceTemplateId'] + vdu_idx = vdu_idxes[vdu_name] + vdu_idxes[vdu_name] += 1 + template['resources'][add_idx(vdu_name, vdu_idx)] = None + vdus[add_idx(vdu_name, vdu_idx)] = None + + for str_name in vnfd.get_vdu_storages(vdu_nodes[vdu_name]): + # may overspec, but no problem + vdus[add_idx(str_name, vdu_idx)] = None + + for cp_name in vnfd.get_vdu_cps(flavour_id, vdu_name): + # may overspec, but no problem + cps[add_idx(cp_name, vdu_idx)] = None + + fields = { + 'template': yaml.safe_dump(template), + 'parameters': {'nfv': {'VDU': vdus, 'CP': cps}} + } + + return fields + + @staticmethod + def change_ext_conn(req, inst, grant_req, grant, tmp_csar_dir): + # change_ext_conn is interested in 'CP' only. + # This method returns only 'CP' part in the 'nfv' dict from + # ChangeExtVnfConnectivityRequest. + # It is applied to json merge patch against the existing 'nfv' + # dict by the caller. + # NOTE: complete 'nfv' dict can not be made at the moment + # since InstantiateVnfRequest is necessary to make it. + + vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) + flavour_id = inst['instantiatedVnfInfo']['flavourId'] + + hot_dict = vnfd.get_base_hot(flavour_id) + top_hot = hot_dict['template'] + + # first modify VDU resources + poped_vdu = {} + for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): + poped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) + + for inst_vnfc in inst['instantiatedVnfInfo'].get( + 'vnfcResourceInfo', []): + vdu_idx = inst_vnfc['metadata'].get('vdu_idx') + if vdu_idx is None: + continue + vdu_name = inst_vnfc['vduId'] + res = add_idx_to_vdu_template(poped_vdu[vdu_name], vdu_idx) + top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res + + nfv_dict = common_script_utils.init_nfv_dict(top_hot) + + cps = nfv_dict.get('CP', {}) + new_cps = {} + for cp_name_idx, cp_value in cps.items(): + cp_name = rm_idx(cp_name_idx) + if 'network' in cp_value: + network = common_script_utils.get_param_network( + cp_name, grant, req) + if network is None: + continue + new_cps.setdefault(cp_name_idx, {}) + new_cps[cp_name_idx]['network'] = network + if 'fixed_ips' in cp_value: + ext_fixed_ips = common_script_utils.get_param_fixed_ips( + cp_name, grant, req) + fixed_ips = [] + for i in range(len(ext_fixed_ips)): + if i not in cp_value['fixed_ips']: + break + ips_i = cp_value['fixed_ips'][i] + if 'subnet' in ips_i: + ips_i['subnet'] = ext_fixed_ips[i].get('subnet') + if 'ip_address' in ips_i: + ips_i['ip_address'] = ext_fixed_ips[i].get( + 'ip_address') + fixed_ips.append(ips_i) + new_cps.setdefault(cp_name_idx, {}) + new_cps[cp_name_idx]['fixed_ips'] = fixed_ips + + fields = {'parameters': {'nfv': {'CP': new_cps}}} + + return fields + + @staticmethod + def change_ext_conn_rollback(req, inst, grant_req, grant, tmp_csar_dir): + vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) + flavour_id = inst['instantiatedVnfInfo']['flavourId'] + + hot_dict = vnfd.get_base_hot(flavour_id) + top_hot = hot_dict['template'] + + # first modify VDU resources + poped_vdu = {} + for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): + poped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) + + for inst_vnfc in inst['instantiatedVnfInfo'].get( + 'vnfcResourceInfo', []): + vdu_idx = inst_vnfc['metadata'].get('vdu_idx') + if vdu_idx is None: + continue + vdu_name = inst_vnfc['vduId'] + res = add_idx_to_vdu_template(poped_vdu[vdu_name], vdu_idx) + top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res + + nfv_dict = common_script_utils.init_nfv_dict(top_hot) + + cps = nfv_dict.get('CP', {}) + new_cps = {} + for cp_name_idx, cp_value in cps.items(): + cp_name = rm_idx(cp_name_idx) + if 'network' in cp_value: + network = common_script_utils.get_param_network_from_inst( + cp_name, inst) + if network is None: + continue + new_cps.setdefault(cp_name_idx, {}) + new_cps[cp_name_idx]['network'] = network + if 'fixed_ips' in cp_value: + ext_fixed_ips = ( + common_script_utils.get_param_fixed_ips_from_inst( + cp_name, inst)) + fixed_ips = [] + for i in range(len(ext_fixed_ips)): + if i not in cp_value['fixed_ips']: + break + ips_i = cp_value['fixed_ips'][i] + if 'subnet' in ips_i: + ips_i['subnet'] = ext_fixed_ips[i].get('subnet') + if 'ip_address' in ips_i: + ips_i['ip_address'] = ext_fixed_ips[i].get( + 'ip_address') + fixed_ips.append(ips_i) + new_cps.setdefault(cp_name_idx, {}) + new_cps[cp_name_idx]['fixed_ips'] = fixed_ips + + fields = {'parameters': {'nfv': {'CP': new_cps}}} + + return fields + + @staticmethod + def heal(req, inst, grant_req, grant, tmp_csar_dir): + # It is not necessary to change parameters at heal basically. + + fields = {'parameters': {'nfv': {}}} + + return fields + + @staticmethod + def change_vnfpkg(req, inst, grant_req, grant, tmp_csar_dir): + vnfd = common_script_utils.get_vnfd(grant_req['dstVnfdId'], + tmp_csar_dir) + flavour_id = inst['instantiatedVnfInfo']['flavourId'] + + hot_dict = vnfd.get_base_hot(flavour_id) + top_hot = hot_dict['template'] + + # first modify VDU resources + poped_vdu = {} + for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): + poped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) + + for res in grant_req['removeResources']: + if res['type'] != 'COMPUTE': + continue + for inst_vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: + if (inst_vnfc['computeResource']['resourceId'] == + res['resource']['resourceId']): + # must be found + vdu_idx = inst_vnfc['metadata']['vdu_idx'] + break + vdu_name = res['resourceTemplateId'] + res = add_idx_to_vdu_template(poped_vdu[vdu_name], vdu_idx) + top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res + + nfv_dict = common_script_utils.init_nfv_dict(top_hot) + + vdus = nfv_dict.get('VDU', {}) + for vdu_name, vdu_value in vdus.items(): + vdu_name = rm_idx(vdu_name) + if 'computeFlavourId' in vdu_value: + vdu_value['computeFlavourId'] = ( + common_script_utils.get_param_flavor( + vdu_name, flavour_id, vnfd, grant)) + if 'vcImageId' in vdu_value: + vdu_value['vcImageId'] = common_script_utils.get_param_image( + vdu_name, flavour_id, vnfd, grant) + if 'locationConstraints' in vdu_value: + vdu_value.pop('locationConstraints') + + fields = { + 'parameters': {'nfv': {'VDU': vdus}} + } + + return fields + + @staticmethod + def change_vnfpkg_rollback(req, inst, grant_req, grant, tmp_csar_dir): + vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) + flavour_id = inst['instantiatedVnfInfo']['flavourId'] + + hot_dict = vnfd.get_base_hot(flavour_id) + top_hot = hot_dict['template'] + + # first modify VDU resources + poped_vdu = {} + for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): + poped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) + + for res in grant_req['removeResources']: + if res['type'] != 'COMPUTE': + continue + for inst_vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: + if (inst_vnfc['computeResource']['resourceId'] == + res['resource']['resourceId']): + # must be found + vdu_idx = inst_vnfc['metadata']['vdu_idx'] + break + vdu_name = res['resourceTemplateId'] + res = add_idx_to_vdu_template(poped_vdu[vdu_name], vdu_idx) + top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res + + nfv_dict = common_script_utils.init_nfv_dict(top_hot) + + images = {} + flavors = {} + for inst_vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: + vdu_idx = inst_vnfc['metadata'].get('vdu_idx') + if vdu_idx is None: + continue + vdu_name = inst_vnfc['vduId'] + if vdu_name in flavors: + continue + for key, value in inst_vnfc['metadata'].items(): + if key == 'flavor': + flavors[add_idx(vdu_name, vdu_idx)] = value + elif key.startswith('image-'): + image_vdu = key.replace('image-', '') + images[image_vdu] = value + + vdus = nfv_dict.get('VDU', {}) + new_vdus = {} + for vdu_name, vdu_value in vdus.items(): + if 'computeFlavourId' in vdu_value: + new_vdus.setdefault(vdu_name, {}) + new_vdus[vdu_name]['computeFlavourId'] = flavors.get(vdu_name) + if 'vcImageId' in vdu_value: + new_vdus.setdefault(vdu_name, {}) + new_vdus[vdu_name]['vcImageId'] = images.get(vdu_name) + + fields = { + 'parameters': {'nfv': {'VDU': new_vdus}} + } + + return fields diff --git a/tacker/sol_refactored/infra_drivers/openstack/userdata_utils.py b/tacker/sol_refactored/infra_drivers/openstack/userdata_utils.py index 341449df4..b534fe3b8 100644 --- a/tacker/sol_refactored/infra_drivers/openstack/userdata_utils.py +++ b/tacker/sol_refactored/infra_drivers/openstack/userdata_utils.py @@ -44,12 +44,32 @@ class AbstractUserData(metaclass=abc.ABCMeta): def scale(req, inst, grant_req, grant, tmp_csar_dir): raise sol_ex.UserDataClassNotImplemented() + @staticmethod + @abc.abstractmethod + def scale_rollback(req, inst, grant_req, grant, tmp_csar_dir): + raise sol_ex.UserDataClassNotImplemented() + @staticmethod @abc.abstractmethod def change_ext_conn(req, inst, grant_req, grant, tmp_csar_dir): raise sol_ex.UserDataClassNotImplemented() + @staticmethod + @abc.abstractmethod + def change_ext_conn_rollback(req, inst, grant_req, grant, tmp_csar_dir): + raise sol_ex.UserDataClassNotImplemented() + @staticmethod @abc.abstractmethod def heal(req, inst, grant_req, grant, tmp_csar_dir): raise sol_ex.UserDataClassNotImplemented() + + @staticmethod + @abc.abstractmethod + def change_vnfpkg(req, inst, grant_req, grant, tmp_csar_dir): + raise sol_ex.UserDataClassNotImplemented() + + @staticmethod + @abc.abstractmethod + def change_vnfpkg_rollback(req, inst, grant_req, grant, tmp_csar_dir): + raise sol_ex.UserDataClassNotImplemented() diff --git a/tacker/tests/functional/sol_v2/test_individual_vnfc_mgmt.py b/tacker/tests/functional/sol_v2/test_individual_vnfc_mgmt.py new file mode 100644 index 000000000..08f963035 --- /dev/null +++ b/tacker/tests/functional/sol_v2/test_individual_vnfc_mgmt.py @@ -0,0 +1,433 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 os +import time + +from tacker.tests.functional.sol_v2_common import paramgen +from tacker.tests.functional.sol_v2_common import test_vnflcm_basic_common + + +class IndividualVnfcMgmtTest(test_vnflcm_basic_common.CommonVnfLcmTest): + + @classmethod + def setUpClass(cls): + super(IndividualVnfcMgmtTest, cls).setUpClass() + cur_dir = os.path.dirname(__file__) + # tacker/tests/functional/sol_v2(here) + # /etc + image_dir = os.path.join( + cur_dir, "../../etc/samples/etsi/nfv/common/Files/images") + image_file = "cirros-0.5.2-x86_64-disk.img" + image_path = os.path.abspath(os.path.join(image_dir, image_file)) + + # tacker/tests/functional/sol_v2(here) + # /sol_refactored + userdata_dir = os.path.join( + cur_dir, "../../../sol_refactored/infra_drivers/openstack") + userdata_file = "userdata_standard.py" + userdata_path = os.path.abspath( + os.path.join(userdata_dir, userdata_file)) + + # main vnf package for StandardUserData test + pkg_path_1 = os.path.join(cur_dir, + "../sol_v2_common/samples/userdata_standard") + cls.vnf_pkg_1, cls.vnfd_id_1 = cls.create_vnf_package( + pkg_path_1, image_path=image_path, userdata_path=userdata_path) + + # for change_vnfpkg test + pkg_path_2 = os.path.join(cur_dir, + "../sol_v2_common/samples/userdata_standard_change_vnfpkg") + cls.vnf_pkg_2, cls.vnfd_id_2 = cls.create_vnf_package( + pkg_path_2, image_path=image_path, userdata_path=userdata_path) + + @classmethod + def tearDownClass(cls): + super(IndividualVnfcMgmtTest, cls).tearDownClass() + cls.delete_vnf_package(cls.vnf_pkg_1) + cls.delete_vnf_package(cls.vnf_pkg_2) + + def setUp(self): + super().setUp() + + def _put_fail_file(self, operation): + with open(f'/tmp/{operation}', 'w'): + pass + + def _rm_fail_file(self, operation): + os.remove(f'/tmp/{operation}') + + def _get_vdu_indexes(self, inst, vdu): + return { + vnfc['metadata'].get('vdu_idx') + for vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo'] + if vnfc['vduId'] == vdu + } + + def _get_vnfc_by_vdu_index(self, inst, vdu, index): + for vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: + if (vnfc['vduId'] == vdu and + vnfc['metadata'].get('vdu_idx') == index): + return vnfc + + def _get_vnfc_id(self, inst, vdu, index): + vnfc = self._get_vnfc_by_vdu_index(inst, vdu, index) + return vnfc['id'] + + def _get_vnfc_cp_net_id(self, inst, vdu, index, cp): + vnfc = self._get_vnfc_by_vdu_index(inst, vdu, index) + for cp_info in vnfc['vnfcCpInfo']: + if cp_info['cpdId'] == cp: + # must be found + ext_cp_id = cp_info['vnfExtCpId'] + break + for ext_vl in inst['instantiatedVnfInfo']['extVirtualLinkInfo']: + for port in ext_vl['extLinkPorts']: + if port['cpInstanceId'] == ext_cp_id: + # must be found + return ext_vl['resourceHandle']['resourceId'] + + def _get_vnfc_image(self, inst, vdu, index): + vnfc = self._get_vnfc_by_vdu_index(inst, vdu, index) + for key, value in vnfc['metadata'].items(): + if key.startswith('image-'): + # must be found + return value + + def _delete_instance(self, inst_id): + for _ in range(3): + resp, body = self.delete_vnf_instance(inst_id) + if resp.status_code == 204: # OK + return + elif resp.status_code == 409: + # may happen. there is a bit time between lcmocc become + # COMPLETED and lock of terminate is freed. + time.sleep(3) + else: + break + self.assertTrue(False) + + def test_basic_operations(self): + """Test basic operations using StandardUserData + + * Note: + This test focuses whether StandardUserData works well. + This test does not check overall items of APIs at all. + + * About LCM operations: + This test includes the following operations. + - Create VNF instance + - 1. Instantiate VNF instance + - Show VNF instance / check + - 2. Scale out operation + - Show VNF instance / check + - 3. Heal operation + - Show VNF instance / check + - 4. Scale in operation + - Show VNF instance / check + - 5. Change_ext_conn operation + - Show VNF instance / check + - 6. Change_vnfpkg operation + - Show VNF instance / check + - Terminate VNF instance + - Delete VNF instance + """ + + net_ids = self.get_network_ids(['net0', 'net1', 'net_mgmt']) + subnet_ids = self.get_subnet_ids(['subnet0', 'subnet1']) + + # Create VNF instance + create_req = paramgen.sample3_create(self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + inst_id = body['id'] + + # 1. Instantiate VNF instance + instantiate_req = paramgen.sample3_instantiate( + net_ids, subnet_ids, self.auth_url) + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Show VNF instance + resp, inst_1 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check number of VDUs and indexes + self.assertEqual({0}, self._get_vdu_indexes(inst_1, 'VDU1')) + self.assertEqual({0}, self._get_vdu_indexes(inst_1, 'VDU2')) + + # 2. Scale out operation + scale_out_req = paramgen.sample3_scale_out() + resp, body = self.scale_vnf_instance(inst_id, scale_out_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Show VNF instance + resp, inst_2 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check number of VDUs and indexes + self.assertEqual({0, 1, 2}, self._get_vdu_indexes(inst_2, 'VDU1')) + self.assertEqual({0}, self._get_vdu_indexes(inst_2, 'VDU2')) + + # 3. Heal operation + heal_req = paramgen.sample3_heal() + # pick up VDU1-1 to heal + vnfc_id = self._get_vnfc_id(inst_2, 'VDU1', 1) + heal_req['vnfcInstanceId'] = [f'VDU1-{vnfc_id}'] + resp, body = self.heal_vnf_instance(inst_id, heal_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Show VNF instance + resp, inst_3 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check id of VDU1-1 is changed and other are not. + self.assertEqual(self._get_vnfc_id(inst_2, 'VDU1', 0), + self._get_vnfc_id(inst_3, 'VDU1', 0)) + self.assertNotEqual(self._get_vnfc_id(inst_2, 'VDU1', 1), + self._get_vnfc_id(inst_3, 'VDU1', 1)) + self.assertEqual(self._get_vnfc_id(inst_2, 'VDU1', 2), + self._get_vnfc_id(inst_3, 'VDU1', 2)) + self.assertEqual(self._get_vnfc_id(inst_2, 'VDU2', 0), + self._get_vnfc_id(inst_3, 'VDU2', 0)) + + # 4. Scale in operation + scale_in_req = paramgen.sample3_scale_in() + resp, body = self.scale_vnf_instance(inst_id, scale_in_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Show VNF instance + resp, inst_4 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check VDU1-2 is removed. other are not changed. + self.assertEqual({0, 1}, self._get_vdu_indexes(inst_4, 'VDU1')) + self.assertEqual({0}, self._get_vdu_indexes(inst_4, 'VDU2')) + self.assertEqual(self._get_vnfc_id(inst_3, 'VDU1', 0), + self._get_vnfc_id(inst_4, 'VDU1', 0)) + self.assertEqual(self._get_vnfc_id(inst_3, 'VDU1', 1), + self._get_vnfc_id(inst_4, 'VDU1', 1)) + self.assertEqual(self._get_vnfc_id(inst_3, 'VDU2', 0), + self._get_vnfc_id(inst_4, 'VDU2', 0)) + + # 5. Change_ext_conn operation + change_ext_conn_req = paramgen.sample3_change_ext_conn(net_ids) + resp, body = self.change_ext_conn(inst_id, change_ext_conn_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Show VNF instance + resp, inst_5 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check VDU1_CP1 is changed to net0 from net1. other are not changed. + self.assertEqual(net_ids['net1'], + self._get_vnfc_cp_net_id(inst_4, 'VDU1', 0, 'VDU1_CP1')) + self.assertEqual(net_ids['net1'], + self._get_vnfc_cp_net_id(inst_4, 'VDU1', 1, 'VDU1_CP1')) + self.assertEqual(net_ids['net1'], + self._get_vnfc_cp_net_id(inst_4, 'VDU2', 0, 'VDU2_CP1')) + self.assertEqual(net_ids['net0'], + self._get_vnfc_cp_net_id(inst_5, 'VDU1', 0, 'VDU1_CP1')) + self.assertEqual(net_ids['net0'], + self._get_vnfc_cp_net_id(inst_5, 'VDU1', 1, 'VDU1_CP1')) + self.assertEqual(net_ids['net1'], + self._get_vnfc_cp_net_id(inst_5, 'VDU2', 0, 'VDU2_CP1')) + + # 6. Change_vnfpkg operation + change_vnfpkg_req = paramgen.sample4_change_vnfpkg(self.vnfd_id_2) + resp, body = self.change_vnfpkg(inst_id, change_vnfpkg_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Show VNF instance + resp, inst_6 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check vnfdId is changed + self.assertEqual(self.vnfd_id_2, inst_6['vnfdId']) + # check images are changed + self.assertNotEqual(self._get_vnfc_image(inst_5, 'VDU1', 0), + self._get_vnfc_image(inst_6, 'VDU1', 0)) + self.assertNotEqual(self._get_vnfc_image(inst_5, 'VDU1', 1), + self._get_vnfc_image(inst_6, 'VDU1', 1)) + self.assertNotEqual(self._get_vnfc_image(inst_5, 'VDU2', 0), + self._get_vnfc_image(inst_6, 'VDU2', 0)) + + # Terminate VNF instance + terminate_req = paramgen.sample4_terminate() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Delete VNF instance + self._delete_instance(inst_id) + + def test_rollback_operations(self): + """Test rollback operations using StandardUserData + + * Note: + This test focuses whether StandardUserData works well. + This test does not check overall items of APIs at all. + + * About LCM operations: + This test includes the following operations. + - Create VNF instance + - Instantiate VNF instance + - Show VNF instance + - 1. Scale out operation => FAILED_TEMP + - Rollback + - Show VNF instance / check + - 2. Change_ext_conn operation => FAILED_TEMP + - Rollback + - Show VNF instance / check + - 3. Change_vnfpkg operation => FAILED_TEMP + - Rollback + - Show VNF instance / check + - Terminate VNF instance + - Delete VNF instance + """ + + net_ids = self.get_network_ids(['net0', 'net1', 'net_mgmt']) + subnet_ids = self.get_subnet_ids(['subnet0', 'subnet1']) + + # Create VNF instance + create_req = paramgen.sample3_create(self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + inst_id = body['id'] + + # Instantiate VNF instance + instantiate_req = paramgen.sample3_instantiate( + net_ids, subnet_ids, self.auth_url) + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Show VNF instance + resp, inst_0 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # 1. Scale out operation + self._put_fail_file('scale_end') + scale_out_req = paramgen.sample3_scale_out() + resp, body = self.scale_vnf_instance(inst_id, scale_out_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_failed_temp(lcmocc_id) + self._rm_fail_file('scale_end') + + # Rollback + resp, body = self.rollback_lcmocc(lcmocc_id) + self.assertEqual(202, resp.status_code) + self.wait_lcmocc_rolled_back(lcmocc_id) + + # Show VNF instance + resp, inst_1 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check number of vnfc and its id are not changed. + self.assertEqual(self._get_vdu_indexes(inst_0, 'VDU1'), + self._get_vdu_indexes(inst_1, 'VDU1')) + self.assertEqual(self._get_vdu_indexes(inst_0, 'VDU2'), + self._get_vdu_indexes(inst_1, 'VDU2')) + self.assertEqual(self._get_vnfc_id(inst_0, 'VDU1', 0), + self._get_vnfc_id(inst_1, 'VDU1', 0)) + self.assertEqual(self._get_vnfc_id(inst_0, 'VDU2', 0), + self._get_vnfc_id(inst_1, 'VDU2', 0)) + + # 2. Change_ext_conn operation + self._put_fail_file('change_external_connectivity_end') + change_ext_conn_req = paramgen.sample3_change_ext_conn(net_ids) + resp, body = self.change_ext_conn(inst_id, change_ext_conn_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_failed_temp(lcmocc_id) + self._rm_fail_file('change_external_connectivity_end') + + # Rollback + resp, body = self.rollback_lcmocc(lcmocc_id) + self.assertEqual(202, resp.status_code) + self.wait_lcmocc_rolled_back(lcmocc_id) + + # Show VNF instance + resp, inst_2 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check network of extVL cps are not changed. (i.e. net1) + self.assertEqual(net_ids['net1'], + self._get_vnfc_cp_net_id(inst_2, 'VDU1', 0, 'VDU1_CP1')) + self.assertEqual(net_ids['net1'], + self._get_vnfc_cp_net_id(inst_2, 'VDU2', 0, 'VDU2_CP1')) + + # 3. Change_vnfpkg operation + self._put_fail_file('change_vnfpkg') + change_vnfpkg_req = paramgen.sample4_change_vnfpkg(self.vnfd_id_2) + resp, body = self.change_vnfpkg(inst_id, change_vnfpkg_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_failed_temp(lcmocc_id) + self._rm_fail_file('change_vnfpkg') + + # Rollback + resp, body = self.rollback_lcmocc(lcmocc_id) + self.assertEqual(202, resp.status_code) + self.wait_lcmocc_rolled_back(lcmocc_id) + + # Show VNF instance + resp, inst_3 = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # check vnfdId is not changed + self.assertEqual(self.vnfd_id_1, inst_3['vnfdId']) + # check images are not changed + self.assertEqual(self._get_vnfc_image(inst_2, 'VDU1', 0), + self._get_vnfc_image(inst_3, 'VDU1', 0)) + self.assertEqual(self._get_vnfc_image(inst_2, 'VDU2', 0), + self._get_vnfc_image(inst_3, 'VDU2', 0)) + + # Terminate VNF instance + terminate_req = paramgen.sample3_terminate() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # Delete VNF instance + self._delete_instance(inst_id) diff --git a/tacker/tests/functional/sol_v2_common/base_v2.py b/tacker/tests/functional/sol_v2_common/base_v2.py index 28c9f9527..0edf84fbc 100644 --- a/tacker/tests/functional/sol_v2_common/base_v2.py +++ b/tacker/tests/functional/sol_v2_common/base_v2.py @@ -107,11 +107,12 @@ class BaseSolV2Test(base.BaseTestCase): @classmethod def create_vnf_package(cls, sample_path, user_data={}, - image_path=None, nfvo=False): + image_path=None, nfvo=False, userdata_path=None): vnfd_id = uuidutils.generate_uuid() tmp_dir = tempfile.mkdtemp() - utils.make_zip(sample_path, tmp_dir, vnfd_id, image_path) + utils.make_zip(sample_path, tmp_dir, vnfd_id, image_path, + userdata_path) zip_file_name = os.path.basename(os.path.abspath(sample_path)) + ".zip" zip_file_path = os.path.join(tmp_dir, zip_file_name) diff --git a/tacker/tests/functional/sol_v2_common/paramgen.py b/tacker/tests/functional/sol_v2_common/paramgen.py index 75bac1951..c8bd9f212 100644 --- a/tacker/tests/functional/sol_v2_common/paramgen.py +++ b/tacker/tests/functional/sol_v2_common/paramgen.py @@ -933,3 +933,197 @@ def change_vnfpkg(vnfd_id): }] } } + + +# sample3 is used for tests of StandardUserData +# +def sample3_create(vnfd_id): + return { + "vnfdId": vnfd_id, + "vnfInstanceName": "sample3", + "vnfInstanceDescription": "test for StandardUserData" + } + + +def sample3_terminate(): + return { + "terminationType": "FORCEFUL" + } + + +def sample3_instantiate(net_ids, subnet_ids, auth_url): + ext_vl_1 = { + "id": uuidutils.generate_uuid(), + "resourceId": net_ids['net1'], + "extCps": [ + { + "cpdId": "VDU1_CP1", + "cpConfig": { + "VDU1_CP2_1": { + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + "ipAddresses": [{ + "type": "IPV4", + "numDynamicAddresses": 1}]}}]} + } + }, + { + "cpdId": "VDU2_CP1", + "cpConfig": { + "VDU2_CP2_1": { + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + "ipAddresses": [{ + "type": "IPV4", + "numDynamicAddresses": 1, + "subnetId": subnet_ids['subnet1']}]}}]} + } + } + ] + } + + return { + "flavourId": "simple", + "instantiationLevelId": "instantiation_level_1", + "extVirtualLinks": [ext_vl_1], + "extManagedVirtualLinks": [ + { + "id": uuidutils.generate_uuid(), + "vnfVirtualLinkDescId": "internalVL1", + "resourceId": net_ids['net_mgmt'] + }, + ], + "vimConnectionInfo": { + "vim1": { + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "vimId": uuidutils.generate_uuid(), + "interfaceInfo": {"endpoint": auth_url}, + "accessInfo": { + "username": "nfv_user", + "region": "RegionOne", + "password": "devstack", + "project": "nfv", + "projectDomain": "Default", + "userDomain": "Default" + } + } + }, + "additionalParams": { + "lcm-operation-user-data": "./UserData/userdata_standard.py", + "lcm-operation-user-data-class": "StandardUserData" + } + } + + +def sample3_scale_out(): + return { + "type": "SCALE_OUT", + "aspectId": "VDU1_scale", + "numberOfSteps": 2, + "additionalParams": { + "lcm-operation-user-data": "./UserData/userdata_standard.py", + "lcm-operation-user-data-class": "StandardUserData" + } + } + + +def sample3_scale_in(): + return { + "type": "SCALE_IN", + "aspectId": "VDU1_scale", + "numberOfSteps": 1, + "additionalParams": { + "lcm-operation-user-data": "./UserData/userdata_standard.py", + "lcm-operation-user-data-class": "StandardUserData" + } + } + + +def sample3_heal(): + return { + "vnfcInstanceId": [], # should be filled + "additionalParams": { + "lcm-operation-user-data": "./UserData/userdata_standard.py", + "lcm-operation-user-data-class": "StandardUserData" + } + } + + +def sample3_change_ext_conn(net_ids): + return { + "extVirtualLinks": [ + { + "id": uuidutils.generate_uuid(), + "resourceId": net_ids['net0'], + "extCps": [ + { + "cpdId": "VDU1_CP1", + "cpConfig": { + "VDU1_CP2_1": { + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + "ipAddresses": [{ + "type": "IPV4", + "numDynamicAddresses": 1}]}}]} + } + } + ] + } + ], + "additionalParams": { + "lcm-operation-user-data": "./UserData/userdata_standard.py", + "lcm-operation-user-data-class": "StandardUserData" + } + } + + +# sample4 is for change_vnfpkg test of StandardUserData +# +def sample4_change_vnfpkg(vnfd_id): + return { + "vnfdId": vnfd_id, + "additionalParams": { + "upgrade_type": "RollingUpdate", + "lcm-operation-coordinate-new-vnf": "./Scripts/coordinate_vnf.py", + "lcm-operation-coordinate-old-vnf": "./Scripts/coordinate_vnf.py", + "vdu_params": [ + { + "vdu_id": "VDU1", + "old_vnfc_param": { + "cp_name": "VDU1_CP1", + "username": "ubuntu", + "password": "ubuntu" + }, + "new_vnfc_param": { + "cp_name": "VDU1_CP1", + "username": "ubuntu", + "password": "ubuntu" + } + }, + { + "vdu_id": "VDU2", + "old_vnfc_param": { + "cp_name": "VDU2_CP1", + "username": "ubuntu", + "password": "ubuntu" + }, + "new_vnfc_param": { + "cp_name": "VDU2_CP1", + "username": "ubuntu", + "password": "ubuntu" + } + } + ], + "lcm-operation-user-data": "./UserData/userdata_standard.py", + "lcm-operation-user-data-class": "StandardUserData" + } + } + + +def sample4_terminate(): + return { + "terminationType": "FORCEFUL" + } diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/nested/VDU1.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/nested/VDU1.yaml new file mode 100644 index 000000000..c0fdb03bb --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/nested/VDU1.yaml @@ -0,0 +1,53 @@ +heat_template_version: 2013-05-23 +description: 'VDU1 HOT for Sample VNF' + +parameters: + flavor: + type: string + image-VDU1: + type: string + net1: + type: string + net2: + type: string + net3: + type: string + affinity: + type: string + +resources: + VDU1: + type: OS::Nova::Server + properties: + flavor: { get_param: flavor } + name: VDU1 + image: { get_param: image-VDU1 } + networks: + - port: + get_resource: VDU1_CP1 +# replace the following line to Port ID when extmanagedVLs' Ports are +# specified in instantiatevnfrequest + - port: + get_resource: VDU1_CP2 + - port: + get_resource: VDU1_CP3 + scheduler_hints: + group: { get_param: affinity } + +# extVL without FixedIP or with numDynamicAddresses + VDU1_CP1: + type: OS::Neutron::Port + properties: + network: { get_param: net1 } + +# CPs of internal VLs are deleted when extmanagedVLs and port are +# specified in instantiatevnfrequest + VDU1_CP2: + type: OS::Neutron::Port + properties: + network: { get_param: net2 } + + VDU1_CP3: + type: OS::Neutron::Port + properties: + network: { get_param: net3 } diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/nested/VDU2.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/nested/VDU2.yaml new file mode 100644 index 000000000..ee3415333 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/nested/VDU2.yaml @@ -0,0 +1,69 @@ +heat_template_version: 2013-05-23 +description: 'VDU2 HOT for Sample VNF' + +parameters: + flavor: + type: string + image-VDU2-VirtualStorage: + type: string + net1: + type: string + net2: + type: string + net3: + type: string + subnet1: + type: string + affinity: + type: string + +resources: + VDU2: + type: OS::Nova::Server + properties: + flavor: { get_param: flavor } + name: VDU2 + block_device_mapping_v2: [{"volume_id": { get_resource: VDU2-VirtualStorage }}] + networks: + - port: + get_resource: VDU2_CP1 +# replace the following line to Port ID when extmanagedVLs' Ports are +# specified in instantiatevnfrequest + - port: + get_resource: VDU2_CP2 + - port: + get_resource: VDU2_CP3 + scheduler_hints: + group: {get_param: affinity } + + VDU2-VirtualStorage: + type: OS::Cinder::Volume + properties: + image: { get_param: image-VDU2-VirtualStorage } + size: 1 + volume_type: { get_resource: multi } + multi: + type: OS::Cinder::VolumeType + properties: + name: VDU2-multi + metadata: { multiattach: " True" } + +# extVL with numDynamicAddresses and subnet + VDU2_CP1: + type: OS::Neutron::Port + properties: + network: { get_param: net1 } + fixed_ips: + - subnet: { get_param: subnet1 } + +# CPs of internal VLs are deleted when extmanagedVLs and port are +# specified in instantiatevnfrequest + VDU2_CP2: + type: OS::Neutron::Port + properties: + network: { get_param: net2 } + + VDU2_CP3: + type: OS::Neutron::Port + properties: + network: { get_param: net3 } diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/sample3.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/sample3.yaml new file mode 100644 index 000000000..bad8030ac --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/BaseHOT/simple/sample3.yaml @@ -0,0 +1,57 @@ +heat_template_version: 2013-05-23 +description: 'Simple Base HOT for Sample VNF' + +parameters: + nfv: + type: json + +resources: + VDU1: + type: VDU1.yaml + properties: + flavor: { get_param: [ nfv, VDU, VDU1, computeFlavourId ] } + image-VDU1: { get_param: [ nfv, VDU, VDU1, vcImageId ] } + net1: { get_param: [ nfv, CP, VDU1_CP1, network ] } + net2: { get_resource: internalVL1 } + net3: { get_resource: internalVL2 } + affinity: { get_resource: nfvi_node_affinity } + + VDU2: + type: VDU2.yaml + properties: + flavor: { get_param: [ nfv, VDU, VDU2, computeFlavourId ] } + image-VDU2-VirtualStorage: { get_param: [ nfv, VDU, VDU2-VirtualStorage, vcImageId ] } + net1: { get_param: [ nfv, CP, VDU2_CP1, network ] } + subnet1: { get_param: [nfv, CP, VDU2_CP1, fixed_ips, 0, subnet ]} + net2: { get_resource: internalVL1 } + net3: { get_resource: internalVL2 } + affinity: { get_resource: nfvi_node_affinity } + +# delete the following lines when extmanagedVLs are specified in instantiatevnfrequest + internalVL1: + type: OS::Neutron::Net + internalVL2: + type: OS::Neutron::Net + + internalVL1_subnet: + type: OS::Neutron::Subnet + properties: + ip_version: 4 + network: + get_resource: internalVL1 + cidr: 192.168.3.0/24 + internalVL2_subnet: + type: OS::Neutron::Subnet + properties: + ip_version: 4 + network: + get_resource: internalVL2 + cidr: 192.168.4.0/24 + + nfvi_node_affinity: + type: OS::Nova::ServerGroup + properties: + name: nfvi_node_affinity + policies: [ 'affinity' ] + +outputs: {} diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_df_simple.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_df_simple.yaml new file mode 100644 index 000000000..d3efc8796 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_df_simple.yaml @@ -0,0 +1,357 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Simple deployment flavour for Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - v2_sample3_types.yaml + +topology_template: + inputs: + descriptor_id: + type: string + descriptor_version: + type: string + provider: + type: string + product_name: + type: string + software_version: + type: string + vnfm_info: + type: list + entry_schema: + type: string + flavour_id: + type: string + flavour_description: + type: string + + substitution_mappings: + node_type: company.provider.VNF + properties: + flavour_id: simple + requirements: + virtual_link_external1_1: [ VDU1_CP1, virtual_link ] + virtual_link_external1_2: [ VDU2_CP1, virtual_link ] + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_description: A simple flavour + interfaces: + Vnflcm: + instantiate_start: + implementation: sample-script + instantiate_end: + implementation: sample-script + terminate_start: + implementation: sample-script + terminate_end: + implementation: sample-script + scale_start: + implementation: sample-script + scale_end: + implementation: sample-script + heal_start: + implementation: sample-script + heal_end: + implementation: sample-script + change_external_connectivity_start: + implementation: sample-script + change_external_connectivity_end: + implementation: sample-script + modify_information_start: + implementation: sample-script + modify_information_end: + implementation: sample-script + artifacts: + sample-script: + description: Sample script + type: tosca.artifacts.Implementation.Python + file: ../Scripts/sample_script.py + + 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: 3 + sw_image_data: + name: cirros-0.5.2-x86_64-disk + version: '0.5.2' + checksum: + algorithm: sha-256 + hash: 932fcae93574e242dc3d772d5235061747dfe537668443a1f0567d893614b464 + container_format: bare + disk_format: qcow2 + min_disk: 0 GB + min_ram: 256 MB + size: 12 GB + capabilities: + virtual_compute: + properties: + requested_additional_capabilities: + properties: + requested_additional_capability_name: m1.tiny + support_mandatory: true + target_performance_parameters: + entry_schema: test + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 3 GB + + VDU2: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU2 + description: VDU2 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + capabilities: + virtual_compute: + properties: + requested_additional_capabilities: + properties: + requested_additional_capability_name: m1.tiny + support_mandatory: true + target_performance_parameters: + entry_schema: test + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 3 GB + requirements: + - virtual_storage: VDU2-VirtualStorage + + VDU2-VirtualStorage: + type: tosca.nodes.nfv.Vdu.VirtualBlockStorage + properties: + virtual_block_storage_data: + size_of_storage: 1 GB + rdma_enabled: true + sw_image_data: + name: VDU2-VirtualStorage-image + version: '0.5.2' + checksum: + algorithm: sha-256 + hash: 932fcae93574e242dc3d772d5235061747dfe537668443a1f0567d893614b464 + container_format: bare + disk_format: qcow2 + min_disk: 0 GB + min_ram: 256 MB + size: 12 GB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: ../Files/images/cirros-0.5.2-x86_64-disk.img + + VDU1_CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU1 + + VDU1_CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU1 + - virtual_link: internalVL1 + + VDU1_CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU1 + - virtual_link: internalVL2 + + VDU2_CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU2 + + VDU2_CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU2 + - virtual_link: internalVL1 + + VDU2_CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU2 + - virtual_link: internalVL2 + + internalVL1: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: External Managed 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: 192.168.3.0/24 + + internalVL2: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: External Managed 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: 192.168.4.0/24 + + groups: + affinityOrAntiAffinityGroup1: + type: tosca.groups.nfv.PlacementGroup + members: [ VDU1, VDU2 ] + + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + VDU1_scale: + name: VDU1_scale + description: VDU1 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 ] + + - VDU2_initial_delta: + type: tosca.policies.nfv.VduInitialDelta + properties: + initial_delta: + number_of_instances: 1 + targets: [ VDU2 ] + + - VDU1_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: VDU1_scale + 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: + VDU1_scale: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + VDU1_scale: + scale_level: 1 + 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: 2 + targets: [ VDU1 ] + + - VDU2_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 1 + targets: [ VDU2 ] + + - internalVL1_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: [ internalVL1 ] + + - internalVL2_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: [ internalVL2 ] + + - policy_antiaffinity_group: + type: tosca.policies.nfv.AntiAffinityRule + targets: [ affinityOrAntiAffinityGroup1 ] + properties: + scope: nfvi_node diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_top.vnfd.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_top.vnfd.yaml new file mode 100644 index 000000000..af82904b3 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_top.vnfd.yaml @@ -0,0 +1,31 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - v2_sample3_types.yaml + - v2_sample3_df_simple.yaml + +topology_template: + inputs: + selected_flavour: + type: string + description: VNF deployment flavour selected by the consumer. It is provided in the API + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_id: { get_input: selected_flavour } + descriptor_id: b1bb0ce7-ebca-4fa7-95ed-4840d7000000 + provider: Company + product_name: Sample VNF + software_version: '1.0' + descriptor_version: '1.0' + vnfm_info: + - Tacker + requirements: + #- virtual_link_external # mapped in lower-level templates + #- virtual_link_internal # mapped in lower-level templates diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_types.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_types.yaml new file mode 100644 index 000000000..8fff47c24 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Definitions/v2_sample3_types.yaml @@ -0,0 +1,55 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: VNF type definition + +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-4840d7000000 ] ] + default: b1bb0ce7-ebca-4fa7-95ed-4840d7000000 + descriptor_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + provider: + type: string + constraints: [ valid_values: [ 'Company' ] ] + default: 'Company' + 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: "flavour" + requirements: + - virtual_link_external1: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_external2: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm \ No newline at end of file diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Scripts/coordinate_vnf.py b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Scripts/coordinate_vnf.py new file mode 100644 index 000000000..d7057da6d --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Scripts/coordinate_vnf.py @@ -0,0 +1,46 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 os +import pickle +import sys + + +class FailScript(object): + def __init__(self, vnfc_param): + self.vnfc_param = vnfc_param + + def run(self): + operation = 'change_vnfpkg' + if self.vnfc_param['is_rollback']: + operation += '_rollback' + if os.path.exists(f'/tmp/{operation}'): + raise Exception(f'test {operation} error') + + +def main(): + vnfc_param = pickle.load(sys.stdin.buffer) + script = FailScript(vnfc_param) + script.run() + + +if __name__ == "__main__": + try: + main() + os._exit(0) + except Exception as ex: + sys.stderr.write(str(ex)) + sys.stderr.flush() + os._exit(1) diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Scripts/sample_script.py b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Scripts/sample_script.py new file mode 100644 index 000000000..cb98d4656 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/Scripts/sample_script.py @@ -0,0 +1,68 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 functools +import os +import pickle +import sys + + +class FailScript(object): + """Define error method for each operation + + For example: + + def instantiate_start(self): + if os.path.exists('/tmp/instantiate_start') + raise Exception('test instantiate_start error') + """ + + def __init__(self, req, inst, grant_req, grant, csar_dir): + self.req = req + self.inst = inst + self.grant_req = grant_req + self.grant = grant + self.csar_dir = csar_dir + + def _fail(self, method): + if os.path.exists(f'/tmp/{method}'): + raise Exception(f'test {method} error') + + def __getattr__(self, name): + return functools.partial(self._fail, name) + + +def main(): + script_dict = pickle.load(sys.stdin.buffer) + + operation = script_dict['operation'] + req = script_dict['request'] + inst = script_dict['vnf_instance'] + grant_req = script_dict['grant_request'] + grant = script_dict['grant_response'] + csar_dir = script_dict['tmp_csar_dir'] + + script = FailScript(req, inst, grant_req, grant, csar_dir) + getattr(script, operation)() + + +if __name__ == "__main__": + try: + main() + os._exit(0) + except Exception as ex: + sys.stderr.write(str(ex)) + sys.stderr.flush() + os._exit(1) diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/TOSCA-Metadata/TOSCA.meta b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/TOSCA-Metadata/TOSCA.meta new file mode 100644 index 000000000..c96ce5f5c --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/contents/TOSCA-Metadata/TOSCA.meta @@ -0,0 +1,4 @@ +TOSCA-Meta-File-Version: 1.0 +CSAR-Version: 1.1 +Created-by: Onboarding portal +Entry-Definitions: Definitions/v2_sample3_top.vnfd.yaml diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard/pkggen.py b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/pkggen.py new file mode 100644 index 000000000..39e69bd56 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard/pkggen.py @@ -0,0 +1,83 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 json +import os +import shutil +import tempfile + +from oslo_utils import uuidutils + +from tacker.tests.functional.sol_v2_common import paramgen +from tacker.tests.functional.sol_v2_common import utils + + +zip_file_name = os.path.basename(os.path.abspath(".")) + '.zip' +tmp_dir = tempfile.mkdtemp() +vnfd_id = uuidutils.generate_uuid() + +# tacker/tests/etc... +# /functional/sol_v2_common/samples/smapleX +image_dir = "../../../../etc/samples/etsi/nfv/common/Files/images/" +image_file = "cirros-0.5.2-x86_64-disk.img" +image_path = os.path.abspath(image_dir + image_file) + +# tacker/sol_refactored/infra_drivers/openstack/userdata_standard.py +# /tests/functional/sol_v2_common/samples/smapleX +userdata_dir = "../../../../../sol_refactored/infra_drivers/openstack/" +userdata_file = "userdata_standard.py" +userdata_path = os.path.abspath(userdata_dir + userdata_file) + +utils.make_zip(".", tmp_dir, vnfd_id, image_path=image_path, + userdata_path=userdata_path) + +shutil.copy(os.path.join(tmp_dir, zip_file_name), ".") +shutil.rmtree(tmp_dir) + +create_req = paramgen.sample3_create(vnfd_id) +terminate_req = paramgen.sample3_terminate() + +net_ids = utils.get_network_ids(['net0', 'net1', 'net_mgmt']) +subnet_ids = utils.get_subnet_ids(['subnet0', 'subnet1']) + +instantiate_req = paramgen.sample3_instantiate( + net_ids, subnet_ids, "http://localhost/identity/v3") + +scale_out_req = paramgen.sample3_scale_out() +scale_in_req = paramgen.sample3_scale_in() +heal_req = paramgen.sample3_heal() +change_ext_conn_req = paramgen.sample3_change_ext_conn(net_ids) + +with open("create_req", "w") as f: + f.write(json.dumps(create_req, indent=2)) + +with open("terminate_req", "w") as f: + f.write(json.dumps(terminate_req, indent=2)) + +with open("instantiate_req", "w") as f: + f.write(json.dumps(instantiate_req, indent=2)) + +with open("scale_out_req", "w") as f: + f.write(json.dumps(scale_out_req, indent=2)) + +with open("scale_in_req", "w") as f: + f.write(json.dumps(scale_in_req, indent=2)) + +# NOTE: vnfcInstanceId should be filled by hand +with open("heal_req", "w") as f: + f.write(json.dumps(heal_req, indent=2)) + +with open("change_ext_conn_req", "w") as f: + f.write(json.dumps(change_ext_conn_req, indent=2)) diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/nested/VDU1.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/nested/VDU1.yaml new file mode 100644 index 000000000..c0fdb03bb --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/nested/VDU1.yaml @@ -0,0 +1,53 @@ +heat_template_version: 2013-05-23 +description: 'VDU1 HOT for Sample VNF' + +parameters: + flavor: + type: string + image-VDU1: + type: string + net1: + type: string + net2: + type: string + net3: + type: string + affinity: + type: string + +resources: + VDU1: + type: OS::Nova::Server + properties: + flavor: { get_param: flavor } + name: VDU1 + image: { get_param: image-VDU1 } + networks: + - port: + get_resource: VDU1_CP1 +# replace the following line to Port ID when extmanagedVLs' Ports are +# specified in instantiatevnfrequest + - port: + get_resource: VDU1_CP2 + - port: + get_resource: VDU1_CP3 + scheduler_hints: + group: { get_param: affinity } + +# extVL without FixedIP or with numDynamicAddresses + VDU1_CP1: + type: OS::Neutron::Port + properties: + network: { get_param: net1 } + +# CPs of internal VLs are deleted when extmanagedVLs and port are +# specified in instantiatevnfrequest + VDU1_CP2: + type: OS::Neutron::Port + properties: + network: { get_param: net2 } + + VDU1_CP3: + type: OS::Neutron::Port + properties: + network: { get_param: net3 } diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/nested/VDU2.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/nested/VDU2.yaml new file mode 100644 index 000000000..ee3415333 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/nested/VDU2.yaml @@ -0,0 +1,69 @@ +heat_template_version: 2013-05-23 +description: 'VDU2 HOT for Sample VNF' + +parameters: + flavor: + type: string + image-VDU2-VirtualStorage: + type: string + net1: + type: string + net2: + type: string + net3: + type: string + subnet1: + type: string + affinity: + type: string + +resources: + VDU2: + type: OS::Nova::Server + properties: + flavor: { get_param: flavor } + name: VDU2 + block_device_mapping_v2: [{"volume_id": { get_resource: VDU2-VirtualStorage }}] + networks: + - port: + get_resource: VDU2_CP1 +# replace the following line to Port ID when extmanagedVLs' Ports are +# specified in instantiatevnfrequest + - port: + get_resource: VDU2_CP2 + - port: + get_resource: VDU2_CP3 + scheduler_hints: + group: {get_param: affinity } + + VDU2-VirtualStorage: + type: OS::Cinder::Volume + properties: + image: { get_param: image-VDU2-VirtualStorage } + size: 1 + volume_type: { get_resource: multi } + multi: + type: OS::Cinder::VolumeType + properties: + name: VDU2-multi + metadata: { multiattach: " True" } + +# extVL with numDynamicAddresses and subnet + VDU2_CP1: + type: OS::Neutron::Port + properties: + network: { get_param: net1 } + fixed_ips: + - subnet: { get_param: subnet1 } + +# CPs of internal VLs are deleted when extmanagedVLs and port are +# specified in instantiatevnfrequest + VDU2_CP2: + type: OS::Neutron::Port + properties: + network: { get_param: net2 } + + VDU2_CP3: + type: OS::Neutron::Port + properties: + network: { get_param: net3 } diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/sample4.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/sample4.yaml new file mode 100644 index 000000000..bad8030ac --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/BaseHOT/simple/sample4.yaml @@ -0,0 +1,57 @@ +heat_template_version: 2013-05-23 +description: 'Simple Base HOT for Sample VNF' + +parameters: + nfv: + type: json + +resources: + VDU1: + type: VDU1.yaml + properties: + flavor: { get_param: [ nfv, VDU, VDU1, computeFlavourId ] } + image-VDU1: { get_param: [ nfv, VDU, VDU1, vcImageId ] } + net1: { get_param: [ nfv, CP, VDU1_CP1, network ] } + net2: { get_resource: internalVL1 } + net3: { get_resource: internalVL2 } + affinity: { get_resource: nfvi_node_affinity } + + VDU2: + type: VDU2.yaml + properties: + flavor: { get_param: [ nfv, VDU, VDU2, computeFlavourId ] } + image-VDU2-VirtualStorage: { get_param: [ nfv, VDU, VDU2-VirtualStorage, vcImageId ] } + net1: { get_param: [ nfv, CP, VDU2_CP1, network ] } + subnet1: { get_param: [nfv, CP, VDU2_CP1, fixed_ips, 0, subnet ]} + net2: { get_resource: internalVL1 } + net3: { get_resource: internalVL2 } + affinity: { get_resource: nfvi_node_affinity } + +# delete the following lines when extmanagedVLs are specified in instantiatevnfrequest + internalVL1: + type: OS::Neutron::Net + internalVL2: + type: OS::Neutron::Net + + internalVL1_subnet: + type: OS::Neutron::Subnet + properties: + ip_version: 4 + network: + get_resource: internalVL1 + cidr: 192.168.3.0/24 + internalVL2_subnet: + type: OS::Neutron::Subnet + properties: + ip_version: 4 + network: + get_resource: internalVL2 + cidr: 192.168.4.0/24 + + nfvi_node_affinity: + type: OS::Nova::ServerGroup + properties: + name: nfvi_node_affinity + policies: [ 'affinity' ] + +outputs: {} diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_df_simple.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_df_simple.yaml new file mode 100644 index 000000000..398f7dd8d --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_df_simple.yaml @@ -0,0 +1,357 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Simple deployment flavour for Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - v2_sample4_types.yaml + +topology_template: + inputs: + descriptor_id: + type: string + descriptor_version: + type: string + provider: + type: string + product_name: + type: string + software_version: + type: string + vnfm_info: + type: list + entry_schema: + type: string + flavour_id: + type: string + flavour_description: + type: string + + substitution_mappings: + node_type: company.provider.VNF + properties: + flavour_id: simple + requirements: + virtual_link_external1_1: [ VDU1_CP1, virtual_link ] + virtual_link_external1_2: [ VDU2_CP1, virtual_link ] + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_description: A simple flavour + interfaces: + Vnflcm: + instantiate_start: + implementation: sample-script + instantiate_end: + implementation: sample-script + terminate_start: + implementation: sample-script + terminate_end: + implementation: sample-script + scale_start: + implementation: sample-script + scale_end: + implementation: sample-script + heal_start: + implementation: sample-script + heal_end: + implementation: sample-script + change_external_connectivity_start: + implementation: sample-script + change_external_connectivity_end: + implementation: sample-script + modify_information_start: + implementation: sample-script + modify_information_end: + implementation: sample-script + artifacts: + sample-script: + description: Sample script + type: tosca.artifacts.Implementation.Python + file: ../Scripts/sample_script.py + + 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: 3 + sw_image_data: + name: VDU1-image + version: '0.5.2' + checksum: + algorithm: sha-256 + hash: 932fcae93574e242dc3d772d5235061747dfe537668443a1f0567d893614b464 + container_format: bare + disk_format: qcow2 + min_disk: 0 GB + min_ram: 256 MB + size: 12 GB + capabilities: + virtual_compute: + properties: + requested_additional_capabilities: + properties: + requested_additional_capability_name: m1.tiny + support_mandatory: true + target_performance_parameters: + entry_schema: test + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 3 GB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: ../Files/images/cirros-0.5.2-x86_64-disk.img + + VDU2: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU2 + description: VDU2 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + capabilities: + virtual_compute: + properties: + requested_additional_capabilities: + properties: + requested_additional_capability_name: m1.tiny + support_mandatory: true + target_performance_parameters: + entry_schema: test + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 3 GB + requirements: + - virtual_storage: VDU2-VirtualStorage + + VDU2-VirtualStorage: + type: tosca.nodes.nfv.Vdu.VirtualBlockStorage + properties: + virtual_block_storage_data: + size_of_storage: 1 GB + rdma_enabled: true + sw_image_data: + name: cirros-0.5.2-x86_64-disk + version: '0.5.2' + checksum: + algorithm: sha-256 + hash: 932fcae93574e242dc3d772d5235061747dfe537668443a1f0567d893614b464 + container_format: bare + disk_format: qcow2 + min_disk: 0 GB + min_ram: 256 MB + size: 12 GB + + VDU1_CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU1 + + VDU1_CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU1 + - virtual_link: internalVL1 + + VDU1_CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU1 + - virtual_link: internalVL2 + + VDU2_CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + requirements: + - virtual_binding: VDU2 + + VDU2_CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU2 + - virtual_link: internalVL1 + + VDU2_CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU2 + - virtual_link: internalVL2 + + internalVL1: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: External Managed 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: 192.168.3.0/24 + + internalVL2: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: External Managed 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: 192.168.4.0/24 + + groups: + affinityOrAntiAffinityGroup1: + type: tosca.groups.nfv.PlacementGroup + members: [ VDU1, VDU2 ] + + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + VDU1_scale: + name: VDU1_scale + description: VDU1 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 ] + + - VDU2_initial_delta: + type: tosca.policies.nfv.VduInitialDelta + properties: + initial_delta: + number_of_instances: 1 + targets: [ VDU2 ] + + - VDU1_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: VDU1_scale + 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: + VDU1_scale: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + VDU1_scale: + scale_level: 1 + 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: 2 + targets: [ VDU1 ] + + - VDU2_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 1 + targets: [ VDU2 ] + + - internalVL1_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: [ internalVL1 ] + + - internalVL2_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: [ internalVL2 ] + + - policy_antiaffinity_group: + type: tosca.policies.nfv.AntiAffinityRule + targets: [ affinityOrAntiAffinityGroup1 ] + properties: + scope: nfvi_node diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_top.vnfd.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_top.vnfd.yaml new file mode 100644 index 000000000..0e639d55b --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_top.vnfd.yaml @@ -0,0 +1,31 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - v2_sample4_types.yaml + - v2_sample4_df_simple.yaml + +topology_template: + inputs: + selected_flavour: + type: string + description: VNF deployment flavour selected by the consumer. It is provided in the API + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_id: { get_input: selected_flavour } + descriptor_id: b1bb0ce7-ebca-4fa7-95ed-4840d7000000 + provider: Company + product_name: Sample VNF + software_version: '1.0' + descriptor_version: '1.0' + vnfm_info: + - Tacker + requirements: + #- virtual_link_external # mapped in lower-level templates + #- virtual_link_internal # mapped in lower-level templates diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_types.yaml b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_types.yaml new file mode 100644 index 000000000..0aac5c339 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Definitions/v2_sample4_types.yaml @@ -0,0 +1,55 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: VNF type definition + +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-4840d7000000 ] ] + default: b1bb0ce7-ebca-4fa7-95ed-4840d7000000 + descriptor_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + provider: + type: string + constraints: [ valid_values: [ 'Company' ] ] + default: 'Company' + 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: "flavour" + requirements: + - virtual_link_external1: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_external2: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Scripts/coordinate_vnf.py b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Scripts/coordinate_vnf.py new file mode 100644 index 000000000..d7057da6d --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Scripts/coordinate_vnf.py @@ -0,0 +1,46 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 os +import pickle +import sys + + +class FailScript(object): + def __init__(self, vnfc_param): + self.vnfc_param = vnfc_param + + def run(self): + operation = 'change_vnfpkg' + if self.vnfc_param['is_rollback']: + operation += '_rollback' + if os.path.exists(f'/tmp/{operation}'): + raise Exception(f'test {operation} error') + + +def main(): + vnfc_param = pickle.load(sys.stdin.buffer) + script = FailScript(vnfc_param) + script.run() + + +if __name__ == "__main__": + try: + main() + os._exit(0) + except Exception as ex: + sys.stderr.write(str(ex)) + sys.stderr.flush() + os._exit(1) diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Scripts/sample_script.py b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Scripts/sample_script.py new file mode 100644 index 000000000..cb98d4656 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/Scripts/sample_script.py @@ -0,0 +1,68 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 functools +import os +import pickle +import sys + + +class FailScript(object): + """Define error method for each operation + + For example: + + def instantiate_start(self): + if os.path.exists('/tmp/instantiate_start') + raise Exception('test instantiate_start error') + """ + + def __init__(self, req, inst, grant_req, grant, csar_dir): + self.req = req + self.inst = inst + self.grant_req = grant_req + self.grant = grant + self.csar_dir = csar_dir + + def _fail(self, method): + if os.path.exists(f'/tmp/{method}'): + raise Exception(f'test {method} error') + + def __getattr__(self, name): + return functools.partial(self._fail, name) + + +def main(): + script_dict = pickle.load(sys.stdin.buffer) + + operation = script_dict['operation'] + req = script_dict['request'] + inst = script_dict['vnf_instance'] + grant_req = script_dict['grant_request'] + grant = script_dict['grant_response'] + csar_dir = script_dict['tmp_csar_dir'] + + script = FailScript(req, inst, grant_req, grant, csar_dir) + getattr(script, operation)() + + +if __name__ == "__main__": + try: + main() + os._exit(0) + except Exception as ex: + sys.stderr.write(str(ex)) + sys.stderr.flush() + os._exit(1) diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/TOSCA-Metadata/TOSCA.meta b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/TOSCA-Metadata/TOSCA.meta new file mode 100644 index 000000000..c2d2a379e --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/contents/TOSCA-Metadata/TOSCA.meta @@ -0,0 +1,4 @@ +TOSCA-Meta-File-Version: 1.0 +CSAR-Version: 1.1 +Created-by: Onboarding portal +Entry-Definitions: Definitions/v2_sample4_top.vnfd.yaml diff --git a/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/pkggen.py b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/pkggen.py new file mode 100644 index 000000000..387ad5f11 --- /dev/null +++ b/tacker/tests/functional/sol_v2_common/samples/userdata_standard_change_vnfpkg/pkggen.py @@ -0,0 +1,52 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 json +import os +import shutil +import tempfile + +from oslo_utils import uuidutils + +from tacker.tests.functional.sol_v2_common import paramgen +from tacker.tests.functional.sol_v2_common import utils + + +zip_file_name = os.path.basename(os.path.abspath(".")) + '.zip' +tmp_dir = tempfile.mkdtemp() +vnfd_id = uuidutils.generate_uuid() + +# tacker/tests/etc... +# /functional/sol_v2_common/samples/sampleX +image_dir = "../../../../etc/samples/etsi/nfv/common/Files/images/" +image_file = "cirros-0.5.2-x86_64-disk.img" +image_path = os.path.abspath(image_dir + image_file) + +# tacker/sol_refactored/infra_drivers/openstack/userdata_standard.py +# /tests/functional/sol_v2_common/samples/sampleX +userdata_dir = "../../../../../sol_refactored/infra_drivers/openstack/" +userdata_file = "userdata_standard.py" +userdata_path = os.path.abspath(userdata_dir + userdata_file) + +utils.make_zip(".", tmp_dir, vnfd_id, image_path=image_path, + userdata_path=userdata_path) + +shutil.copy(os.path.join(tmp_dir, zip_file_name), ".") +shutil.rmtree(tmp_dir) + +change_vnfpkg_req = paramgen.sample4_change_vnfpkg(vnfd_id) + +with open("change_vnfpkg_req", "w") as f: + f.write(json.dumps(change_vnfpkg_req, indent=2)) diff --git a/tacker/tests/functional/sol_v2_common/utils.py b/tacker/tests/functional/sol_v2_common/utils.py index 607b3a9e9..2d5e602bb 100644 --- a/tacker/tests/functional/sol_v2_common/utils.py +++ b/tacker/tests/functional/sol_v2_common/utils.py @@ -23,7 +23,8 @@ import subprocess SAMPLE_VNFD_ID = "b1bb0ce7-ebca-4fa7-95ed-4840d7000000" -def make_zip(sample_dir, tmp_dir, vnfd_id, image_path=None): +def make_zip(sample_dir, tmp_dir, vnfd_id, image_path=None, + userdata_path=None): # NOTE: '.zip' will be added by shutil.make_archive zip_file_name = os.path.basename(os.path.abspath(sample_dir)) zip_file_path = os.path.join(tmp_dir, zip_file_name) @@ -53,6 +54,12 @@ def make_zip(sample_dir, tmp_dir, vnfd_id, image_path=None): os.makedirs(file_path) shutil.copy(image_path, file_path) + if userdata_path is not None: + # mkdir UserData/ and copy userdata_path into it + file_path = os.path.join(tmp_contents, "UserData") + os.makedirs(file_path) + shutil.copy(userdata_path, file_path) + shutil.make_archive(zip_file_path, "zip", tmp_contents)