diff --git a/releasenotes/notes/support-cnf-scale-with-sol003-99ad0c79f205e745.yaml b/releasenotes/notes/support-cnf-scale-with-sol003-99ad0c79f205e745.yaml new file mode 100644 index 000000000..fcc8f9c18 --- /dev/null +++ b/releasenotes/notes/support-cnf-scale-with-sol003-99ad0c79f205e745.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add Container based VNF scale operation support with ETSI NFV-SOL003 + v2.6.1 VNF Lifecycle Management. Users can scale the number of pod replicas + managed by controller resources such as Kubernetes Deployment, StatefulSet, + and ReplicaSet. diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 3780df9ae..a6d24d5aa 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -26,6 +26,7 @@ from oslo_utils import excutils from oslo_utils import timeutils from oslo_utils import uuidutils from sqlalchemy import exc as sqlexc +from toscaparser import tosca_template import ast import functools @@ -53,6 +54,8 @@ from tacker.objects import fields from tacker.objects import vnf_lcm_subscriptions as subscription_obj from tacker.plugins.common import constants from tacker.policies import vnf_lcm as vnf_lcm_policies +from tacker.tosca import utils as toscautils +from tacker.vnflcm import utils as vnflcm_utils import tacker.vnfm.nfvo_client as nfvo_client from tacker.vnfm import vim_client from tacker import wsgi @@ -977,7 +980,24 @@ class VnfLcmController(wsgi.Controller): return self._make_problem_detail( str(e), 500, title='Internal Server Error') - def _scale(self, context, vnf_info, vnf_instance, request_body): + def _get_scale_max_level_from_vnfd(self, context, vnf_instance, aspect_id): + vnfd_dict = vnflcm_utils._get_vnfd_dict(context, + vnf_instance.vnfd_id, + vnf_instance.instantiated_vnf_info.flavour_id) + tosca = tosca_template.ToscaTemplate(parsed_params={}, a_file=False, + yaml_dict_tpl=vnfd_dict) + tosca_policies = tosca.topology_template.policies + + aspect_max_level_dict = {} + toscautils._extract_policy_info( + tosca_policies, {}, {}, {}, {}, {}, aspect_max_level_dict) + + return aspect_max_level_dict.get(aspect_id) + + @check_vnf_state(action="scale", + instantiation_state=[fields.VnfInstanceState.INSTANTIATED], + task_state=[None]) + def _scale(self, context, vnf_instance, vnf_info, request_body): req_body = utils.convert_camelcase_to_snakecase(request_body) scale_vnf_request = objects.ScaleVnfRequest.obj_from_primitive( req_body, context=context) @@ -1005,18 +1025,34 @@ class VnfLcmController(wsgi.Controller): if not scale_vnf_request.additional_params.get('is_auto'): scale_vnf_request.additional_params['is_auto'] = "False" + vim_type = vnf_instance.vim_connection_info[0].vim_type if scale_vnf_request.type == 'SCALE_IN': if current_level == 0 or\ current_level < scale_vnf_request.number_of_steps: return self._make_problem_detail( 'can not scale_in', 400, title='can not scale_in') + if vim_type == "kubernetes" and\ + scale_vnf_request.additional_params['is_reverse'] == "True": + return self._make_problem_detail( + 'is_reverse option is not supported when Kubernetes ' + 'scale operation', + 400, + title='is_reverse option is not supported when Kubernetes ' + 'scale operation') scale_level = current_level - scale_vnf_request.number_of_steps elif scale_vnf_request.type == 'SCALE_OUT': - scaleGroupDict = jsonutils.loads( - vnf_info['attributes']['scale_group']) - max_level = (scaleGroupDict['scaleGroupDict'] - [scale_vnf_request.aspect_id]['maxLevel']) + if vim_type == "kubernetes": + max_level = self._get_scale_max_level_from_vnfd( + context=context, + vnf_instance=vnf_instance, + aspect_id=scale_vnf_request.aspect_id) + else: + scaleGroupDict = jsonutils.loads( + vnf_info['attributes']['scale_group']) + max_level = (scaleGroupDict['scaleGroupDict'] + [scale_vnf_request.aspect_id]['maxLevel']) + scale_level = current_level + scale_vnf_request.number_of_steps if max_level < scale_level: return self._make_problem_detail( @@ -1043,6 +1079,9 @@ class VnfLcmController(wsgi.Controller): error_point=1) vnf_lcm_op_occ.create() + vnf_instance.task_state = fields.VnfInstanceTaskState.SCALING + vnf_instance.save() + vnflcm_url = CONF.vnf_lcm.endpoint_url + \ "/vnflcm/v1/vnf_lcm_op_occs/" + vnf_lcm_op_occs_id insta_url = CONF.vnf_lcm.endpoint_url + \ @@ -1096,7 +1135,7 @@ class VnfLcmController(wsgi.Controller): if not vnf_instance.instantiated_vnf_info.scale_status: return self._make_problem_detail( 'NOT SCALE VNF', 409, title='NOT SCALE VNF') - return self._scale(context, vnf_info, vnf_instance, body) + return self._scale(context, vnf_instance, vnf_info, body) except vnfm.VNFNotFound as vnf_e: return self._make_problem_detail( str(vnf_e), 404, title='VNF NOT FOUND') diff --git a/tacker/conductor/conductor_server.py b/tacker/conductor/conductor_server.py index 78dfb54a8..680fb42ac 100644 --- a/tacker/conductor/conductor_server.py +++ b/tacker/conductor/conductor_server.py @@ -453,8 +453,6 @@ class Conductor(manager.Manager): yaml.dump(flavour.get('tpl_dict'), default_flow_style=False) vnfd_attribute.create() - break - @revert_upload_vnf_package def upload_vnf_package_content(self, context, vnf_package): vnf_package.onboarding_state = ( diff --git a/tacker/extensions/vnfm.py b/tacker/extensions/vnfm.py index 40d040b0e..d3d64bf1b 100644 --- a/tacker/extensions/vnfm.py +++ b/tacker/extensions/vnfm.py @@ -109,6 +109,14 @@ class CNFCreateWaitFailed(exceptions.TackerException): message = _('CNF Create Failed with reason: %(reason)s') +class CNFScaleFailed(exceptions.TackerException): + message = _('CNF Scale Failed with reason: %(reason)s') + + +class CNFScaleWaitFailed(exceptions.TackerException): + message = _('CNF Scale Wait Failed with reason: %(reason)s') + + class ServiceTypeNotFound(exceptions.NotFound): message = _('service type %(service_type_id)s could not be found') diff --git a/tacker/objects/fields.py b/tacker/objects/fields.py index 1874c3900..644ae925d 100644 --- a/tacker/objects/fields.py +++ b/tacker/objects/fields.py @@ -137,9 +137,10 @@ class VnfInstanceTaskState(BaseTackerEnum): INSTANTIATING = 'INSTANTIATING' HEALING = 'HEALING' TERMINATING = 'TERMINATING' + SCALING = 'SCALING' ERROR = 'ERROR' - ALL = (INSTANTIATING, HEALING, TERMINATING, ERROR) + ALL = (INSTANTIATING, HEALING, TERMINATING, SCALING, ERROR) class VnfInstanceTaskStateField(BaseEnumField): diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_df_scalingsteps.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_df_scalingsteps.yaml new file mode 100644 index 000000000..7b699d6aa --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_df_scalingsteps.yaml @@ -0,0 +1,105 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - helloworld3_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: scalingsteps + requirements: + virtual_link_external: [] + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_description: A flavour for setting scaling_step to 2 + + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: vdu1 + description: kubernetes controller resource as VDU + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 5 + + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + vdu1_aspect: + name: vdu1_aspect + 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 ] + + - vdu1_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: vdu1_aspect + deltas: + delta_1: + number_of_instances: 2 + targets: [ VDU1 ] + + - instantiation_levels: + type: tosca.policies.nfv.InstantiationLevels + properties: + levels: + instantiation_level_1: + description: Smallest size + scale_info: + vdu1_aspect: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + vdu1_aspect: + scale_level: 2 + default_level: instantiation_level_1 + + - vdu1_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 5 + targets: [ VDU1 ] diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_df_simple.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_df_simple.yaml new file mode 100644 index 000000000..628d87660 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_df_simple.yaml @@ -0,0 +1,105 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - helloworld3_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_external: [] + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_description: A simple flavour + + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: vdu1 + description: kubernetes controller resource as VDU + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 3 + + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + vdu1_aspect: + name: vdu1_aspect + 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 ] + + - vdu1_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: vdu1_aspect + 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_aspect: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + vdu1_aspect: + scale_level: 2 + default_level: instantiation_level_1 + + - vdu1_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 3 + targets: [ VDU1 ] diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_top.vnfd.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_top.vnfd.yaml new file mode 100644 index 000000000..dbd6739f2 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_top.vnfd.yaml @@ -0,0 +1,32 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - helloworld3_types.yaml + - helloworld3_df_simple.yaml + - helloworld3_df_scalingsteps.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-4840d70a1177 + 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/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_types.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_types.yaml new file mode 100644 index 000000000..754636d59 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Definitions/helloworld3_types.yaml @@ -0,0 +1,53 @@ +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-4840d70a1177 ] ] + default: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 + descriptor_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + provider: + type: string + constraints: [ valid_values: [ '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,scalingsteps ] ] + default: simple + flavour_description: + type: string + default: "" + requirements: + - virtual_link_external: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/deployment_scale.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/deployment_scale.yaml new file mode 100644 index 000000000..9ebe1b56b --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/deployment_scale.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vdu1 + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: webserver + template: + metadata: + labels: + app: webserver + spec: + containers: + - name: nginx + image: nginx + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + protocol: TCP \ No newline at end of file diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/replicaset_scale.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/replicaset_scale.yaml new file mode 100644 index 000000000..1d0228cec --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/replicaset_scale.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: vdu1 + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: webserver + template: + metadata: + labels: + app: webserver + spec: + containers: + - name: nginx + image: nginx + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + protocol: TCP diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/statefulset_scale.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/statefulset_scale.yaml new file mode 100644 index 000000000..98d7835d3 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/Files/kubernetes/statefulset_scale.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: vdu1 + namespace: default +spec: + selector: + matchLabels: + app: nginx + serviceName: "nginx" + replicas: 1 + template: + metadata: + labels: + app: nginx + spec: + terminationGracePeriodSeconds: 10 + containers: + - name: nginx + image: k8s.gcr.io/nginx-slim:0.8 + ports: + - containerPort: 80 + name: web + volumeMounts: + - name: www + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: www + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 64Mi +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: www-vdu1-0 +spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 64Mi + hostPath: + path: /data + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: www-vdu1-1 +spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 64Mi + hostPath: + path: /data + type: DirectoryOrCreate diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/TOSCA-Metadata/TOSCA.meta b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/TOSCA-Metadata/TOSCA.meta new file mode 100644 index 000000000..0f0a33d13 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_scale/TOSCA-Metadata/TOSCA.meta @@ -0,0 +1,19 @@ +TOSCA-Meta-File-Version: 1.0 +Created-by: dummy_user +CSAR-Version: 1.1 +Entry-Definitions: Definitions/helloworld3_top.vnfd.yaml + +Name: Files/kubernetes/deployment_scale.yaml +Content-Type: application/yaml +Algorithm: SHA-256 +Hash: 8f8c45d63880fe9190e49c42458bf40c6aa7752ec84049db1027389715e12840 + +Name: Files/kubernetes/statefulset_scale.yaml +Content-Type: application/yaml +Algorithm: SHA-256 +Hash: a9c35728419fb1e72ba73362fb17472afc63010a82ad4e3b84f3cf52161186e4 + +Name: Files/kubernetes/replicaset_scale.yaml +Content-Type: application/yaml +Algorithm: SHA-256 +Hash: 6585f8e34425c3c5ffd5ca40a262acaea1ede7fc7caa9920e5af8deb2d8dd83c diff --git a/tacker/tests/functional/sol_kubernetes/vnflcm/test_kubernetes_scale.py b/tacker/tests/functional/sol_kubernetes/vnflcm/test_kubernetes_scale.py new file mode 100644 index 000000000..ff67ee844 --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes/vnflcm/test_kubernetes_scale.py @@ -0,0 +1,484 @@ +# Copyright (C) 2020 FUJITSU +# 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 +import unittest + +from oslo_serialization import jsonutils +from oslo_utils import uuidutils +from sqlalchemy import desc +from sqlalchemy.orm import joinedload + +from tacker.common import exceptions +from tacker import context +from tacker.db import api as db_api +from tacker.db.db_sqlalchemy import api +from tacker.db.db_sqlalchemy import models +from tacker.objects import fields +from tacker.objects import vnf_lcm_op_occs +from tacker.tests.functional import base +from tacker.tests import utils + +VNF_PACKAGE_UPLOAD_TIMEOUT = 300 +VNF_INSTANTIATE_TIMEOUT = 600 +VNF_TERMINATE_TIMEOUT = 600 +VNF_SCALE_TIMEOUT = 600 +RETRY_WAIT_TIME = 5 + + +def _create_and_upload_vnf_package(tacker_client, csar_package_name, + user_defined_data): + # create vnf package + body = jsonutils.dumps({"userDefinedData": user_defined_data}) + resp, vnf_package = tacker_client.do_request( + '/vnfpkgm/v1/vnf_packages', "POST", body=body) + + # upload vnf package + csar_package_path = "../../../etc/samples/etsi/nfv/%s" % csar_package_name + file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), + csar_package_path)) + + # Generating unique vnfd id. This is required when multiple workers + # are running concurrently. The call below creates a new temporary + # CSAR with unique vnfd id. + file_path, uniqueid = utils.create_csar_with_unique_vnfd_id(file_path) + + with open(file_path, 'rb') as file_object: + resp, resp_body = tacker_client.do_request( + '/vnfpkgm/v1/vnf_packages/{id}/package_content'.format( + id=vnf_package['id']), + "PUT", body=file_object, content_type='application/zip') + + # wait for onboard + timeout = VNF_PACKAGE_UPLOAD_TIMEOUT + start_time = int(time.time()) + show_url = os.path.join('/vnfpkgm/v1/vnf_packages', vnf_package['id']) + vnfd_id = None + while True: + resp, body = tacker_client.do_request(show_url, "GET") + if body['onboardingState'] == "ONBOARDED": + vnfd_id = body['vnfdId'] + break + + if ((int(time.time()) - start_time) > timeout): + raise Exception("Failed to onboard vnf package") + + time.sleep(1) + + # remove temporarily created CSAR file + os.remove(file_path) + return vnf_package['id'], vnfd_id + + +def _delete_wait_vnf_instance(tacker_client, id): + timeout = VNF_TERMINATE_TIMEOUT + url = os.path.join("/vnflcm/v1/vnf_instances", id) + start_time = int(time.time()) + while True: + resp, body = tacker_client.do_request(url, "DELETE") + if 204 == resp.status_code: + break + + if ((int(time.time()) - start_time) > timeout): + raise Exception("Failed to delete vnf instance") + + time.sleep(RETRY_WAIT_TIME) + + +def _delete_vnf_instance(tacker_client, id): + _delete_wait_vnf_instance(tacker_client, id) + + # verify vnf instance is deleted + url = os.path.join("/vnflcm/v1/vnf_instances", id) + resp, body = tacker_client.do_request(url, "GET") + + +def _show_vnf_instance(tacker_client, id): + show_url = os.path.join("/vnflcm/v1/vnf_instances", id) + resp, vnf_instance = tacker_client.do_request(show_url, "GET") + + return vnf_instance + + +def _vnf_instance_wait( + tacker_client, id, + instantiation_state=fields.VnfInstanceState.INSTANTIATED, + timeout=VNF_INSTANTIATE_TIMEOUT): + show_url = os.path.join("/vnflcm/v1/vnf_instances", id) + start_time = int(time.time()) + while True: + resp, body = tacker_client.do_request(show_url, "GET") + if body['instantiationState'] == instantiation_state: + break + + if ((int(time.time()) - start_time) > timeout): + raise Exception("Failed to wait vnf instance") + + time.sleep(RETRY_WAIT_TIME) + + +def _terminate_vnf_instance(tacker_client, id, request_body): + url = os.path.join("/vnflcm/v1/vnf_instances", id, "terminate") + resp, body = tacker_client.do_request( + url, "POST", body=jsonutils.dumps(request_body)) + + timeout = request_body.get('gracefulTerminationTimeout') + start_time = int(time.time()) + + _vnf_instance_wait( + tacker_client, id, + instantiation_state=fields.VnfInstanceState.NOT_INSTANTIATED, + timeout=VNF_TERMINATE_TIMEOUT) + + # If gracefulTerminationTimeout is set, check whether vnf + # instantiation_state is set to NOT_INSTANTIATED after + # gracefulTerminationTimeout seconds. + if timeout and int(time.time()) - start_time < timeout: + raise Exception("Failed to terminate vnf instance") + + +class VnfLcmKubernetesScaleTest(base.BaseTackerTest): + + @classmethod + def setUpClass(cls): + cls.tacker_client = base.BaseTackerTest.tacker_http_client() + cls.vnf_package_resource, cls.vnfd_id_resource = \ + _create_and_upload_vnf_package( + cls.tacker_client, "test_cnf_scale", + {"key": "sample_scale_functional"}) + cls.vnf_instance_ids = [] + super(VnfLcmKubernetesScaleTest, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + # Terminate vnf forcefully + terminate_req_body = { + "terminationType": fields.VnfInstanceTerminationType.FORCEFUL, + } + for id in cls.vnf_instance_ids: + _terminate_vnf_instance(cls.tacker_client, id, + terminate_req_body) + _delete_vnf_instance(cls.tacker_client, id) + + # Update vnf package operational state to DISABLED + update_req_body = jsonutils.dumps({ + "operationalState": "DISABLED"}) + base_path = "/vnfpkgm/v1/vnf_packages" + for package_id in [cls.vnf_package_resource]: + resp, resp_body = cls.tacker_client.do_request( + '{base_path}/{id}'.format(id=package_id, + base_path=base_path), + "PATCH", content_type='application/json', + body=update_req_body) + + # Delete vnf package + url = '/vnfpkgm/v1/vnf_packages/%s' % package_id + cls.tacker_client.do_request(url, "DELETE") + + super(VnfLcmKubernetesScaleTest, cls).tearDownClass() + + def setUp(self): + super(VnfLcmKubernetesScaleTest, self).setUp() + self.base_vnf_instances_url = "/vnflcm/v1/vnf_instances" + self.base_vnf_lcm_op_occs_url = "/vnflcm/v1/vnf_lcm_op_occs" + self.context = context.get_admin_context() + vim_list = self.client.list_vims() + if not vim_list: + self.skipTest("Vims are not configured") + + vim_id = 'vim-kubernetes' + vim = self.get_vim(vim_list, vim_id) + if not vim: + self.skipTest("Kubernetes VIM '%s' is missing" % vim_id) + self.vim_id = vim['id'] + + def _instantiate_vnf_instance_request( + self, flavour_id, vim_id=None, additional_param=None): + request_body = {"flavourId": flavour_id} + + if vim_id: + request_body["vimConnectionInfo"] = [ + {"id": uuidutils.generate_uuid(), + "vimId": vim_id, + "vimType": "kubernetes"}] + + if additional_param: + request_body["additionalParams"] = additional_param + + return request_body + + def _create_vnf_instance(self, vnfd_id, vnf_instance_name=None, + vnf_instance_description=None): + request_body = {'vnfdId': vnfd_id} + if vnf_instance_name: + request_body['vnfInstanceName'] = vnf_instance_name + + if vnf_instance_description: + request_body['vnfInstanceDescription'] = vnf_instance_description + + resp, response_body = self.http_client.do_request( + self.base_vnf_instances_url, "POST", + body=jsonutils.dumps(request_body)) + return resp, response_body + + def _instantiate_vnf_instance(self, id, request_body): + url = os.path.join(self.base_vnf_instances_url, id, "instantiate") + resp, body = self.http_client.do_request( + url, "POST", body=jsonutils.dumps(request_body)) + self.assertEqual(202, resp.status_code) + _vnf_instance_wait(self.tacker_client, id) + + def _create_and_instantiate_vnf_instance(self, flavour_id, + additional_params): + # create vnf instance + vnf_instance_name = "test_vnf_instance_for_cnf_scale-%s" % \ + uuidutils.generate_uuid() + vnf_instance_description = "vnf instance for cnf scale testing" + resp, vnf_instance = self._create_vnf_instance( + self.vnfd_id_resource, vnf_instance_name=vnf_instance_name, + vnf_instance_description=vnf_instance_description) + + # instantiate vnf instance + additional_param = additional_params + request_body = self._instantiate_vnf_instance_request( + flavour_id, vim_id=self.vim_id, additional_param=additional_param) + + self._instantiate_vnf_instance(vnf_instance['id'], request_body) + vnf_instance = _show_vnf_instance( + self.tacker_client, vnf_instance['id']) + self.vnf_instance_ids.append(vnf_instance['id']) + + return vnf_instance + + def _scale_vnf_instance(self, id, type, aspect_id, + number_of_steps=1): + url = os.path.join(self.base_vnf_instances_url, id, "scale") + # generate body + request_body = { + "type": type, + "aspectId": aspect_id, + "numberOfSteps": number_of_steps} + resp, body = self.http_client.do_request( + url, "POST", body=jsonutils.dumps(request_body)) + self.assertEqual(202, resp.status_code) + + def _test_scale_cnf(self, id, type, aspect_id, previous_level, + delta_num=1, number_of_steps=1): + # scale operation + self._scale_vnf_instance(id, type, aspect_id, number_of_steps) + # wait vnflcm_op_occs.operation_state become COMPLETE + self._wait_vnflcm_op_occs(self.context, id) + # check scaleStatus after scale operation + vnf_instance = _show_vnf_instance( + self.tacker_client, id) + scale_status_after = \ + vnf_instance['instantiatedVnfInfo']['scaleStatus'] + if type == 'SCALE_OUT': + expected_level = previous_level + number_of_steps + else: + expected_level = previous_level - number_of_steps + for status in scale_status_after: + if status.get('aspectId') == aspect_id: + self.assertEqual(status.get('scaleLevel'), expected_level) + previous_level = status.get('scaleLevel') + + return previous_level + + def _test_scale_cnf_fail(self, id, type, aspect_id, previous_level, + delta_num=1, number_of_steps=1): + # scale operation + self._scale_vnf_instance(id, type, aspect_id, number_of_steps) + # wait vnflcm_op_occs.operation_state become FAILED_TEMP + self._wait_vnflcm_op_occs(self.context, id, "FAILED_TEMP") + # check scaleStatus after scale operation + vnf_instance = _show_vnf_instance( + self.tacker_client, id) + scale_status_after = \ + vnf_instance['instantiatedVnfInfo']['scaleStatus'] + expected_level = previous_level + for status in scale_status_after: + if status.get('aspectId') == aspect_id: + self.assertEqual(status.get('scaleLevel'), expected_level) + previous_level = status.get('scaleLevel') + + return previous_level + + def _rollback_vnf_instance(self, vnf_lcm_op_occ_id): + url = os.path.join( + self.base_vnf_lcm_op_occs_url, vnf_lcm_op_occ_id, "rollback") + # generate body + resp, body = self.http_client.do_request(url, "POST") + self.assertEqual(202, resp.status_code) + + def _test_rollback_cnf(self, id, aspect_id, previous_level, + delta_num=1, number_of_steps=1): + # get vnflcm_op_occ id for rollback + vnflcm_op_occ = self._vnf_notify_get_by_id(self.context, id) + vnf_lcm_op_occ_id = vnflcm_op_occ.id + + # rollback operation + self._rollback_vnf_instance(vnf_lcm_op_occ_id) + # wait vnflcm_op_occs.operation_state become ROLLED_BACK + self._wait_vnflcm_op_occs(self.context, id, "ROLLED_BACK") + # check scaleStatus after scale operation + vnf_instance = _show_vnf_instance( + self.tacker_client, id) + scale_status_after = \ + vnf_instance['instantiatedVnfInfo']['scaleStatus'] + expected_level = previous_level + for status in scale_status_after: + if status.get('aspectId') == aspect_id: + self.assertEqual(status.get('scaleLevel'), expected_level) + previous_level = status.get('scaleLevel') + + @db_api.context_manager.reader + def _vnf_notify_get_by_id(self, context, vnf_instance_id, + columns_to_join=None): + query = api.model_query( + context, models.VnfLcmOpOccs, + read_deleted="no", project_only=True).filter_by( + vnf_instance_id=vnf_instance_id).order_by( + desc("created_at")) + + if columns_to_join: + for column in columns_to_join: + query = query.options(joinedload(column)) + + db_vnflcm_op_occ = query.first() + + if not db_vnflcm_op_occ: + raise exceptions.VnfInstanceNotFound(id=vnf_instance_id) + + vnflcm_op_occ = vnf_lcm_op_occs.VnfLcmOpOcc.obj_from_db_obj( + context, db_vnflcm_op_occ) + return vnflcm_op_occ + + def _wait_vnflcm_op_occs( + self, context, vnf_instance_id, + operation_state='COMPLETED'): + timeout = VNF_SCALE_TIMEOUT + start_time = int(time.time()) + while True: + vnflcm_op_occ = self._vnf_notify_get_by_id( + context, vnf_instance_id) + + if vnflcm_op_occ.operation_state == operation_state: + break + + if ((int(time.time()) - start_time) > timeout): + raise Exception("Failed to wait scale instance") + + time.sleep(RETRY_WAIT_TIME) + + def test_scale_cnf_with_statefulset(self): + inst_additional_param = { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/statefulset_scale.yaml"]} + vnf_instance = self._create_and_instantiate_vnf_instance( + "simple", inst_additional_param) + aspect_id = "vdu1_aspect" + scale_status_initial = \ + vnf_instance['instantiatedVnfInfo']['scaleStatus'] + self.assertTrue(len(scale_status_initial) > 0) + for status in scale_status_initial: + self.assertIsNotNone(status.get('aspectId')) + self.assertIsNotNone(status.get('scaleLevel')) + if status.get('aspectId') == aspect_id: + previous_level = status.get('scaleLevel') + + # test scale out + previous_level = self._test_scale_cnf( + vnf_instance['id'], 'SCALE_OUT', aspect_id, previous_level) + + # test scale in + previous_level = self._test_scale_cnf( + vnf_instance['id'], 'SCALE_IN', aspect_id, previous_level) + + def test_scale_cnf_with_replicaset(self): + inst_additional_param = { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/replicaset_scale.yaml"]} + vnf_instance = self._create_and_instantiate_vnf_instance( + "simple", inst_additional_param) + aspect_id = "vdu1_aspect" + scale_status_initial = \ + vnf_instance['instantiatedVnfInfo']['scaleStatus'] + self.assertTrue(len(scale_status_initial) > 0) + for status in scale_status_initial: + self.assertIsNotNone(status.get('aspectId')) + self.assertIsNotNone(status.get('scaleLevel')) + if status.get('aspectId') == aspect_id: + previous_level = status.get('scaleLevel') + + # test scale out + previous_level = self._test_scale_cnf( + vnf_instance['id'], 'SCALE_OUT', aspect_id, previous_level) + + # test scale in + previous_level = self._test_scale_cnf( + vnf_instance['id'], 'SCALE_IN', aspect_id, previous_level) + + def test_scale_cnf_deployment_with_scaling_and_delta_two(self): + inst_additional_param = { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/deployment_scale.yaml"]} + # Use flavour_id scalingsteps that is set to delta_num=2 + vnf_instance = self._create_and_instantiate_vnf_instance( + "scalingsteps", inst_additional_param) + aspect_id = "vdu1_aspect" + scale_status_initial = \ + vnf_instance['instantiatedVnfInfo']['scaleStatus'] + self.assertTrue(len(scale_status_initial) > 0) + for status in scale_status_initial: + self.assertIsNotNone(status.get('aspectId')) + self.assertIsNotNone(status.get('scaleLevel')) + if status.get('aspectId') == aspect_id: + previous_level = status.get('scaleLevel') + + # test scale out (test for delta_num=2 and number_of_steps=2) + previous_level = self._test_scale_cnf( + vnf_instance['id'], 'SCALE_OUT', aspect_id, previous_level, + delta_num=2, number_of_steps=2) + + # test scale in (test for delta_num=2 and number_of_steps=2) + previous_level = self._test_scale_cnf( + vnf_instance['id'], 'SCALE_IN', aspect_id, previous_level, + delta_num=2, number_of_steps=2) + + @unittest.skip("Reduce test time") + def test_scale_out_cnf_rollback(self): + inst_additional_param = { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/statefulset_scale.yaml"]} + vnf_instance = self._create_and_instantiate_vnf_instance( + "simple", inst_additional_param) + aspect_id = "vdu1_aspect" + scale_status_initial = \ + vnf_instance['instantiatedVnfInfo']['scaleStatus'] + self.assertTrue(len(scale_status_initial) > 0) + for status in scale_status_initial: + self.assertIsNotNone(status.get('aspectId')) + self.assertIsNotNone(status.get('scaleLevel')) + if status.get('aspectId') == aspect_id: + previous_level = status.get('scaleLevel') + + # fail scale out for rollback + previous_level = self._test_scale_cnf_fail( + vnf_instance['id'], 'SCALE_OUT', aspect_id, previous_level, + number_of_steps=2) + + # test rollback + self._test_rollback_cnf(vnf_instance['id'], aspect_id, previous_level) diff --git a/tacker/tests/unit/nfvo/test_nfvo_plugin.py b/tacker/tests/unit/nfvo/test_nfvo_plugin.py index 6ab577d90..04b498dae 100644 --- a/tacker/tests/unit/nfvo/test_nfvo_plugin.py +++ b/tacker/tests/unit/nfvo/test_nfvo_plugin.py @@ -213,6 +213,9 @@ class FakeVNFMPlugin(mock.Mock): 'name': 'dummy_vnf_update', 'attributes': {}} + def _update_vnf_scaling(self, *args, **kwargs): + pass + class TestNfvoPlugin(db_base.SqlTestCase): def setUp(self): diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 9de51931e..82696661d 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -184,8 +184,9 @@ def return_vnf_instance( } instantiated_vnf_info = get_instantiated_vnf_info - s_status = {"aspect_id": "SP1", "scale_level": 1} - scale_status = objects.ScaleInfo(**s_status) + if scale_status == "scale_status": + s_status = {"aspect_id": "SP1", "scale_level": 1} + scale_status = objects.ScaleInfo(**s_status) instantiated_vnf_info.update( {"ext_cp_info": [], @@ -792,10 +793,10 @@ def _get_vnf(**updates): return vnf_data -def scale_request(type, number_of_steps, is_reverse): +def scale_request(type, aspect_id, number_of_steps, is_reverse): scale_request_data = { 'type': type, - 'aspect_id': "SP1", + 'aspect_id': aspect_id, 'number_of_steps': number_of_steps, 'scale_level': 1, 'additional_params': {"is_reverse": is_reverse}, @@ -844,12 +845,41 @@ def vnf_scale(): vim_id=uuidsentinel.vim_id) -def vnflcm_rollback(error_point=7): +def vnflcm_scale_in_cnf(): + dt = datetime.datetime(2000, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) return objects.VnfLcmOpOcc( - state_entered_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), - start_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), + state_entered_time=dt, + start_time=dt, + vnf_instance_id=uuidsentinel.vnf_instance_id, + operation='SCALE', + operation_state='STARTING', + is_automatic_invocation=False, + operation_params='{"type": "SCALE_IN", "aspect_id": "vdu1_aspect"}', + error_point=1, + id=constants.UUID, + created_at=dt) + + +def vnflcm_scale_out_cnf(): + dt = datetime.datetime(2000, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) + return objects.VnfLcmOpOcc( + state_entered_time=dt, + start_time=dt, + vnf_instance_id=uuidsentinel.vnf_instance_id, + operation='SCALE', + operation_state='STARTING', + is_automatic_invocation=False, + operation_params='{"type": "SCALE_OUT", "aspect_id": "vdu1_aspect"}', + error_point=1, + id=constants.UUID, + created_at=dt) + + +def vnflcm_rollback(error_point=7): + dt = datetime.datetime(2000, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) + return objects.VnfLcmOpOcc( + state_entered_time=dt, + start_time=dt, vnf_instance_id=uuidsentinel.vnf_instance_id, operation='SCALE', operation_state='FAILED_TEMP', @@ -857,16 +887,14 @@ def vnflcm_rollback(error_point=7): operation_params='{"type": "SCALE_OUT", "aspect_id": "SP1"}', error_point=error_point, id=constants.UUID, - created_at=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC)) + created_at=dt) def vnflcm_rollback_insta(error_point=7): + dt = datetime.datetime(2000, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) return objects.VnfLcmOpOcc( - state_entered_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), - start_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), + state_entered_time=dt, + start_time=dt, vnf_instance_id=uuidsentinel.vnf_instance_id, operation='INSTANTIATE', operation_state='FAILED_TEMP', @@ -874,16 +902,14 @@ def vnflcm_rollback_insta(error_point=7): operation_params='{}', error_point=error_point, id=constants.UUID, - created_at=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC)) + created_at=dt) def vnflcm_rollback_active(): + dt = datetime.datetime(2000, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) return objects.VnfLcmOpOcc( - state_entered_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), - start_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), + state_entered_time=dt, + start_time=dt, vnf_instance_id=uuidsentinel.vnf_instance_id, operation='SCALE', operation_state='ACTIVE', @@ -891,16 +917,14 @@ def vnflcm_rollback_active(): operation_params='{"type": "SCALE_OUT", "aspect_id": "SP1"}', error_point=7, id=constants.UUID, - created_at=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC)) + created_at=dt) def vnflcm_rollback_ope(): + dt = datetime.datetime(2000, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) return objects.VnfLcmOpOcc( - state_entered_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), - start_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), + state_entered_time=dt, + start_time=dt, vnf_instance_id=uuidsentinel.vnf_instance_id, operation='HEAL', operation_state='FAILED_TEMP', @@ -908,16 +932,14 @@ def vnflcm_rollback_ope(): operation_params='{}', error_point=7, id=constants.UUID, - created_at=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC)) + created_at=dt) def vnflcm_rollback_scale_in(): + dt = datetime.datetime(2000, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) return objects.VnfLcmOpOcc( - state_entered_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), - start_time=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), + state_entered_time=dt, + start_time=dt, vnf_instance_id=uuidsentinel.vnf_instance_id, operation='SCALE', operation_state='FAILED_TEMP', @@ -925,8 +947,7 @@ def vnflcm_rollback_scale_in(): operation_params='{"type": "SCALE_IN", "aspect_id": "SP1"}', error_point=7, id=constants.UUID, - created_at=datetime.datetime(2000, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC)) + created_at=dt) def vnf_rollback(): @@ -1090,6 +1111,110 @@ def vnf_dict(): return vnf_dict +def vnf_dict_cnf(): + vnf_dict = { + 'attributes': {}, + 'status': 'ACTIVE', + 'vnfd_id': 'e889e4fe-52fe-437d-b1e1-a690dc95e3f8', + 'tenant_id': '13d2ca8de70d48b2a2e0dbac2c327c0b', + 'vim_id': '3f41faa7-5630-47d2-9d4a-1216953c8887', + 'instance_id': 'd1121d3c-368b-4ac2-b39d-835aa3e4ccd8', + 'placement_attr': {'vim_name': 'kubernetes-vim'}, + 'id': '436aaa6e-2db6-4d6e-a3fc-e728b2f0ac56', + 'name': 'cnf_create_1', + 'vnfd': { + 'attributes': { + 'vnfd_simple': 'dummy' + } + } + } + return vnf_dict + + +def vnfd_dict_cnf(): + tacker_dir = os.getcwd() + def_dir = tacker_dir + "/samples/vnf_packages/Definitions/" + vnfd_dict = { + "tosca_definitions_version": "tosca_simple_yaml_1_2", + "description": "Sample VNF flavour for Sample VNF", + "imports": [ + def_dir + "etsi_nfv_sol001_common_types.yaml", + def_dir + "etsi_nfv_sol001_vnfd_types.yaml", + def_dir + "helloworld3_types.yaml"], + "topology_template": { + "node_templates": { + "VNF": { + "type": "company.provider.VNF", + "properties": { + "flavour_description": "A simple flavour"}}, + "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}}}}, + "policies": [ + { + "scaling_aspects": { + "type": "tosca.policies.nfv.ScalingAspects", + "properties": { + "aspects": { + "vdu1_aspect": { + "name": "vdu1_aspect", + "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": 0}}, + "targets": ["VDU1"]}}, + { + "vdu1_scaling_aspect_deltas": { + "type": "tosca.policies.nfv.VduScalingAspectDeltas", + "properties": { + "aspect": "vdu1_aspect", + "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_aspect": { + "scale_level": 0}}}, + "instantiation_level_2": { + "description": "Largest size", + "scale_info": { + "vdu1_aspect": { + "scale_level": 2}}} + }, + "default_level": "instantiation_level_1"}}}, + { + "vdu1_instantiation_levels": { + "type": "tosca.policies.nfv.VduInstantiationLevels", + "properties": { + "levels": { + "instantiation_level_1": { + "number_of_instances": 0}, + "instantiation_level_2": { + "number_of_instances": 2}}}, + "targets": ["VDU1"]}} + ] + } + } + return vnfd_dict + + class InjectContext(wsgi.Middleware): """Add a 'tacker.context' to WSGI environ.""" @@ -1294,13 +1419,12 @@ def fake_vnf_lcm_op_occs(): } changed_info_obj = objects.VnfInfoModifications(**changed_info) + dt = datetime.datetime(1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) vnf_lcm_op_occs = { 'id': constants.UUID, 'operation_state': 'COMPLETED', - 'state_entered_time': datetime.datetime(1900, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), - 'start_time': datetime.datetime(1900, 1, 1, 1, 1, 1, - tzinfo=iso8601.UTC), + 'state_entered_time': dt, + 'start_time': dt, 'vnf_instance_id': constants.UUID, 'operation': 'MODIFY_INFO', 'is_automatic_invocation': False, diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 5608dee96..195dc833b 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -69,6 +69,8 @@ class FakeVNFMPlugin(mock.Mock): self.vnf3_vnfd_id = 'e4015e9f-1ef2-49fb-adb6-070791ad3c45' self.vnf3_vnf_id = '7168062e-9fa1-4203-8cb7-f5c99ff3ee1b' self.vnf3_update_vnf_id = '10f66bc5-b2f1-45b7-a7cd-6dd6ad0017f5' + self.vnf_for_cnf_vnfd_id = 'e889e4fe-52fe-437d-b1e1-a690dc95e3f8' + self.vnf_for_cnf_vnf_id = '436aaa6e-2db6-4d6e-a3fc-e728b2f0ac56' self.cp11_id = 'd18c8bae-898a-4932-bff8-d5eac981a9c9' self.cp11_update_id = 'a18c8bae-898a-4932-bff8-d5eac981a9b8' @@ -110,6 +112,8 @@ class FakeVNFMPlugin(mock.Mock): return self.get_dummy_vnf_error() elif self.vnf3_vnf_id in args: return self.get_dummy_vnf_not_error() + elif self.vnf_for_cnf_vnf_id in args: + return fakes.vnf_dict_cnf() else: return self.get_dummy_vnf_active() @@ -2369,6 +2373,7 @@ class TestController(base.TestCase): return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.VnfLcmOpOcc, "create") @mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive") + @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf") @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale") @@ -2379,13 +2384,18 @@ class TestController(base.TestCase): mock_scale, mock_get_vnf, mock_vnf_instance_get_by_id, + mock_vnf_instance_save, mock_obj_from_primitive, mock_create, mock_get_service_plugins): mock_get_vnf.return_value = fakes._get_vnf() + vim_connection_info = objects.VimConnectionInfo( + vim_type="openstack") + update = {'vim_connection_info': [vim_connection_info]} mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance( - fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status") + fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status", + **update) mock_obj_from_primitive.return_value = fakes.scale_request_make( "SCALE_IN", 1) mock_create.return_value = 200 @@ -2410,6 +2420,7 @@ class TestController(base.TestCase): return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.VnfLcmOpOcc, "create") @mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive") + @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf") @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale") @@ -2420,13 +2431,18 @@ class TestController(base.TestCase): mock_scale, mock_get_vnf, mock_vnf_instance_get_by_id, + mock_vnf_instance_save, mock_obj_from_primitive, mock_create, mock_get_service_plugins): mock_get_vnf.return_value = fakes._get_vnf() + vim_connection_info = objects.VimConnectionInfo( + vim_type="openstack") + update = {'vim_connection_info': [vim_connection_info]} mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance( - fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status") + fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status", + **update) mock_obj_from_primitive.return_value = fakes.scale_request_make( "SCALE_OUT", 1) mock_create.return_value = 200 @@ -2451,6 +2467,7 @@ class TestController(base.TestCase): return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.VnfLcmOpOcc, "create") @mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive") + @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf") @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale") @@ -2459,13 +2476,19 @@ class TestController(base.TestCase): mock_scale, mock_get_vnf, mock_vnf_instance_get_by_id, + mock_vnf_instance_save, mock_obj_from_primitive, mock_create, mock_get_service_plugins): mock_get_vnf.return_value = fakes._get_vnf() + vim_connection_info = objects.VimConnectionInfo( + vim_type="openstack") + update = {'vim_connection_info': [vim_connection_info]} mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance( - fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status") + fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status", + **update) + mock_obj_from_primitive.return_value = fakes.scale_request_make( "SCALE_IN", 4) mock_create.return_value = 200 @@ -2505,8 +2528,12 @@ class TestController(base.TestCase): mock_get_service_plugins): mock_get_vnf.return_value = fakes._get_vnf() + vim_connection_info = objects.VimConnectionInfo( + vim_type="openstack") + update = {'vim_connection_info': [vim_connection_info]} mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance( - fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status") + fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status", + **update) mock_obj_from_primitive.return_value = fakes.scale_request_make( "SCALE_OUT", 4) mock_create.return_value = 200 @@ -2533,6 +2560,7 @@ class TestController(base.TestCase): return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive") @mock.patch.object(controller.VnfLcmController, "_get_rollback_vnf") + @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "send_notification") @mock.patch.object(objects.VnfLcmOpOcc, "create") @@ -2541,6 +2569,7 @@ class TestController(base.TestCase): mock_create, mock_send_notification, mock_vnf_instance, + mock_vnf_instance_save, mock_get_vnf, mock_obj_from_primitive, get_service_plugins): @@ -2556,9 +2585,13 @@ class TestController(base.TestCase): "SCALE_IN", 1) mock_get_vnf.return_value = vnf_obj + vim_connection_info = objects.VimConnectionInfo( + vim_type="openstack") + update = {'vim_connection_info': [vim_connection_info]} vnf_instance = fakes.return_vnf_instance( fields.VnfInstanceState.INSTANTIATED, - scale_status="scale_status") + scale_status="scale_status", + **update) vnf_instance.instantiated_vnf_info.instance_id =\ uuidsentinel.instance_id @@ -2571,9 +2604,10 @@ class TestController(base.TestCase): vnf_info = fakes._get_vnf() vnf_instance = fakes.return_vnf_instance( - fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status") + fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status", + **update) self.controller._scale(self.context, - vnf_info, vnf_instance, body) + vnf_instance, vnf_info, body) mock_send_notification.assert_called_once() self.assertEqual(mock_send_notification.call_args[0][1].get( @@ -2592,6 +2626,228 @@ class TestController(base.TestCase): self.assertEqual(mock_send_notification.call_args[0][1].get( 'isAutomaticInvocation'), 'False') + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VnfPackageVnfd, "get_by_id") + @mock.patch.object(objects.VnfLcmOpOcc, "create") + @mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "send_notification") + def test_scale_in_cnf( + self, + mock_send_notification, + mock_scale, + mock_get_vnf, + mock_vnf_instance_get_by_id, + mock_vnf_instance_save, + mock_obj_from_primitive, + mock_create, + mock_vnf_package_vnfd_get_by_id, + mock_get_service_plugins): + + mock_get_vnf.return_value = fakes.vnf_dict_cnf() + vim_connection_info = objects.VimConnectionInfo( + vim_type="kubernetes") + update = {'vim_connection_info': [vim_connection_info]} + scale_status = objects.ScaleInfo( + aspect_id='vdu1_aspect', scale_level=1) + mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, scale_status=scale_status, + **update) + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + scale_request = fakes.scale_request_make("SCALE_IN", 1) + scale_request.aspect_id = "vdu1_aspect" + mock_obj_from_primitive.return_value = scale_request + mock_create.return_value = 200 + + body = { + "type": "SCALE_IN", + "aspectId": "vdu1_aspect", + "numberOfSteps": 1, + "additionalParams": {}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/scale' % + FakeVNFMPlugin().vnf_for_cnf_vnf_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + resp = req.get_response(self.app) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_scale.assert_called_once() + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.vnflcm.utils._get_vnfd_dict') + @mock.patch.object(objects.VnfPackageVnfd, "get_by_id") + @mock.patch.object(objects.VnfLcmOpOcc, "create") + @mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "send_notification") + def test_scale_out_cnf( + self, + mock_send_notification, + mock_scale, + mock_get_vnf, + mock_vnf_instance_get_by_id, + mock_vnf_instance_save, + mock_obj_from_primitive, + mock_create, + mock_vnf_package_vnfd_get_by_id, + mock_vnfd_dict, + mock_get_service_plugins): + + vnf_info = fakes.vnf_dict_cnf() + mock_get_vnf.return_value = vnf_info + vim_connection_info = objects.VimConnectionInfo( + vim_type="kubernetes") + update = {'vim_connection_info': [vim_connection_info]} + scale_status = objects.ScaleInfo( + aspect_id='vdu1_aspect', scale_level=1) + mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, scale_status=scale_status, + **update) + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnfd_dict.return_value = fakes.vnfd_dict_cnf() + scale_request = fakes.scale_request_make("SCALE_OUT", 1) + scale_request.aspect_id = "vdu1_aspect" + mock_obj_from_primitive.return_value = scale_request + mock_create.return_value = 200 + + body = { + "type": "SCALE_OUT", + "aspectId": "vdu1_aspect", + "numberOfSteps": 1, + "additionalParams": {}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/scale' % + FakeVNFMPlugin().vnf_for_cnf_vnf_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + resp = req.get_response(self.app) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_scale.assert_called_once() + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VnfLcmOpOcc, "create") + @mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "send_notification") + def test_scale_in_cnf_error_is_reverse( + self, + mock_send_notification, + mock_scale, + mock_get_vnf, + mock_vnf_instance_get_by_id, + mock_vnf_instance_save, + mock_obj_from_primitive, + mock_create, + mock_get_service_plugins): + + mock_get_vnf.return_value = fakes.vnf_dict_cnf() + vim_connection_info = objects.VimConnectionInfo( + vim_type="kubernetes") + update = {'vim_connection_info': [vim_connection_info]} + scale_status = objects.ScaleInfo( + aspect_id='vdu1_aspect', scale_level=1) + mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, scale_status=scale_status, + **update) + scale_request = fakes.scale_request_make("SCALE_IN", 1) + scale_request.aspect_id = "vdu1_aspect" + scale_request.additional_params = {"is_reverse": "True"} + mock_obj_from_primitive.return_value = scale_request + mock_create.return_value = 200 + + body = { + "type": "SCALE_IN", + "aspectId": "vdu1_aspect", + "numberOfSteps": 1, + "additionalParams": { + "is_reverse": "True"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/scale' % + FakeVNFMPlugin().vnf_for_cnf_vnf_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + mock_scale.assert_not_called() + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.vnflcm.utils._get_vnfd_dict') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + @mock.patch.object(objects.VnfLcmOpOcc, "create") + @mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "send_notification") + def test_scale_out_cnf_err_over_max_scale_level( + self, + mock_send_notification, + mock_scale, + mock_get_vnf, + mock_vnf_instance_get_by_id, + mock_vnf_instance_save, + mock_obj_from_primitive, + mock_create, + mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, + mock_vnfd_dict, + mock_get_service_plugins): + + vnf_info = fakes.vnf_dict_cnf() + mock_get_vnf.return_value = vnf_info + vim_connection_info = objects.VimConnectionInfo( + vim_type="kubernetes") + update = {'vim_connection_info': [vim_connection_info]} + scale_status = objects.ScaleInfo( + aspect_id='vdu1_aspect', scale_level=1) + mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, scale_status=scale_status, + **update) + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + mock_vnfd_dict.return_value = fakes.vnfd_dict_cnf() + scale_request = fakes.scale_request_make("SCALE_OUT", 3) + scale_request.aspect_id = "vdu1_aspect" + mock_obj_from_primitive.return_value = scale_request + mock_create.return_value = 200 + + body = { + "type": "SCALE_OUT", + "aspectId": "vdu1_aspect", + "numberOfSteps": 3, + "additionalParams": {}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/scale' % + FakeVNFMPlugin().vnf_for_cnf_vnf_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + mock_scale.assert_not_called() + @mock.patch.object(TackerManager, 'get_service_plugins', return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") diff --git a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py index d21c26036..8f522cb87 100644 --- a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py +++ b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py @@ -411,7 +411,10 @@ class TestVnflcmDriver(db_base.SqlTestCase): test_utils.copy_csar_files(fake_csar, "vnflcm4") self._mock_vnf_manager(fail_method_name='create_wait') driver = vnflcm_driver.VnfLcmDriver() - vnf_dict = {"vnfd": {"attributes": {}}, "attributes": {}} + scale_status = objects.ScaleInfo(aspect_id='SP1', scale_level=0) + vnf_dict = {"vnfd": {"attributes": {}}, + "attributes": {"scaling_group_names": {"SP1": "G1"}}, + "scale_status": [scale_status]} error = self.assertRaises(exceptions.VnfInstantiationWaitFailed, driver.instantiate_vnf, self.context, vnf_instance_obj, vnf_dict, instantiate_vnf_req_obj) @@ -750,7 +753,9 @@ class TestVnflcmDriver(db_base.SqlTestCase): uuidsentinel.instance_id self._mock_vnf_manager() driver = vnflcm_driver.VnfLcmDriver() - vnf_dict = {"attributes": {}} + scale_status = objects.ScaleInfo(aspect_id='SP1', scale_level=0) + vnf_dict = {"attributes": {"scaling_group_names": {"SP1": "G1"}}, + "scale_status": [scale_status]} mock_make_final_vnf_dict.return_value = {} driver.heal_vnf(self.context, vnf_instance, vnf_dict, heal_vnf_req) self.assertEqual(1, mock_save.call_count) @@ -1062,9 +1067,9 @@ class TestVnflcmDriver(db_base.SqlTestCase): '{ \"SP1\": { \"vdu\": [\"VDU1\"], \"num\": ' + \ '1, \"maxLevel\": 3, \"initialNum\": 0, ' + \ '\"initialLevel\": 0, \"default\": 0 }}}' - scale_vnf_request = fakes.scale_request("SCALE_IN", 1, "True") + scale_vnf_request = fakes.scale_request("SCALE_IN", "SP1", 1, "True") vim_connection_info = vim_connection.VimConnectionInfo( - vim_type="fake_type") + vim_type="openstack") scale_name_list = ["fake"] grp_id = "fake_id" driver = vnflcm_driver.VnfLcmDriver() @@ -1089,9 +1094,9 @@ class TestVnflcmDriver(db_base.SqlTestCase): '{ \"SP1\": { \"vdu\": [\"VDU1\"], \"num\": ' + \ '1, \"maxLevel\": 3, \"initialNum\": 0, ' + \ '\"initialLevel\": 0, \"default\": 0 }}}' - scale_vnf_request = fakes.scale_request("SCALE_IN", 1, "False") + scale_vnf_request = fakes.scale_request("SCALE_IN", "SP1", 1, "False") vim_connection_info = vim_connection.VimConnectionInfo( - vim_type="fake_type") + vim_type="openstack") scale_name_list = ["fake"] grp_id = "fake_id" with open(vnf_info["attributes"]["heat_template"], "r") as f: @@ -1119,9 +1124,9 @@ class TestVnflcmDriver(db_base.SqlTestCase): '{ \"SP1\": { \"vdu\": [\"VDU1\"], \"num\": ' + \ '1, \"maxLevel\": 3, \"initialNum\": 0, ' + \ '\"initialLevel\": 0, \"default\": 0 }}}' - scale_vnf_request = fakes.scale_request("SCALE_OUT", 1, "False") + scale_vnf_request = fakes.scale_request("SCALE_OUT", "SP1", 1, "False") vim_connection_info = vim_connection.VimConnectionInfo( - vim_type="fake_type") + vim_type="openstack") scale_name_list = ["fake"] grp_id = "fake_id" with open(vnf_info["attributes"]["heat_template"], "r") as f: @@ -1149,9 +1154,9 @@ class TestVnflcmDriver(db_base.SqlTestCase): '{ \"SP1\": { \"vdu\": [\"VDU1\"], \"num\": ' + \ '1, \"maxLevel\": 3, \"initialNum\": 0, ' + \ '\"initialLevel\": 0, \"default\": 1 }}}' - scale_vnf_request = fakes.scale_request("SCALE_OUT", 1, "False") + scale_vnf_request = fakes.scale_request("SCALE_OUT", "SP1", 1, "False") vim_connection_info = vim_connection.VimConnectionInfo( - vim_type="fake_type") + vim_type="openstack") scale_name_list = ["fake"] grp_id = "fake_id" with open(vnf_info["attributes"]["heat_template"], "r") as f: @@ -1161,6 +1166,108 @@ class TestVnflcmDriver(db_base.SqlTestCase): driver.scale(self.context, vnf_info, scale_vnf_request, vim_connection_info, scale_name_list, grp_id) + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(VnfLcmDriver, + '_init_mgmt_driver_hash') + @mock.patch.object(yaml, "safe_load") + @mock.patch('tacker.vnflcm.utils._get_vnfd_dict') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.VnfLcmOpOcc, "save") + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(driver_manager.DriverManager, "invoke") + def test_scale_in_cnf(self, mock_invoke, mock_vnf_instance_get_by_id, + mock_lcm_save, mock_vim, mock_vnf_package_vnfd, + mock_vnfd_dict, mock_yaml_safe_load, mock_init_hash, + mock_get_service_plugins): + mock_init_hash.return_value = { + "vnflcm_noop": "ffea638bfdbde3fb01f191bbe75b031859" + "b18d663b127100eb72b19eecd7ed51" + } + vnf_info = fakes.vnf_dict_cnf() + vnf_info['vnf_lcm_op_occ'] = fakes.vnflcm_scale_in_cnf() + vnf_info['scale_level'] = 1 + vnf_info['after_scale_level'] = 0 + vnf_info['notification'] = {} + scale_vnf_request = fakes.scale_request( + "SCALE_IN", "vdu1_aspect", 1, "False") + vim_connection_info = vim_connection.VimConnectionInfo( + vim_type="kubernetes") + update = {'vim_connection_info': [vim_connection_info]} + scale_status = objects.ScaleInfo( + aspect_id='vdu1_aspect', scale_level=1) + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, scale_status=scale_status, + **update) + mock_vnfd_dict.return_value = fakes.vnfd_dict_cnf() + mock_yaml_safe_load.return_value = fakes.vnfd_dict_cnf() + mock_invoke.side_effect = [ + # Kubernetes.get_scale_in_ids called in _scale_vnf_pre() + [[], [], None, None], + # Kubernetes.scale called in scale() + None, + # Kubernetes.scale_wait called in scale() + None, + # scale_resource_update called in _scale_resource_update() + None] + mock_vnf_package_vnfd.return_value = fakes.return_vnf_package_vnfd() + driver = vnflcm_driver.VnfLcmDriver() + vim_obj = {'vim_id': uuidsentinel.vim_id, + 'vim_name': 'fake_vim', + 'vim_type': 'kubernetes', + 'vim_auth': { + 'auth_url': 'http://localhost:8443', + 'password': 'test_pw', + 'username': 'test_user', + 'project_name': 'test_project'}} + self.vim_client.get_vim.return_value = vim_obj + driver.scale_vnf(self.context, vnf_info, vnf_instance, + scale_vnf_request) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(yaml, "safe_load") + @mock.patch('tacker.vnflcm.utils._get_vnfd_dict') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfLcmOpOcc, "save") + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(driver_manager.DriverManager, "invoke") + def test_scale_out_cnf(self, mock_invoke, mock_vnf_instance_get_by_id, + mock_lcm_save, mock_vnf_package_vnfd, mock_vnfd_dict, + mock_yaml_safe_load, mock_get_service_plugins): + vnf_info = fakes.vnf_dict_cnf() + vnf_info['vnf_lcm_op_occ'] = fakes.vnflcm_scale_out_cnf() + vnf_info['scale_level'] = 0 + vnf_info['after_scale_level'] = 1 + vnf_info['notification'] = {} + scale_vnf_request = fakes.scale_request( + "SCALE_OUT", "vdu1_aspect", 1, "False") + vim_connection_info = vim_connection.VimConnectionInfo( + vim_type="kubernetes") + update = {'vim_connection_info': [vim_connection_info]} + scale_status = objects.ScaleInfo( + aspect_id='vdu1_aspect', scale_level=1) + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, scale_status=scale_status, + **update) + mock_vnf_instance_get_by_id.return_value = vnf_instance + mock_vnf_package_vnfd.return_value = fakes.return_vnf_package_vnfd() + mock_vnfd_dict.return_value = fakes.vnfd_dict_cnf() + mock_yaml_safe_load.return_value = fakes.vnfd_dict_cnf() + driver = vnflcm_driver.VnfLcmDriver() + vim_obj = {'vim_id': uuidsentinel.vim_id, + 'vim_name': 'fake_vim', + 'vim_type': 'kubernetes', + 'vim_auth': { + 'auth_url': 'http://localhost:8443', + 'password': 'test_pw', + 'username': 'test_user', + 'project_name': 'test_project'}} + self.vim_client.get_vim.return_value = vim_obj + driver.scale_vnf(self.context, vnf_info, vnf_instance, + scale_vnf_request) + @mock.patch.object(TackerManager, 'get_service_plugins', return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(VnfLcmDriver, @@ -2068,3 +2175,69 @@ class TestVnflcmDriver(db_base.SqlTestCase): self.assertEqual(1, mock_scale.call_count) self.assertEqual(1, mock_wait.call_count) self.assertEqual(2, mock_scale_resource.call_count) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(VnfLcmDriver, + '_init_mgmt_driver_hash') + @mock.patch('tacker.vnflcm.utils._get_vnfd_dict') + @mock.patch.object(yaml, "safe_load") + @mock.patch.object(objects.VnfLcmOpOcc, "save") + @mock.patch.object(VNFLcmRPCAPI, "send_notification") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(vnflcm_driver.VnfLcmDriver, "_update_vnf_rollback_pre") + @mock.patch.object(vnflcm_driver.VnfLcmDriver, "_update_vnf_rollback") + def test_rollback_vnf_scale_cnf( + self, + mock_update, + mock_up, + mock_insta_save, + mock_notification, + mock_lcm_save, + mock_yaml_safe_load, + mock_vnfd_dict, + mock_init_hash, + mock_get_service_plugins): + mock_init_hash.return_value = { + "vnflcm_noop": "ffea638bfdbde3fb01f191bbe75b031859" + "b18d663b127100eb72b19eecd7ed51" + } + vim_connection_info = vim_connection.VimConnectionInfo( + vim_type="kubernetes") + update = {'vim_connection_info': [vim_connection_info]} + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, **update) + + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + vnf_instance.instantiated_vnf_info.scale_status = [] + vnf_instance.instantiated_vnf_info.scale_status.append( + objects.ScaleInfo(aspect_id='vdu1_aspect', scale_level=0)) + vnf_lcm_op_occs = fakes.vnflcm_rollback() + vnf_lcm_op_occs.operation_params = \ + '{"type": "SCALE_OUT", "aspect_id": "vdu1_aspect"}' + vnf_info = fakes.vnf_dict_cnf() + vnf_info['vnf_lcm_op_occ'] = vnf_lcm_op_occs + vnf_info['scale_level'] = 1 + mock_vnfd_dict.return_value = fakes.vnfd_dict_cnf() + operation_params = jsonutils.loads(vnf_lcm_op_occs.operation_params) + mock_yaml_safe_load.return_value = fakes.vnfd_dict_cnf() + vim_obj = {'vim_id': uuidsentinel.vim_id, + 'vim_name': 'fake_vim', + 'vim_type': 'kubernetes', + 'vim_auth': { + 'auth_url': 'http://localhost:8443', + 'password': 'test_pw', + 'username': 'test_user', + 'project_name': 'test_project'}} + self.vim_client.get_vim.return_value = vim_obj + + self._mock_vnf_manager() + driver = vnflcm_driver.VnfLcmDriver() + + driver.rollback_vnf( + self.context, + vnf_info, + vnf_instance, + operation_params) + self.assertEqual(1, mock_lcm_save.call_count) diff --git a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py index 5528e2aff..bb77995bc 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py +++ b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py @@ -15,6 +15,8 @@ from kubernetes import client +from tacker.db.db_sqlalchemy import models +from tacker.tests import uuidsentinel CREATE_K8S_FALSE_VALUE = None @@ -982,3 +984,44 @@ def fake_pod_list(): ) )] ) + + +def get_scale_policy(type, aspect_id='vdu1', delta_num=1): + policy = dict() + policy['vnf_instance_id'] = uuidsentinel.vnf_instance_id + policy['action'] = type + policy['name'] = aspect_id + policy['delta_num'] = delta_num + policy['vdu_defs'] = { + 'VDU1': { + 'type': 'tosca.nodes.nfv.Vdu.Compute', + 'properties': { + 'name': 'fake_name', + 'description': 'test description', + 'vdu_profile': { + 'min_number_of_instances': 1, + 'max_number_of_instances': 3}}}} + + return policy + + +def get_vnf_resource_list(kind, name='fake_name'): + vnf_resource = models.VnfResource() + vnf_resource.vnf_instance_id = uuidsentinel.vnf_instance_id + vnf_resource.resource_name = \ + _("fake_namespace,{name}").format(name=name) + vnf_resource.resource_type = \ + _("v1,{kind}").format(kind=kind) + return [vnf_resource] + + +def get_fake_pod_info(kind, name='fake_name', pod_status='Running'): + if kind == 'Deployment': + pod_name = _('{name}-1234567890-abcde').format(name=name) + elif kind == 'ReplicaSet': + pod_name = _('{name}-12345').format(name=name) + elif kind == 'StatefulSet': + pod_name = _('{name}-1').format(name=name) + return client.V1Pod( + metadata=client.V1ObjectMeta(name=pod_name), + status=client.V1PodStatus(phase=pod_status)) diff --git a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver.py b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver.py index e5b3fc5ab..769df7f36 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver.py +++ b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver.py @@ -30,6 +30,7 @@ from tacker.objects import vnf_package_vnfd from tacker.objects import vnf_resources as vnf_resource_obj from tacker.tests.unit import base from tacker.tests.unit.db import utils +from tacker.tests.unit.vnflcm import fakes as vnflcm_fakes from tacker.tests.unit.vnfm.infra_drivers.kubernetes import fakes from tacker.tests.unit.vnfm.infra_drivers.openstack.fixture_data import \ fixture_data_utils as fd_utils @@ -1799,3 +1800,490 @@ class TestKubernetes(base.TestCase): "{'namespace': 'test', 'name': " + "'curry-test001', 'apiVersion': 'apps/v1', " + "'kind': 'Deployment', 'status': 'Creating'}") + + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_deployment_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_in_deployment(self, mock_vnf_resource_list, + mock_read_namespaced_deployment_scale, + mock_patch_namespaced_deployment_scale): + policy = fakes.get_scale_policy(type='in') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment') + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=2), + status=client.V1ScaleStatus(replicas=2)) + mock_patch_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.kubernetes.scale(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None) + mock_read_namespaced_deployment_scale.assert_called_once() + mock_patch_namespaced_deployment_scale.assert_called_once() + + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_stateful_set_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_stateful_set_scale') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_in_stateful_set(self, mock_vnf_resource_list, + mock_read_namespaced_stateful_set_scale, + mock_patch_namespaced_stateful_set_scale): + policy = fakes.get_scale_policy(type='in') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='StatefulSet') + mock_read_namespaced_stateful_set_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=2), + status=client.V1ScaleStatus(replicas=2)) + mock_patch_namespaced_stateful_set_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.kubernetes.scale(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None) + mock_read_namespaced_stateful_set_scale.assert_called_once() + mock_patch_namespaced_stateful_set_scale.assert_called_once() + + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_replica_set_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_replica_set_scale') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_in_replica_set(self, mock_vnf_resource_list, + mock_read_namespaced_replica_set_scale, + mock_patch_namespaced_replica_set_scale): + policy = fakes.get_scale_policy(type='in') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='ReplicaSet') + mock_read_namespaced_replica_set_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=2), + status=client.V1ScaleStatus(replicas=2)) + mock_patch_namespaced_replica_set_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.kubernetes.scale(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None) + mock_read_namespaced_replica_set_scale.assert_called_once() + mock_patch_namespaced_replica_set_scale.assert_called_once() + + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_deployment_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_out(self, mock_vnf_resource_list, + mock_read_namespaced_deployment_scale, + mock_patch_namespaced_deployment_scale): + policy = fakes.get_scale_policy(type='out') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment') + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + mock_patch_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=2), + status=client.V1ScaleStatus(replicas=2)) + self.kubernetes.scale(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None) + mock_read_namespaced_deployment_scale.assert_called_once() + mock_patch_namespaced_deployment_scale.assert_called_once() + + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_target_not_found(self, mock_vnf_resource_list): + policy = fakes.get_scale_policy(type='in') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Depoyment', name='other_name') + self.assertRaises(vnfm.CNFScaleFailed, + self.kubernetes.scale, + self.context, None, + utils.get_vim_auth_obj(), policy, None) + + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_out_of_target_kind(self, mock_vnf_resource_list): + policy = fakes.get_scale_policy(type='in') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Pod') + self.assertRaises(vnfm.CNFScaleFailed, + self.kubernetes.scale, + self.context, None, + utils.get_vim_auth_obj(), policy, None) + + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_in_less_than_min_replicas(self, mock_vnf_resource_list, + mock_read_namespaced_deployment_scale): + policy = fakes.get_scale_policy(type='in') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment') + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.assertRaises(vnfm.CNFScaleFailed, + self.kubernetes.scale, + self.context, None, + utils.get_vim_auth_obj(), policy, None) + + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_out_over_max_replicas(self, mock_vnf_resource_list, + mock_read_namespaced_deployment_scale): + policy = fakes.get_scale_policy(type='out') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment') + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=3), + status=client.V1ScaleStatus(replicas=3)) + self.assertRaises(vnfm.CNFScaleFailed, + self.kubernetes.scale, + self.context, None, + utils.get_vim_auth_obj(), policy, None) + + def _test_scale_legacy(self, scale_type, + current_replicas, after_replicas, + mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler): + policy = fakes.get_scale_policy(type=scale_type, aspect_id='SP1') + policy['instance_id'] = "fake_namespace,fake_name" + mock_vnf_resource_list.return_value = [] + mock_read_namespaced_deployment.return_value = \ + client.V1Deployment( + spec=client.V1ScaleSpec(replicas=current_replicas), + status=client.V1DeploymentStatus(replicas=current_replicas), + metadata=client.V1ObjectMeta(labels={'scaling_name': 'SP1'})) + mock_read_namespaced_horizontal_pod_autoscaler.return_value = \ + client.V1HorizontalPodAutoscaler( + spec=client.V1HorizontalPodAutoscalerSpec( + min_replicas=1, max_replicas=3, + scale_target_ref=client.V1CrossVersionObjectReference( + kind='Deployment', name='fake_name'))) + mock_patch_namespaced_deployment_scale.return_value = \ + client.V1Scale( + spec=client.V1ScaleSpec(replicas=after_replicas), + status=client.V1ScaleStatus(replicas=after_replicas)) + self.kubernetes.scale(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None) + + @mock.patch.object(client.AutoscalingV1Api, + 'read_namespaced_horizontal_pod_autoscaler') + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_deployment_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_legacy_in(self, mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler): + self._test_scale_legacy('in', 2, 1, + mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler) + mock_read_namespaced_deployment.assert_called_once() + mock_read_namespaced_horizontal_pod_autoscaler.assert_called_once() + mock_patch_namespaced_deployment_scale.assert_called_once() + + @mock.patch.object(client.AutoscalingV1Api, + 'read_namespaced_horizontal_pod_autoscaler') + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_deployment_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_legacy_out(self, mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler): + self._test_scale_legacy('out', 2, 3, + mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler) + mock_read_namespaced_deployment.assert_called_once() + mock_read_namespaced_horizontal_pod_autoscaler.assert_called_once() + mock_patch_namespaced_deployment_scale.assert_called_once() + + @mock.patch.object(client.AutoscalingV1Api, + 'read_namespaced_horizontal_pod_autoscaler') + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_deployment_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_legacy_in_less_than_min(self, mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler): + self._test_scale_legacy('in', 1, 1, + mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler) + mock_read_namespaced_deployment.assert_called_once() + mock_read_namespaced_horizontal_pod_autoscaler.assert_called_once() + mock_patch_namespaced_deployment_scale.assert_called_once() + + @mock.patch.object(client.AutoscalingV1Api, + 'read_namespaced_horizontal_pod_autoscaler') + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_deployment_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_legacy_out_over_max(self, mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler): + self._test_scale_legacy('out', 3, 3, + mock_vnf_resource_list, + mock_read_namespaced_deployment, + mock_patch_namespaced_deployment_scale, + mock_read_namespaced_horizontal_pod_autoscaler) + mock_read_namespaced_deployment.assert_called_once() + mock_read_namespaced_horizontal_pod_autoscaler.assert_called_once() + mock_patch_namespaced_deployment_scale.assert_called_once() + + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_deployment(self, mock_vnf_resource_list, + mock_list_namespaced_pod, + mock_read_namespaced_deployment_scale): + policy = fakes.get_scale_policy(type='out') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment') + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info(kind='Deployment')]) + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.kubernetes.scale_wait(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None, + last_event_id=None) + mock_list_namespaced_pod.assert_called_once() + + @mock.patch.object(client.AppsV1Api, 'read_namespaced_stateful_set_scale') + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_stateful_set(self, mock_vnf_resource_list, + mock_list_namespaced_pod, + mock_read_namespaced_stateful_set_scale): + policy = fakes.get_scale_policy(type='out') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='StatefulSet') + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info(kind='StatefulSet')]) + mock_read_namespaced_stateful_set_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.kubernetes.scale_wait(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None, + last_event_id=None) + mock_list_namespaced_pod.assert_called_once() + + @mock.patch.object(client.AppsV1Api, 'read_namespaced_replica_set_scale') + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_replica_set(self, mock_vnf_resource_list, + mock_list_namespaced_pod, + mock_read_namespaced_replica_set_scale): + policy = fakes.get_scale_policy(type='out') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='ReplicaSet') + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info(kind='ReplicaSet')]) + mock_read_namespaced_replica_set_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.kubernetes.scale_wait(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None, + last_event_id=None) + mock_list_namespaced_pod.assert_called_once() + + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_target_not_found(self, mock_vnf_resource_list): + policy = fakes.get_scale_policy(type='out') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Depoyment', name='other_name') + self.assertRaises(vnfm.CNFScaleWaitFailed, + self.kubernetes.scale_wait, + self.context, None, + utils.get_vim_auth_obj(), policy, None, None) + + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_retry_over(self, mock_vnf_resource_list, + mock_list_namespaced_pod, + mock_read_namespaced_deployment_scale): + policy = fakes.get_scale_policy(type='out') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment') + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info( + kind='Deployment', pod_status='Pending')]) + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=2), + status=client.V1ScaleStatus(replicas=2)) + self.assertRaises(vnfm.CNFScaleWaitFailed, + self.kubernetes.scale_wait, + self.context, None, + utils.get_vim_auth_obj(), policy, None, None) + + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_status_unknown(self, mock_vnf_resource_list, + mock_list_namespaced_pod, + mock_read_namespaced_deployment_scale): + policy = fakes.get_scale_policy(type='out') + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment') + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info( + kind='Deployment', pod_status='Unknown')]) + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=2), + status=client.V1ScaleStatus(replicas=2)) + self.assertRaises(vnfm.CNFScaleWaitFailed, + self.kubernetes.scale_wait, + self.context, None, + utils.get_vim_auth_obj(), policy, None, None) + + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_legacy(self, mock_vnf_resource_list, + mock_list_namespaced_pod): + policy = fakes.get_scale_policy(type='out', aspect_id='SP1') + policy['instance_id'] = "fake_namespace,fake_name" + mock_vnf_resource_list.return_value = [] + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info( + kind='Deployment', pod_status='Running')]) + self.kubernetes.scale_wait(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + policy=policy, + region_name=None, + last_event_id=None) + mock_list_namespaced_pod.assert_called_once() + + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_legacy_retry_over(self, mock_vnf_resource_list, + mock_list_namespaced_pod): + policy = fakes.get_scale_policy(type='out', aspect_id='SP1') + policy['instance_id'] = "fake_namespace,fake_name" + mock_vnf_resource_list.return_value = [] + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info( + kind='Deployment', pod_status='Pending')]) + self.assertRaises(vnfm.VNFCreateWaitFailed, + self.kubernetes.scale_wait, + self.context, None, + utils.get_vim_auth_obj(), policy, None, None) + + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_scale_wait_legacy_status_unknown(self, mock_vnf_resource_list, + mock_list_namespaced_pod): + policy = fakes.get_scale_policy(type='out', aspect_id='SP1') + policy['instance_id'] = "fake_namespace,fake_name" + mock_vnf_resource_list.return_value = [] + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info( + kind='Deployment', pod_status='Unknown')]) + self.assertRaises(vnfm.VNFCreateWaitFailed, + self.kubernetes.scale_wait, + self.context, None, + utils.get_vim_auth_obj(), policy, None, None) + + @mock.patch.object(client.AppsV1Api, 'patch_namespaced_deployment_scale') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch('tacker.vnflcm.utils._get_vnfd_dict') + @mock.patch.object(objects.VnfPackageVnfd, "get_by_id") + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_scale_in_reverse(self, mock_vnf_instance_get_by_id, + mock_vnf_resource_list, + mock_vnf_package_vnfd_get_by_id, + mock_vnfd_dict, + mock_read_namespaced_deployment_scale, + mock_patch_namespaced_deployment_scale): + vnf_info = vnflcm_fakes.vnf_dict_cnf() + vnf_info['vnf_lcm_op_occ'] = vnflcm_fakes.vnflcm_scale_out_cnf() + scale_vnf_request = vnflcm_fakes.scale_request( + "SCALE_OUT", "vdu1_aspect", 1, "False") + scale_status = objects.ScaleInfo( + aspect_id='vdu1_aspect', scale_level=1) + mock_vnf_instance_get_by_id.return_value = \ + vnflcm_fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, + scale_status=scale_status) + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment', name='vdu1') + mock_vnf_package_vnfd_get_by_id.return_value = \ + vnflcm_fakes.return_vnf_package_vnfd() + mock_vnfd_dict.return_value = vnflcm_fakes.vnfd_dict_cnf() + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=2), + status=client.V1ScaleStatus(replicas=2)) + mock_patch_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.kubernetes.scale_in_reverse(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + vnf_info=vnf_info, + scale_vnf_request=scale_vnf_request, + region_name=None, + scale_name_list=None, + grp_id=None) + mock_read_namespaced_deployment_scale.assert_called_once() + mock_patch_namespaced_deployment_scale.assert_called_once() + + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment_scale') + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch('tacker.vnflcm.utils._get_vnfd_dict') + @mock.patch.object(objects.VnfPackageVnfd, "get_by_id") + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_scale_update_wait(self, mock_vnf_instance_get_by_id, + mock_vnf_resource_list, + mock_vnf_package_vnfd_get_by_id, + mock_vnfd_dict, + mock_list_namespaced_pod, + mock_read_namespaced_deployment_scale): + vnf_info = vnflcm_fakes.vnf_dict_cnf() + vnf_info['vnf_lcm_op_occ'] = vnflcm_fakes.vnflcm_scale_out_cnf() + scale_status = objects.ScaleInfo( + aspect_id='vdu1_aspect', scale_level=1) + mock_vnf_instance_get_by_id.return_value = \ + vnflcm_fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, + scale_status=scale_status) + mock_vnf_resource_list.return_value = \ + fakes.get_vnf_resource_list(kind='Deployment', name='vdu1') + mock_vnf_package_vnfd_get_by_id.return_value = \ + vnflcm_fakes.return_vnf_package_vnfd() + mock_vnfd_dict.return_value = vnflcm_fakes.vnfd_dict_cnf() + mock_list_namespaced_pod.return_value = \ + client.V1PodList(items=[ + fakes.get_fake_pod_info(kind='Deployment', name='vdu1')]) + mock_read_namespaced_deployment_scale.return_value = \ + client.V1Scale(spec=client.V1ScaleSpec(replicas=1), + status=client.V1ScaleStatus(replicas=1)) + self.kubernetes.scale_update_wait(context=self.context, plugin=None, + auth_attr=utils.get_vim_auth_obj(), + vnf_info=vnf_info, + region_name=None) + mock_list_namespaced_pod.assert_called_once() diff --git a/tacker/tosca/utils.py b/tacker/tosca/utils.py index 4d388e643..3da2722ca 100644 --- a/tacker/tosca/utils.py +++ b/tacker/tosca/utils.py @@ -700,12 +700,15 @@ def convert_inst_req_info(heat_dict, inst_req_info, tosca): aspect_id_dict = {} # { vduId: initialDelta } vdu_delta_dict = {} + # { aspectId: maxScaleLevel } + aspect_max_level_dict = {} tosca_policies = tosca.topology_template.policies default_inst_level_id = _extract_policy_info( tosca_policies, inst_level_dict, aspect_delta_dict, aspect_id_dict, - aspect_vdu_dict, vdu_delta_dict) + aspect_vdu_dict, vdu_delta_dict, + aspect_max_level_dict) if inst_level_id is not None: # Case which instLevelId is defined. @@ -848,7 +851,8 @@ def _convert_ext_mng_vl(heat_dict, vl_name, vl_id): def _extract_policy_info(tosca_policies, inst_level_dict, aspect_delta_dict, aspect_id_dict, - aspect_vdu_dict, vdu_delta_dict): + aspect_vdu_dict, vdu_delta_dict, + aspect_max_level_dict): default_inst_level_id = None if tosca_policies: for p in tosca_policies: @@ -885,7 +889,8 @@ def _extract_policy_info(tosca_policies, inst_level_dict, delta_names = aspect_val['step_deltas'] delta_name = delta_names[0] aspect_id_dict[aspect_id] = delta_name - + aspect_max_level_dict[aspect_id] = \ + aspect_val['max_scale_level'] elif p.type == ETSI_INITIAL_DELTA: vdus = p.targets initial_delta = \ diff --git a/tacker/vnflcm/utils.py b/tacker/vnflcm/utils.py index f4e637ad3..33bd72cf3 100644 --- a/tacker/vnflcm/utils.py +++ b/tacker/vnflcm/utils.py @@ -994,6 +994,7 @@ def _convert_desired_capacity(inst_level_id, vnfd_dict, vdu): inst_level_dict = {} aspect_id_dict = {} vdu_delta_dict = {} + aspect_max_level_dict = {} desired_capacity = 1 tosca = tosca_template.ToscaTemplate(parsed_params={}, a_file=False, @@ -1002,7 +1003,8 @@ def _convert_desired_capacity(inst_level_id, vnfd_dict, vdu): default_inst_level_id = toscautils._extract_policy_info( tosca_policies, inst_level_dict, aspect_delta_dict, aspect_id_dict, - aspect_vdu_dict, vdu_delta_dict) + aspect_vdu_dict, vdu_delta_dict, + aspect_max_level_dict) if vdu_delta_dict.get(vdu) is None: return desired_capacity @@ -1064,3 +1066,89 @@ def get_base_nest_hot_dict(context, flavour_id, vnfd_id): LOG.debug("Loaded base hot: %s", base_hot_dict) LOG.debug("Loaded nested_hot_dict: %s", nested_hot_dict) return base_hot_dict, nested_hot_dict + + +def get_extract_policy_infos(tosca): + aspect_delta_dict = {} + aspect_vdu_dict = {} + inst_level_dict = {} + aspect_id_dict = {} + vdu_delta_dict = {} + aspect_max_level_dict = {} + + tosca_policies = tosca.topology_template.policies + default_inst_level_id = toscautils._extract_policy_info( + tosca_policies, inst_level_dict, + aspect_delta_dict, aspect_id_dict, + aspect_vdu_dict, vdu_delta_dict, + aspect_max_level_dict) + + extract_policy_infos = dict() + extract_policy_infos['inst_level_dict'] = inst_level_dict + extract_policy_infos['aspect_delta_dict'] = aspect_delta_dict + extract_policy_infos['aspect_id_dict'] = aspect_id_dict + extract_policy_infos['aspect_vdu_dict'] = aspect_vdu_dict + extract_policy_infos['vdu_delta_dict'] = vdu_delta_dict + extract_policy_infos['aspect_max_level_dict'] = aspect_max_level_dict + extract_policy_infos['default_inst_level_id'] = default_inst_level_id + + return extract_policy_infos + + +def get_scale_delta_num(extract_policy_infos, aspect_id): + delta_num = 1 + + if extract_policy_infos['aspect_id_dict'] is None: + return delta_num + delta_id = extract_policy_infos['aspect_id_dict'].get(aspect_id) + if delta_id is None: + return delta_num + delta_num = \ + extract_policy_infos['aspect_delta_dict'].get(aspect_id).get(delta_id) + + return delta_num + + +def get_default_scale_status(context, vnf_instance, vnfd_dict): + default_scale_status = None + + vnfd_dict = _get_vnfd_dict(context, + vnf_instance.vnfd_id, + vnf_instance.instantiated_vnf_info.flavour_id) + tosca = tosca_template.ToscaTemplate(parsed_params={}, a_file=False, + yaml_dict_tpl=vnfd_dict) + extract_policy_infos = get_extract_policy_infos(tosca) + + if extract_policy_infos['inst_level_dict'] is None: + return default_scale_status + default_inst_level_id = extract_policy_infos['default_inst_level_id'] + default_al_dict = \ + extract_policy_infos['inst_level_dict'].get(default_inst_level_id) + if default_al_dict is None: + return default_scale_status + default_scale_status = [] + for aspect_id, level_num in default_al_dict.items(): + default_scale_status.append( + objects.ScaleInfo( + aspect_id=aspect_id, + scale_level=level_num)) + + return default_scale_status + + +def get_target_vdu_def_dict(extract_policy_infos, aspect_id, tosca): + vdu_def_dict = {} + + tosca_node_tpls = tosca.topology_template.nodetemplates + + if extract_policy_infos['aspect_vdu_dict'] is None: + return vdu_def_dict + vdus = extract_policy_infos['aspect_vdu_dict'].get(aspect_id) + if vdus is None: + return vdu_def_dict + for nt in tosca_node_tpls: + for node_name, node_value in nt.templates.items(): + if node_name in vdus: + vdu_def_dict[node_name] = node_value + + return vdu_def_dict diff --git a/tacker/vnflcm/vnflcm_driver.py b/tacker/vnflcm/vnflcm_driver.py index 74c9bf855..be9267c1f 100644 --- a/tacker/vnflcm/vnflcm_driver.py +++ b/tacker/vnflcm/vnflcm_driver.py @@ -29,6 +29,7 @@ from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import encodeutils from oslo_utils import excutils +from toscaparser import tosca_template from tacker.common import driver_manager from tacker.common import exceptions @@ -92,6 +93,7 @@ def revert_to_error_scale(function): "instance %(id)s. Error: %(error)s", {"id": vnf_instance.id, "error": e}) try: + vnf_instance.task_state = None self._vnf_instance_update(context, vnf_instance) except Exception as e: LOG.warning("Failed to revert instantiation info for vnf " @@ -386,6 +388,15 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): if vnf_dict['attributes'].get('scaling_group_names'): vnf_instance.instantiated_vnf_info.scale_status = \ vnf_dict['scale_status'] + elif vnf_instance.instantiated_vnf_info: + default_scale_status = vnflcm_utils.\ + get_default_scale_status( + context=context, + vnf_instance=vnf_instance, + vnfd_dict=vnfd_dict) + if default_scale_status is not None: + vnf_instance.instantiated_vnf_info.scale_status = \ + default_scale_status try: self._vnf_manager.invoke( @@ -815,16 +826,21 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): vnf_lcm_op_occ.error_point = 7 vnf_instance.instantiated_vnf_info.scale_level =\ vnf_info['after_scale_level'] - scaleGroupDict = \ - jsonutils.loads(vnf_info['attributes']['scale_group']) - (scaleGroupDict - ['scaleGroupDict'][scale_vnf_request.aspect_id]['default']) =\ - vnf_info['res_num'] - vnf_info['attributes']['scale_group'] =\ - jsonutils.dump_as_bytes(scaleGroupDict) + if vim_connection_info.vim_type != 'kubernetes': + # NOTE(ueha): The logic of Scale for OpenStack VIM is widely hard + # coded with `vnf_info`. This dependency is to be refactored in + # future. + scaleGroupDict = \ + jsonutils.loads(vnf_info['attributes']['scale_group']) + (scaleGroupDict + ['scaleGroupDict'][scale_vnf_request.aspect_id]['default']) =\ + vnf_info['res_num'] + vnf_info['attributes']['scale_group'] =\ + jsonutils.dump_as_bytes(scaleGroupDict) vnf_lcm_op_occ = vnf_info['vnf_lcm_op_occ'] vnf_lcm_op_occ.operation_state = 'COMPLETED' vnf_lcm_op_occ.resource_changes = resource_changes + vnf_instance.task_state = None self._vnfm_plugin._update_vnf_scaling(context, vnf_info, 'PENDING_' + scale_vnf_request.type, 'ACTIVE', @@ -1084,10 +1100,33 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): LOG.debug( "is_reverse: %s", scale_vnf_request.additional_params.get('is_reverse')) - scale_json = vnf_info['attributes']['scale_group'] - scaleGroupDict = jsonutils.loads(scale_json) - key_aspect = scale_vnf_request.aspect_id - default = scaleGroupDict['scaleGroupDict'][key_aspect]['default'] + default = None + if vim_connection_info.vim_type == 'kubernetes': + policy['vnf_instance_id'] = \ + vnf_info['vnf_lcm_op_occ'].get('vnf_instance_id') + vnf_instance = objects.VnfInstance.get_by_id(context, + policy['vnf_instance_id']) + vnfd_dict = vnflcm_utils._get_vnfd_dict(context, + vnf_instance.vnfd_id, + vnf_instance.instantiated_vnf_info.flavour_id) + tosca = tosca_template.ToscaTemplate( + parsed_params={}, a_file=False, yaml_dict_tpl=vnfd_dict) + extract_policy_infos = vnflcm_utils.get_extract_policy_infos(tosca) + policy['vdu_defs'] = vnflcm_utils.get_target_vdu_def_dict( + extract_policy_infos=extract_policy_infos, + aspect_id=scale_vnf_request.aspect_id, + tosca=tosca) + policy['delta_num'] = vnflcm_utils.get_scale_delta_num( + extract_policy_infos=extract_policy_infos, + aspect_id=scale_vnf_request.aspect_id) + else: + # NOTE(ueha): The logic of Scale for OpenStack VIM is widely hard + # coded with `vnf_info`. This dependency is to be refactored in + # future. + scale_json = vnf_info['attributes']['scale_group'] + scaleGroupDict = jsonutils.loads(scale_json) + key_aspect = scale_vnf_request.aspect_id + default = scaleGroupDict['scaleGroupDict'][key_aspect]['default'] if (scale_vnf_request.type == 'SCALE_IN' and scale_vnf_request.additional_params['is_reverse'] == 'True'): self._vnf_manager.invoke( @@ -1132,26 +1171,32 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): region_name=vim_connection_info.access_info.get('region_name') ) else: - heat_template = vnf_info['attributes']['heat_template'] - policy_in_name = scale_vnf_request.aspect_id + '_scale_in' - policy_out_name = scale_vnf_request.aspect_id + '_scale_out' + cooldown = None + if vim_connection_info.vim_type != 'kubernetes': + # NOTE(ueha): The logic of Scale for OpenStack VIM is widely + # hard coded with `vnf_info`. This dependency is to be + # refactored in future. + heat_template = vnf_info['attributes']['heat_template'] + policy_in_name = scale_vnf_request.aspect_id + '_scale_in' + policy_out_name = scale_vnf_request.aspect_id + '_scale_out' - heat_resource = yaml.safe_load(heat_template) - if scale_vnf_request.type == 'SCALE_IN': - policy['action'] = 'in' - policy_temp = heat_resource['resources'][policy_in_name] - policy_prop = policy_temp['properties'] - cooldown = policy_prop.get('cooldown') - policy_name = policy_in_name - else: - policy['action'] = 'out' - policy_temp = heat_resource['resources'][policy_out_name] - policy_prop = policy_temp['properties'] - cooldown = policy_prop.get('cooldown') - policy_name = policy_out_name + heat_resource = yaml.safe_load(heat_template) + if scale_vnf_request.type == 'SCALE_IN': + policy['action'] = 'in' + policy_temp = heat_resource['resources'][policy_in_name] + policy_prop = policy_temp['properties'] + cooldown = policy_prop.get('cooldown') + policy_name = policy_in_name + else: + policy['action'] = 'out' + policy_temp = heat_resource['resources'][policy_out_name] + policy_prop = policy_temp['properties'] + cooldown = policy_prop.get('cooldown') + policy_name = policy_out_name + + policy_temp = heat_resource['resources'][policy_name] + policy_prop = policy_temp['properties'] - policy_temp = heat_resource['resources'][policy_name] - policy_prop = policy_temp['properties'] for i in range(scale_vnf_request.number_of_steps): last_event_id = self._vnf_manager.invoke( vim_connection_info.vim_type, @@ -1290,11 +1335,15 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): grp_id = None self._update_vnf_rollback_pre(context, vnf_info) if vnf_lcm_op_occs.operation == 'SCALE': - scaleGroupDict = jsonutils.loads( - vnf_info['attributes']['scale_group']) - cap_size = scaleGroupDict['scaleGroupDict'][operation_params - ['aspect_id']]['default'] - vnf_info['res_num'] = cap_size + if vim_connection_info.vim_type != 'kubernetes': + # NOTE(ueha): The logic of Scale for OpenStack VIM is widely + # hard coded with `vnf_info`. This dependency is to be + # refactored in future. + scaleGroupDict = jsonutils.loads( + vnf_info['attributes']['scale_group']) + cap_size = scaleGroupDict['scaleGroupDict'][operation_params + ['aspect_id']]['default'] + vnf_info['res_num'] = cap_size scale_vnf_request = objects.ScaleVnfRequest.obj_from_primitive( operation_params, context=context) for scale in vnf_instance.instantiated_vnf_info.scale_status: @@ -1403,6 +1452,7 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): status = 'ACTIVE' else: status = 'INACTIVE' + vnf_instance.task_state = None self._vnfm_plugin._update_vnf_rollback(context, vnf_info, 'ERROR', status, diff --git a/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py b/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py index daae8878f..a63654e1c 100644 --- a/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py +++ b/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py @@ -23,6 +23,7 @@ from kubernetes import client from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils +from toscaparser import tosca_template from tacker._i18n import _ from tacker.common.container import kubernetes_utils @@ -34,6 +35,7 @@ from tacker import objects from tacker.objects import vnf_package as vnf_package_obj from tacker.objects import vnf_package_vnfd as vnfd_obj from tacker.objects import vnf_resources as vnf_resource_obj +from tacker.vnflcm import utils as vnflcm_utils from tacker.vnfm.infra_drivers import abstract_driver from tacker.vnfm.infra_drivers.kubernetes.k8s import translate_outputs from tacker.vnfm.infra_drivers.kubernetes import translate_template @@ -1019,6 +1021,96 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, finally: self.clean_authenticate_vim(auth_cred, file_descriptor) + def _scale_legacy(self, policy, auth_cred): + LOG.debug("VNF are scaled by updating instance of deployment") + + app_v1_api_client = self.kubernetes.get_app_v1_api_client( + auth=auth_cred) + scaling_api_client = self.kubernetes.get_scaling_api_client( + auth=auth_cred) + deployment_names = policy['instance_id'].split(COMMA_CHARACTER) + policy_name = policy['name'] + policy_action = policy['action'] + + for i in range(0, len(deployment_names), 2): + namespace = deployment_names[i] + deployment_name = deployment_names[i + 1] + deployment_info = app_v1_api_client.\ + read_namespaced_deployment(namespace=namespace, + name=deployment_name) + scaling_info = scaling_api_client.\ + read_namespaced_horizontal_pod_autoscaler( + namespace=namespace, + name=deployment_name) + + replicas = deployment_info.status.replicas + scale_replicas = replicas + vnf_scaling_name = deployment_info.metadata.labels.\ + get("scaling_name") + if vnf_scaling_name == policy_name: + if policy_action == 'out': + scale_replicas = replicas + 1 + elif policy_action == 'in': + scale_replicas = replicas - 1 + + min_replicas = scaling_info.spec.min_replicas + max_replicas = scaling_info.spec.max_replicas + if (scale_replicas < min_replicas) or \ + (scale_replicas > max_replicas): + LOG.debug("Scaling replicas is out of range. The number of" + " replicas keeps %(number)s replicas", + {'number': replicas}) + scale_replicas = replicas + deployment_info.spec.replicas = scale_replicas + app_v1_api_client.patch_namespaced_deployment_scale( + namespace=namespace, + name=deployment_name, + body=deployment_info) + + def _call_read_scale_api(self, app_v1_api_client, namespace, name, kind): + """select kubernetes read scale api and call""" + def convert(name): + name_with_underscores = re.sub( + '(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', + name_with_underscores).lower() + snake_case_kind = convert(kind) + try: + read_scale_api = eval('app_v1_api_client.' + 'read_namespaced_%s_scale' % snake_case_kind) + response = read_scale_api(name=name, namespace=namespace) + except Exception as e: + error_reason = _("Failed the request to read a scale information." + " namespace: {namespace}, name: {name}," + " kind: {kind}, Reason: {exception}").format( + namespace=namespace, name=name, kind=kind, exception=e) + raise vnfm.CNFScaleFailed(reason=error_reason) + + return response + + def _call_patch_scale_api(self, app_v1_api_client, namespace, name, + kind, body): + """select kubernetes patch scale api and call""" + def convert(name): + name_with_underscores = re.sub( + '(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', + name_with_underscores).lower() + snake_case_kind = convert(kind) + try: + patch_scale_api = eval('app_v1_api_client.' + 'patch_namespaced_%s_scale' % snake_case_kind) + response = patch_scale_api(name=name, namespace=namespace, + body=body) + except Exception as e: + error_reason = _("Failed the request to update a scale information" + ". namespace: {namespace}, name: {name}," + " kind: {kind}, Reason: {exception}").format( + namespace=namespace, name=name, kind=kind, exception=e) + raise vnfm.CNFScaleFailed(reason=error_reason) + + return response + @log.log def scale(self, context, plugin, auth_attr, policy, region_name): """Scale function @@ -1027,58 +1119,154 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, The min_replicas and max_replicas is limited by the number of replicas of policy scaling when user define VNF descriptor. """ - LOG.debug("VNF are scaled by updating instance of deployment") # initialize Kubernetes APIs auth_cred, file_descriptor = self._get_auth_creds(auth_attr) + vnf_resources = objects.VnfResourceList.get_by_vnf_instance_id( + context, policy['vnf_instance_id']) try: - app_v1_api_client = self.kubernetes.get_app_v1_api_client( - auth=auth_cred) - scaling_api_client = self.kubernetes.get_scaling_api_client( - auth=auth_cred) - deployment_names = policy['instance_id'].split(COMMA_CHARACTER) - policy_name = policy['name'] - policy_action = policy['action'] + if not vnf_resources: + # execute legacy scale method + self._scale_legacy(policy, auth_cred) + else: + app_v1_api_client = self.kubernetes.get_app_v1_api_client( + auth=auth_cred) + aspect_id = policy['name'] + vdu_defs = policy['vdu_defs'] + is_found = False + error_reason = None + for vnf_resource in vnf_resources: + # The resource that matches the following is the resource + # to be scaled: + # The `name` of the resource stored in vnf_resource (the + # name defined in `metadata.name` of Kubernetes object + # file) matches the value of `properties.name` of VDU + # defined in VNFD. + name = vnf_resource.resource_name.\ + split(COMMA_CHARACTER)[1] + for vdu_id, vdu_def in vdu_defs.items(): + vdu_properties = vdu_def.get('properties') + if name == vdu_properties.get('name'): + namespace = vnf_resource.resource_name.\ + split(COMMA_CHARACTER)[0] + kind = vnf_resource.resource_type.\ + split(COMMA_CHARACTER)[1] + is_found = True + break + if is_found: + break + else: + error_reason = _( + "Target VnfResource for aspectId" + " {aspect_id} is not found in DB").format( + aspect_id=aspect_id) + raise vnfm.CNFScaleFailed(reason=error_reason) - for i in range(0, len(deployment_names), 2): - namespace = deployment_names[i] - deployment_name = deployment_names[i + 1] - deployment_info = app_v1_api_client.\ - read_namespaced_deployment(namespace=namespace, - name=deployment_name) - scaling_info = scaling_api_client.\ - read_namespaced_horizontal_pod_autoscaler( - namespace=namespace, - name=deployment_name) + target_kinds = ["Deployment", "ReplicaSet", "StatefulSet"] + if kind not in target_kinds: + error_reason = _( + "Target kind {kind} is out of scale target").\ + format(kind=kind) + raise vnfm.CNFScaleFailed(reason=error_reason) - replicas = deployment_info.status.replicas - scale_replicas = replicas - vnf_scaling_name = deployment_info.metadata.labels.\ - get("scaling_name") - if vnf_scaling_name == policy_name: - if policy_action == 'out': - scale_replicas = replicas + 1 - elif policy_action == 'in': - scale_replicas = replicas - 1 + scale_info = self._call_read_scale_api( + app_v1_api_client=app_v1_api_client, + namespace=namespace, + name=name, + kind=kind) - min_replicas = scaling_info.spec.min_replicas - max_replicas = scaling_info.spec.max_replicas + current_replicas = scale_info.status.replicas + vdu_profile = vdu_properties.get('vdu_profile') + if policy['action'] == 'out': + scale_replicas = current_replicas + policy['delta_num'] + elif policy['action'] == 'in': + scale_replicas = current_replicas - policy['delta_num'] + + max_replicas = vdu_profile.get('max_number_of_instances') + min_replicas = vdu_profile.get('min_number_of_instances') if (scale_replicas < min_replicas) or \ (scale_replicas > max_replicas): - LOG.debug("Scaling replicas is out of range. The number of" - " replicas keeps %(number)s replicas", - {'number': replicas}) - scale_replicas = replicas - deployment_info.spec.replicas = scale_replicas - app_v1_api_client.patch_namespaced_deployment_scale( + error_reason = _( + "The number of target replicas after" + " scaling [{after_replicas}] is out of range").\ + format( + after_replicas=scale_replicas) + raise vnfm.CNFScaleFailed(reason=error_reason) + + scale_info.spec.replicas = scale_replicas + self._call_patch_scale_api( + app_v1_api_client=app_v1_api_client, namespace=namespace, - name=deployment_name, - body=deployment_info) + name=name, + kind=kind, + body=scale_info) except Exception as e: LOG.error('Scaling VNF got an error due to %s', e) raise finally: self.clean_authenticate_vim(auth_cred, file_descriptor) + def _scale_wait_legacy(self, policy, auth_cred): + core_v1_api_client = self.kubernetes.get_core_v1_api_client( + auth=auth_cred) + deployment_info = policy['instance_id'].split(",") + + pods_information = self._get_pods_information( + core_v1_api_client=core_v1_api_client, + deployment_info=deployment_info) + status = self._get_pod_status(pods_information) + + stack_retries = self.STACK_RETRIES + error_reason = None + while status == 'Pending' and stack_retries > 0: + time.sleep(self.STACK_RETRY_WAIT) + + pods_information = self._get_pods_information( + core_v1_api_client=core_v1_api_client, + deployment_info=deployment_info) + status = self._get_pod_status(pods_information) + + # LOG.debug('status: %s', status) + stack_retries = stack_retries - 1 + + LOG.debug('VNF initializing status: %(service_name)s %(status)s', + {'service_name': str(deployment_info), 'status': status}) + + if stack_retries == 0 and status != 'Running': + error_reason = _("Resource creation is not completed within" + " {wait} seconds as creation of stack {stack}" + " is not completed").format( + wait=(self.STACK_RETRIES * + self.STACK_RETRY_WAIT), + stack=policy['instance_id']) + LOG.error("VNF Creation failed: %(reason)s", + {'reason': error_reason}) + raise vnfm.VNFCreateWaitFailed(reason=error_reason) + + elif stack_retries != 0 and status != 'Running': + raise vnfm.VNFCreateWaitFailed(reason=error_reason) + + def _is_match_pod_naming_rule(self, rsc_kind, rsc_name, pod_name): + match_result = None + if rsc_kind == 'Deployment': + # Expected example: name-012789abef-019az + match_result = re.match( + rsc_name + '-([0-9a-f]{10})-([0-9a-z]{5})+$', + pod_name) + elif rsc_kind == 'ReplicaSet': + # Expected example: name-019az + match_result = re.match( + rsc_name + '-([0-9a-z]{5})+$', + pod_name) + elif rsc_kind == 'StatefulSet': + # Expected example: name-0 + match_result = re.match( + rsc_name + '-[0-9]+$', + pod_name) + if match_result: + return True + else: + return False + def scale_wait(self, context, plugin, auth_attr, policy, region_name, last_event_id): """Scale wait function @@ -1088,47 +1276,87 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, """ # initialize Kubernetes APIs auth_cred, file_descriptor = self._get_auth_creds(auth_attr) + vnf_resources = objects.VnfResourceList.get_by_vnf_instance_id( + context, policy['vnf_instance_id']) try: - core_v1_api_client = self.kubernetes.get_core_v1_api_client( - auth=auth_cred) - deployment_info = policy['instance_id'].split(",") + if not vnf_resources: + # execute legacy scale_wait method + self._scale_wait_legacy(policy, auth_cred) + else: + core_v1_api_client = self.kubernetes.get_core_v1_api_client( + auth=auth_cred) + app_v1_api_client = self.kubernetes.get_app_v1_api_client( + auth=auth_cred) + aspect_id = policy['name'] + vdu_defs = policy['vdu_defs'] + is_found = False + error_reason = None + for vnf_resource in vnf_resources: + name = vnf_resource.resource_name.\ + split(COMMA_CHARACTER)[1] + for vdu_id, vdu_def in vdu_defs.items(): + vdu_properties = vdu_def.get('properties') + if name == vdu_properties.get('name'): + namespace = vnf_resource.resource_name.\ + split(COMMA_CHARACTER)[0] + kind = vnf_resource.resource_type.\ + split(COMMA_CHARACTER)[1] + is_found = True + break + if is_found: + break + else: + error_reason = _( + "Target VnfResource for aspectId {aspect_id}" + " is not found in DB").format( + aspect_id=aspect_id) + raise vnfm.CNFScaleWaitFailed(reason=error_reason) - pods_information = self._get_pods_information( - core_v1_api_client=core_v1_api_client, - deployment_info=deployment_info) - status = self._get_pod_status(pods_information) + scale_info = self._call_read_scale_api( + app_v1_api_client=app_v1_api_client, + namespace=namespace, + name=name, + kind=kind) + status = 'Pending' + stack_retries = self.STACK_RETRIES + error_reason = None + while status == 'Pending' and stack_retries > 0: + pods_information = list() + respone = core_v1_api_client.list_namespaced_pod( + namespace=namespace) + for pod in respone.items: + match_result = self._is_match_pod_naming_rule( + kind, name, pod.metadata.name) + if match_result: + pods_information.append(pod) - stack_retries = self.STACK_RETRIES - error_reason = None - while status == 'Pending' and stack_retries > 0: - time.sleep(self.STACK_RETRY_WAIT) + status = self._get_pod_status(pods_information) + if status == 'Running' and \ + scale_info.spec.replicas != len(pods_information): + status = 'Pending' - pods_information = self._get_pods_information( - core_v1_api_client=core_v1_api_client, - deployment_info=deployment_info) - status = self._get_pod_status(pods_information) + if status == 'Pending': + stack_retries = stack_retries - 1 + time.sleep(self.STACK_RETRY_WAIT) + elif status == 'Unknown': + error_reason = _( + "CNF Scale failed caused by the Pod status" + " is Unknown") + raise vnfm.CNFScaleWaitFailed(reason=error_reason) - # LOG.debug('status: %s', status) - stack_retries = stack_retries - 1 - - LOG.debug('VNF initializing status: %(service_name)s %(status)s', - {'service_name': str(deployment_info), 'status': status}) - - if stack_retries == 0 and status != 'Running': - error_reason = _("Resource creation is not completed within" - " {wait} seconds as creation of stack {stack}" - " is not completed").format( - wait=(self.STACK_RETRIES * - self.STACK_RETRY_WAIT), - stack=policy['instance_id']) - LOG.error("VNF Creation failed: %(reason)s", - {'reason': error_reason}) - raise vnfm.VNFCreateWaitFailed(reason=error_reason) - - elif stack_retries != 0 and status != 'Running': - raise vnfm.VNFCreateWaitFailed(reason=error_reason) + if stack_retries == 0 and status != 'Running': + error_reason = _( + "CNF Scale failed to complete within" + " {wait} seconds while waiting for the aspect_id" + " {aspect_id} to be scaled").format( + wait=(self.STACK_RETRIES * + self.STACK_RETRY_WAIT), + aspect_id=aspect_id) + LOG.error("CNF Scale failed: %(reason)s", + {'reason': error_reason}) + raise vnfm.CNFScaleWaitFailed(reason=error_reason) except Exception as e: - LOG.error('Scaling wait VNF got an error due to %s', e) + LOG.error('Scaling wait CNF got an error due to %s', e) raise finally: self.clean_authenticate_vim(auth_cred, file_descriptor) @@ -1315,7 +1543,8 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, vnf_dict, auth_attr, region_name): - pass + return_id_list = [] + return return_id_list def get_scale_in_ids(self, plugin, @@ -1325,7 +1554,11 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, auth_attr, region_name, number_of_steps): - pass + return_id_list = [] + return_name_list = [] + return_grp_id = None + return_res_num = None + return return_id_list, return_name_list, return_grp_id, return_res_num def scale_resource_update(self, context, vnf_instance, scale_vnf_request, @@ -1341,7 +1574,33 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, region_name, scale_name_list, grp_id): - pass + # NOTE(ueha): The `is_reverse` option is not supported in kubernetes + # VIM, and returns an error response to the user if `is_reverse` is + # true. However, since this method is called in the sequence of + # rollback operation, implementation is required. + vnf_instance_id = vnf_info['vnf_lcm_op_occ'].vnf_instance_id + aspect_id = scale_vnf_request.aspect_id + vnf_instance = objects.VnfInstance.get_by_id(context, vnf_instance_id) + vnfd_dict = vnflcm_utils._get_vnfd_dict(context, + vnf_instance.vnfd_id, + vnf_instance.instantiated_vnf_info.flavour_id) + tosca = tosca_template.ToscaTemplate(parsed_params={}, a_file=False, + yaml_dict_tpl=vnfd_dict) + extract_policy_infos = vnflcm_utils.get_extract_policy_infos(tosca) + + policy = dict() + policy['name'] = aspect_id + policy['action'] = 'in' + policy['vnf_instance_id'] = vnf_instance_id + policy['vdu_defs'] = vnflcm_utils.get_target_vdu_def_dict( + extract_policy_infos=extract_policy_infos, + aspect_id=scale_vnf_request.aspect_id, + tosca=tosca) + policy['delta_num'] = vnflcm_utils.get_scale_delta_num( + extract_policy_infos=extract_policy_infos, + aspect_id=scale_vnf_request.aspect_id) + + self.scale(context, plugin, auth_attr, policy, region_name) def scale_out_initial(self, context, @@ -1358,7 +1617,30 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, auth_attr, vnf_info, region_name): - pass + lcm_op_occ = vnf_info.get('vnf_lcm_op_occ') + vnf_instance_id = lcm_op_occ.get('vnf_instance_id') + operation_params = jsonutils.loads(lcm_op_occ.get('operation_params')) + scale_vnf_request = objects.ScaleVnfRequest.obj_from_primitive( + operation_params, context=context) + aspect_id = scale_vnf_request.aspect_id + vnf_instance = objects.VnfInstance.get_by_id(context, vnf_instance_id) + vnfd_dict = vnflcm_utils._get_vnfd_dict(context, + vnf_instance.vnfd_id, + vnf_instance.instantiated_vnf_info.flavour_id) + tosca = tosca_template.ToscaTemplate(parsed_params={}, a_file=False, + yaml_dict_tpl=vnfd_dict) + extract_policy_infos = vnflcm_utils.get_extract_policy_infos(tosca) + + policy = dict() + policy['name'] = aspect_id + policy['vnf_instance_id'] = lcm_op_occ.get('vnf_instance_id') + policy['vdu_defs'] = vnflcm_utils.get_target_vdu_def_dict( + extract_policy_infos=extract_policy_infos, + aspect_id=scale_vnf_request.aspect_id, + tosca=tosca) + + self.scale_wait(context, plugin, auth_attr, policy, + region_name, None) def get_cinder_list(self, vnf_info): @@ -1380,4 +1662,7 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, aspect_id, auth_attr, region_name): - pass + return_id_list = [] + return_name_list = [] + return_grp_id = None + return return_id_list, return_name_list, return_grp_id