Support CNF heal operations based on ETSI NFV

Implements Container based VNF heal operation support with ETSI
NFV-SOL002 and SOL003 v2.6.1 VNF Lifecycle Management. For "Heal VNFC
with SOL002", users can heal Pod (mapped as VNFC) that is singleton or
created using controller resources such as Kubernetes Deployment,
DaemonSet, StatefulSet and ReplicaSet. For "Heal VNF instance with
SOL003", users can heal entire VNF instance by termination and
instantiation of the VNF.

Other changes:
* Store Pod information to vnfcResourceInfo after instantiation.
* Update vnfcResourceInfo after scale operation.

Implements: blueprint support-cnf-heal
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-cnf-heal.html
Change-Id: I62bf8d67eb9b3889f2ae69be9bb45363d083f48e
changes/56/775656/11
Ayumu Ueha 2 years ago
parent 7e8a7351fa
commit b05a83db76
  1. 10
      releasenotes/notes/support-cnf-heal-with-etsi-sol-34b404bd2709ad51.yaml
  2. 3
      tacker/conductor/conductor_server.py
  3. 8
      tacker/extensions/vnfm.py
  4. 114
      tacker/tests/etc/samples/etsi/nfv/test_cnf_heal/Definitions/helloworld3_df_complex.yaml
  5. 105
      tacker/tests/etc/samples/etsi/nfv/test_cnf_heal/Definitions/helloworld3_df_simple.yaml
  6. 32
      tacker/tests/etc/samples/etsi/nfv/test_cnf_heal/Definitions/helloworld3_top.vnfd.yaml
  7. 53
      tacker/tests/etc/samples/etsi/nfv/test_cnf_heal/Definitions/helloworld3_types.yaml
  8. 22
      tacker/tests/etc/samples/etsi/nfv/test_cnf_heal/Files/kubernetes/deployment_heal_complex.yaml
  9. 22
      tacker/tests/etc/samples/etsi/nfv/test_cnf_heal/Files/kubernetes/deployment_heal_simple.yaml
  10. 12
      tacker/tests/etc/samples/etsi/nfv/test_cnf_heal/Files/kubernetes/pod_heal.yaml
  11. 19
      tacker/tests/etc/samples/etsi/nfv/test_cnf_heal/TOSCA-Metadata/TOSCA.meta
  12. 124
      tacker/tests/etc/samples/etsi/nfv/test_create_vnf_instance_and_instantiate_and_terminate_cnf_resources/Definitions/helloworld3_df_simple.yaml
  13. 2
      tacker/tests/etc/samples/etsi/nfv/test_create_vnf_instance_and_instantiate_and_terminate_cnf_resources/Files/kubernetes/daemon-set.yaml
  14. 2
      tacker/tests/etc/samples/etsi/nfv/test_create_vnf_instance_and_instantiate_and_terminate_cnf_resources/Files/kubernetes/deployment.yaml
  15. 2
      tacker/tests/etc/samples/etsi/nfv/test_create_vnf_instance_and_instantiate_and_terminate_cnf_resources/Files/kubernetes/pod.yaml
  16. 2
      tacker/tests/etc/samples/etsi/nfv/test_create_vnf_instance_and_instantiate_and_terminate_cnf_resources/Files/kubernetes/replicaset_service_secret.yaml
  17. 2
      tacker/tests/etc/samples/etsi/nfv/test_create_vnf_instance_and_instantiate_and_terminate_cnf_resources/Files/kubernetes/statefulset.yaml
  18. 10
      tacker/tests/etc/samples/etsi/nfv/test_create_vnf_instance_and_instantiate_and_terminate_cnf_resources/TOSCA-Metadata/TOSCA.meta
  19. 403
      tacker/tests/functional/sol_kubernetes/vnflcm/test_kubernetes_heal.py
  20. 105
      tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py
  21. 1237
      tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver.py
  22. 10
      tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py
  23. 6
      tacker/vnflcm/vnflcm_driver.py
  24. 5
      tacker/vnfm/infra_drivers/abstract_driver.py
  25. 6
      tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py
  26. 589
      tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py
  27. 2
      tacker/vnfm/infra_drivers/noop.py
  28. 5
      tacker/vnfm/infra_drivers/openstack/openstack.py

@ -0,0 +1,10 @@
---
features:
- |
Add Container based VNF heal operation support with ETSI NFV-SOL002 and
SOL003 v2.6.1 VNF Lifecycle Management. For "Heal VNFC with SOL002", users
can heal Pod (mapped as VNFC) that is singleton or created using controller
resources such as Kubernetes Deployment, DaemonSet, StatefulSet, and
ReplicaSet. For "Heal VNF instance with SOL003", users can heal entire VNF
instance by termination and instantiation of the VNF. And the VNFC resource
information are stored and updated for the heal operation of the Pod.

@ -737,7 +737,8 @@ class Conductor(manager.Manager):
self.vnf_manager.invoke(vim_connection_info.vim_type,
'post_vnf_instantiation', context=context,
vnf_instance=vnf_instance,
vim_connection_info=vim_connection_info)
vim_connection_info=vim_connection_info,
instantiate_vnf_req=instantiate_vnf_req)
except Exception as ex:
try:

@ -117,6 +117,14 @@ class CNFScaleWaitFailed(exceptions.TackerException):
message = _('CNF Scale Wait Failed with reason: %(reason)s')
class CNFHealFailed(exceptions.TackerException):
message = _('%(reason)s')
class CNFHealWaitFailed(exceptions.TackerException):
message = _('%(reason)s')
class ServiceTypeNotFound(exceptions.NotFound):
message = _('service type %(service_type_id)s could not be found')

@ -0,0 +1,114 @@
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: complex
requirements:
virtual_link_external: []
node_templates:
VNF:
type: company.provider.VNF
properties:
flavour_description: A flavour for multiple resources
VDU1:
type: tosca.nodes.nfv.Vdu.Compute
properties:
name: vdu1-heal-complex
description: kubernetes resource as VDU1
vdu_profile:
min_number_of_instances: 2
max_number_of_instances: 3
VDU2:
type: tosca.nodes.nfv.Vdu.Compute
properties:
name: vdu2-heal
description: kubernetes resource as VDU2
vdu_profile:
min_number_of_instances: 1
max_number_of_instances: 1
policies:
- scaling_aspects:
type: tosca.policies.nfv.ScalingAspects
properties:
aspects:
vdu1_aspect:
name: vdu1_aspect
description: vdu1 scaling aspect
max_scale_level: 1
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: 1
default_level: instantiation_level_1
- vdu1_instantiation_levels:
type: tosca.policies.nfv.VduInstantiationLevels
properties:
levels:
instantiation_level_1:
number_of_instances: 2
instantiation_level_2:
number_of_instances: 3
targets: [ VDU1 ]

@ -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-heal-simple
description: kubernetes controller resource as VDU
vdu_profile:
min_number_of_instances: 2
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: 1
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: 1
default_level: instantiation_level_1
- vdu1_instantiation_levels:
type: tosca.policies.nfv.VduInstantiationLevels
properties:
levels:
instantiation_level_1:
number_of_instances: 2
instantiation_level_2:
number_of_instances: 3
targets: [ VDU1 ]

@ -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_complex.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

@ -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,complex ] ]
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

@ -0,0 +1,22 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vdu1-heal-complex
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: webserver
template:
metadata:
labels:
app: webserver
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
protocol: TCP

@ -0,0 +1,22 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vdu1-heal-simple
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: webserver
template:
metadata:
labels:
app: webserver
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
protocol: TCP

@ -0,0 +1,12 @@
apiVersion: v1
kind: Pod
metadata:
name: vdu2-heal
namespace: default
spec:
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: webserver2
ports:
- containerPort: 8080

@ -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/pod_heal.yaml
Content-Type: application/yaml
Algorithm: SHA-256
Hash: 08fabdd52e8a386669f177c0a7a8a351b036bcde3bf399ca1816455d81dd191c
Name: Files/kubernetes/deployment_heal_simple.yaml
Content-Type: application/yaml
Algorithm: SHA-256
Hash: 39c9b301d04714c6b124b333057a22d316835c3cb340c4e2ebfadc296c3fbfbc
Name: Files/kubernetes/deployment_heal_complex.yaml
Content-Type: application/yaml
Algorithm: SHA-256
Hash: 06c018b9f4b231a604a6cd223a2552fecc4c6dc8bedf9325e84f7fe2b6fe8492

@ -0,0 +1,124 @@
tosca_definitions_version: tosca_simple_yaml_1_2
description: Simple deployment flavour for Sample VNF
imports:
- etsi_nfv_sol001_common_types.yaml
- etsi_nfv_sol001_vnfd_types.yaml
- 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: VDU1 compute node
vdu_profile:
min_number_of_instances: 1
max_number_of_instances: 1
VDU2:
type: tosca.nodes.nfv.Vdu.Compute
properties:
name: vdu2
description: VDU2 compute node
vdu_profile:
min_number_of_instances: 1
max_number_of_instances: 3
policies:
- scaling_aspects:
type: tosca.policies.nfv.ScalingAspects
properties:
aspects:
vdu2_aspect:
name: vdu2_aspect
description: vdu2 scaling aspect
max_scale_level: 2
step_deltas:
- delta_1
- VDU2_initial_delta:
type: tosca.policies.nfv.VduInitialDelta
properties:
initial_delta:
number_of_instances: 1
targets: [ VDU2 ]
- VDU2_scaling_aspect_deltas:
type: tosca.policies.nfv.VduScalingAspectDeltas
properties:
aspect: vdu2_aspect
deltas:
delta_1:
number_of_instances: 1
targets: [ VDU2 ]
- instantiation_levels:
type: tosca.policies.nfv.InstantiationLevels
properties:
levels:
instantiation_level_1:
description: Smallest size
scale_info:
vdu2_aspect:
scale_level: 0
instantiation_level_2:
description: Largest size
scale_info:
vdu2_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: 1
targets: [ VDU1 ]
- VDU2_instantiation_levels:
type: tosca.policies.nfv.VduInstantiationLevels
properties:
levels:
instantiation_level_1:
number_of_instances: 1
instantiation_level_2:
number_of_instances: 3
targets: [ VDU2 ]

@ -1,7 +1,7 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx
name: vdu1
namespace: default
spec:
selector:

@ -1,7 +1,7 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: curry-probe-test001
name: vdu2
namespace: default
spec:
replicas: 1

@ -2,7 +2,7 @@ apiVersion: v1
kind: Pod
metadata:
namespace: default
name: curry-endpoint-test001
name: vdu1
spec:
containers:
- image: celebdor/kuryr-demo

@ -28,7 +28,7 @@ metadata:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: curry-replicaset-multiple
name: vdu2
namespace: default
spec:
replicas: 2

@ -1,7 +1,7 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: curry-ns-statefulset
name: vdu2
namespace: default
spec:
selector:

@ -26,12 +26,12 @@ Hash: 4042352e0de6aa0ad28d44354bd8e0d62fc8e753c8f52b7edf69d2a7a25d8f8d
Name: Files/kubernetes/daemon-set.yaml
Content-Type: test-data
Algorithm: SHA-256
Hash: c0750df79c9ba2824b032b6a485764486b014021aa6dade5ef61f1c10569412f
Hash: f8ed04536a8795af4828b2f731225abc34986f9ea30237d9652669ca57d9d217
Name: Files/kubernetes/deployment.yaml
Content-Type: test-data
Algorithm: SHA-256
Hash: 6a40dfb06764394fb604ae807d1198bc2e2ee8aece3b9483dfde48e53f316a58
Hash: 80f160c9bdd9daa6d0111c8d40b5575946b8c0f23696aa8d91d20f313adae087
Name: Files/kubernetes/horizontal-pod-autoscaler.yaml
Content-Type: test-data
@ -91,12 +91,12 @@ Hash: 5d4d3d399e04cdba1f9c691ac7e690e295ff02b7c935abae873b68a83a858c50
Name: Files/kubernetes/pod.yaml
Content-Type: test-data
Algorithm: SHA-256
Hash: a708dcf5ba4d3a7c675f18b71484a32b7e4446e80e57dcc3035b8a921c3f659d
Hash: 6c97b1a8fc8d21a6a9e7ab1c383b49d3ec31f79a83de218f5537d18531ddfbd8
Name: Files/kubernetes/replicaset_service_secret.yaml
Content-Type: test-data
Algorithm: SHA-256
Hash: 8ed52e5e167890efd7fba29c748f717dff01d68b60ff9a06af178cbafdfdc765
Hash: 7d83ba61def65be3203b164b496057e4d062249804df82eba1831111cc4614a0
Name: Files/kubernetes/resource-quota.yaml
Content-Type: test-data
@ -116,7 +116,7 @@ Hash: 83bd9c40db8c798d0cab0e793a4b40a4ac7eca4fec4fba89ab4257d0f397db40
Name: Files/kubernetes/statefulset.yaml
Content-Type: test-data
Algorithm: SHA-256
Hash: d0beddd39f6808cb62094146778961b068871393df3474e0787145639a94f649
Hash: 6829939e8b30a36c69d0e84c65b36701712c89bfbe827536cba8c0cdb15a816b
Name: Files/kubernetes/storage-class.yaml
Content-Type: test-data

@ -0,0 +1,403 @@
# 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
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_HEAL_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
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) > VNF_PACKAGE_UPLOAD_TIMEOUT):
raise Exception("Failed to onboard vnf package, process could not"
" be completed within %d seconds", VNF_PACKAGE_UPLOAD_TIMEOUT)
time.sleep(RETRY_WAIT_TIME)
# remove temporarily created CSAR file
os.remove(file_path)
return vnf_package['id'], vnfd_id
def _delete_wait_vnf_instance(tacker_client, id):
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) > VNF_TERMINATE_TIMEOUT):
raise Exception("Failed to delete vnf instance, process could not"
" be completed within %d seconds", VNF_TERMINATE_TIMEOUT)
time.sleep(RETRY_WAIT_TIME)
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, process could not"
" be completed within %d seconds", timeout)
time.sleep(RETRY_WAIT_TIME)
class VnfLcmKubernetesHealTest(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_heal",
{"key": "sample_heal_functional"})
cls.vnf_instance_ids = []
super(VnfLcmKubernetesHealTest, cls).setUpClass()
@classmethod
def tearDownClass(cls):
# 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(VnfLcmKubernetesHealTest, cls).tearDownClass()
def setUp(self):
super(VnfLcmKubernetesHealTest, 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_heal-%s" % \
uuidutils.generate_uuid()
vnf_instance_description = "vnf instance for cnf heal 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 _terminate_vnf_instance(self, id):
# Terminate vnf forcefully
request_body = {
"terminationType": fields.VnfInstanceTerminationType.FORCEFUL,
}
url = os.path.join(self.base_vnf_instances_url, id, "terminate")
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,
instantiation_state=fields.VnfInstanceState.NOT_INSTANTIATED,
timeout=VNF_TERMINATE_TIMEOUT)
def _delete_vnf_instance(self, id):
_delete_wait_vnf_instance(self.tacker_client, id)
# verify vnf instance is deleted
url = os.path.join(self.base_vnf_instances_url, id)
resp, body = self.http_client.do_request(url, "GET")
self.assertEqual(404, resp.status_code)
def _heal_vnf_instance(self, id, vnfc_instance_id):
url = os.path.join(self.base_vnf_instances_url, id, "heal")
# generate body
request_body = {
"vnfcInstanceId": vnfc_instance_id}
resp, body = self.http_client.do_request(
url, "POST", body=jsonutils.dumps(request_body))
self.assertEqual(202, resp.status_code)
@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'):
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) > VNF_HEAL_TIMEOUT):
raise Exception("Failed to wait heal instance")
time.sleep(RETRY_WAIT_TIME)
def _get_vnfc_resource_info(self, vnf_instance):
inst_vnf_info = vnf_instance['instantiatedVnfInfo']
vnfc_resource_info = inst_vnf_info['vnfcResourceInfo']
return vnfc_resource_info
def test_heal_cnf_with_sol002(self):
"""Test heal as per SOL002 for CNF
This test will instantiate cnf. Heal API will be invoked as per SOL002
i.e. with vnfcInstanceId, so that the specified vnfc instance is healed
which includes Kubernetes resources (Pod and Deployment).
"""
# use def-files of singleton Pod and Deployment (replicas=2)
inst_additional_param = {
"lcm-kubernetes-def-files": [
"Files/kubernetes/deployment_heal_complex.yaml",
"Files/kubernetes/pod_heal.yaml"]}
vnf_instance = self._create_and_instantiate_vnf_instance(
"complex", inst_additional_param)
before_vnfc_rscs = self._get_vnfc_resource_info(vnf_instance)
# get vnfc_instance_id of heal target
deployment_target_vnfc = None
for vnfc_rsc in before_vnfc_rscs:
compute_resource = vnfc_rsc['computeResource']
rsc_kind = compute_resource['vimLevelResourceType']
if rsc_kind == 'Pod':
# target 1: Singleton Pod
pod_target_vnfc = vnfc_rsc
elif not deployment_target_vnfc:
# target 2: Deployment's Pod
deployment_target_vnfc = vnfc_rsc
else:
# not target: Deployment's remianing one
deployment_not_target_vnfc = vnfc_rsc
# test heal SOL-002 (partial heal)
vnfc_instance_id = \
[pod_target_vnfc['id'], deployment_target_vnfc['id']]
self._heal_vnf_instance(vnf_instance['id'], vnfc_instance_id)
# wait vnflcm_op_occs.operation_state become COMPLETE
self._wait_vnflcm_op_occs(self.context, vnf_instance['id'])
# check vnfcResourceInfo after heal operation
vnf_instance = _show_vnf_instance(
self.tacker_client, vnf_instance['id'])
after_vnfc_rscs = self._get_vnfc_resource_info(vnf_instance)
self.assertEqual(len(before_vnfc_rscs), len(after_vnfc_rscs))
for vnfc_rsc in after_vnfc_rscs:
after_pod_name = vnfc_rsc['computeResource']['resourceId']
if vnfc_rsc['id'] == pod_target_vnfc['id']:
# check stored pod name is not changed (Pod)
after_resource = pod_target_vnfc
compute_resource = after_resource['computeResource']
before_pod_name = compute_resource['resourceId']
self.assertEqual(after_pod_name, before_pod_name)
elif vnfc_rsc['id'] == deployment_target_vnfc['id']:
# check stored pod name is changed (Deployment)
after_resource = deployment_target_vnfc
compute_resource = after_resource['computeResource']
before_pod_name = compute_resource['resourceId']
self.assertNotEqual(after_pod_name, before_pod_name)
else:
# check stored pod name is not changed (not target)
after_resource = deployment_not_target_vnfc
compute_resource = after_resource['computeResource']
before_pod_name = compute_resource['resourceId']
self.assertEqual(after_pod_name, before_pod_name)
self._terminate_vnf_instance(vnf_instance['id'])
self._delete_vnf_instance(vnf_instance['id'])
def test_heal_cnf_with_sol003(self):
"""Test heal as per SOL003 for CNF
This test will instantiate cnf. Heal API will be invoked as per SOL003
i.e. without passing vnfcInstanceId, so that the entire vnf is healed
which includes Kubernetes resource (Deployment).
"""
# use def-files of Deployment (replicas=2)
inst_additional_param = {
"lcm-kubernetes-def-files": [
"Files/kubernetes/deployment_heal_simple.yaml"]}
vnf_instance = self._create_and_instantiate_vnf_instance(
"simple", inst_additional_param)
before_vnfc_rscs = self._get_vnfc_resource_info(vnf_instance)
# test heal SOL-003 (entire heal)
vnfc_instance_id = []
self._heal_vnf_instance(vnf_instance['id'], vnfc_instance_id)
# wait vnflcm_op_occs.operation_state become COMPLETE
self._wait_vnflcm_op_occs(self.context, vnf_instance['id'])
# check vnfcResourceInfo after heal operation
vnf_instance = _show_vnf_instance(
self.tacker_client, vnf_instance['id'])
after_vnfc_rscs = self._get_vnfc_resource_info(vnf_instance)
self.assertEqual(len(before_vnfc_rscs), len(after_vnfc_rscs))
# check id and pod name (as computeResource.resourceId) is changed
for before_vnfc_rsc in before_vnfc_rscs:
for after_vnfc_rsc in after_vnfc_rscs:
self.assertNotEqual(
before_vnfc_rsc['id'], after_vnfc_rsc['id'])
self.assertNotEqual(
before_vnfc_rsc['computeResource']['resourceId'],
after_vnfc_rsc['computeResource']['resourceId'])
# terminate vnf instance
self._terminate_vnf_instance(vnf_instance['id'])
self._delete_vnf_instance(vnf_instance['id'])

@ -13,9 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
from kubernetes import client
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from tacker.db.db_sqlalchemy import models
from tacker import objects
from tacker.objects import vim_connection
from tacker.tests import uuidsentinel
CREATE_K8S_FALSE_VALUE = None
@ -485,6 +490,17 @@ def fake_v1_deployment():
status=client.V1DeploymentStatus(
replicas=1,
ready_replicas=1
),
spec=client.V1DeploymentSpec(
replicas=2,
selector=client.V1LabelSelector(
match_labels={'app': 'webserver'}
),
template=client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
labels={'app': 'webserver'}
)
)
)
)
@ -515,6 +531,17 @@ def fake_v1_replica_set():
status=client.V1ReplicaSetStatus(
replicas=1,
ready_replicas=1
),
spec=client.V1ReplicaSetSpec(
replicas=2,
selector=client.V1LabelSelector(
match_labels={'app': 'webserver'}
),
template=client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
labels={'app': 'webserver'}
)
)
)
)
@ -923,6 +950,16 @@ def fake_daemon_set():
desired_number_scheduled=13,
current_number_scheduled=4,
number_misscheduled=2,
),
spec=client.V1DaemonSetSpec(
selector=client.V1LabelSelector(
match_labels={'app': 'webserver'}
),
template=client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
labels={'app': 'webserver'}
)
)
)
)
@ -1015,13 +1052,65 @@ def get_vnf_resource_list(kind, name='fake_name'):
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)
def get_fake_pod_info(kind, name='fake_name', pod_status='Running',
pod_name=None):
if not pod_name:
if kind == 'Deployment':
pod_name = _('{name}-1234567890-abcde').format(name=name)
elif kind == 'ReplicaSet' or kind == 'DaemonSet':
pod_name = _('{name}-12345').format(name=name)
elif kind == 'StatefulSet':
pod_name = _('{name}-1').format(name=name)
elif kind == 'Pod':
pod_name = name
return client.V1Pod(
metadata=client.V1ObjectMeta(name=pod_name),
metadata=client.V1ObjectMeta(name=pod_name,
creation_timestamp=datetime.datetime.now().isoformat('T')),
status=client.V1PodStatus(phase=pod_status))
def fake_vnfc_resource_info(vdu_id='VDU1', rsc_kind='Deployment',
rsc_name='fake_name', pod_name=None,
namespace=None):
def _get_metadata_str(name, namespace="fake_namespace"):
if namespace == "brank":
namespace = ""
metadata = {
'name': name,
'namespace': namespace}
return jsonutils.dumps(metadata)
vnfc_obj = objects.VnfcResourceInfo()
vnfc_obj.id = uuidutils.generate_uuid()
vnfc_obj.vdu_id = vdu_id
if not pod_name:
v1_pod = get_fake_pod_info(rsc_kind, rsc_name)
pod_name = v1_pod.metadata.name
compute_resource = objects.ResourceHandle(
resource_id=pod_name,
vim_level_resource_type=rsc_kind)
vnfc_obj.compute_resource = compute_resource
metadata = {}
if namespace:
metadata['Pod'] = _get_metadata_str(
name=pod_name, namespace=namespace)
if rsc_kind != 'Pod':
metadata[rsc_kind] = _get_metadata_str(
name=rsc_name, namespace=namespace)
else:
metadata['Pod'] = _get_metadata_str(name=pod_name)
if rsc_kind != 'Pod':
metadata[rsc_kind] = _get_metadata_str(name=rsc_name)
vnfc_obj.metadata = metadata
return vnfc_obj
def fake_vim_connection_info():
access_info = {
'auth_url': 'http://fake_url:6443',
'ssl_ca_cert': None}
return vim_connection.VimConnectionInfo(
vim_type="kubernetes",
access_info=access_info)

@ -1382,10 +1382,11 @@ class TestOpenStack(base.FixturedTestCase):
vnf_link_ports[0].resource_handle.vim_level_resource_type,
'physical_resource_id': uuidsentinel.cp1_resource_id}]
inst_req_info = fd_utils.get_instantiate_vnf_request()
self._responses_in_stack_list(inst_vnf_info.instance_id,
resources=resources)
self.openstack.post_vnf_instantiation(
self.context, vnf_instance, vim_connection_info)
self.context, vnf_instance, vim_connection_info, inst_req_info)
self.assertEqual(vnf_instance.instantiated_vnf_info.
vnfc_resource_info[0].metadata['stack_id'],
inst_vnf_info.instance_id)
@ -1453,8 +1454,9 @@ class TestOpenStack(base.FixturedTestCase):
'physical_resource_id': uuidsentinel.v_l_resource_info_id}]
self._responses_in_stack_list(inst_vnf_info.instance_id,
resources=resources)
inst_req_info = fd_utils.get_instantiate_vnf_request()
self.openstack.post_vnf_instantiation(
self.context, vnf_instance, vim_connection_info)
self.context, vnf_instance, vim_connection_info, inst_req_info)
self.assertEqual(vnf_instance.instantiated_vnf_info.
vnfc_resource_info[0].metadata['stack_id'],
inst_vnf_info.instance_id)
@ -1623,7 +1625,7 @@ class TestOpenStack(base.FixturedTestCase):
"UPDATE_COMPLETE"])
stack = self.openstack.heal_vnf_wait(
self.context, vnf_instance, vim_connection_info)
self.context, vnf_instance, vim_connection_info, None)
self.assertEqual('UPDATE_COMPLETE', stack.stack_status)
def test_heal_vnf_wait_fail(self):
@ -1640,7 +1642,7 @@ class TestOpenStack(base.FixturedTestCase):
self.openstack.STACK_RETRIES = 1
result = self.assertRaises(vnfm.VNFHealWaitFailed,
self.openstack.heal_vnf_wait, self.context, vnf_instance,
vim_connection_info)
vim_connection_info, None)
expected_msg = ("VNF Heal action is not completed within 10 seconds "
"on stack %s") % inst_vnf_info.instance_id

@ -611,7 +611,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver):
self._vnf_manager.invoke(
vim_connection_info.vim_type, 'heal_vnf_wait',
context=context, vnf_instance=vnf_instance,
vim_connection_info=vim_connection_info)
vim_connection_info=vim_connection_info,
heal_vnf_request=heal_vnf_request)
except Exception as exp:
LOG.error("Failed to update vnf %(id)s resources for instance "
"%(instance)s. Error: %(error)s",
@ -675,7 +676,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver):
self._vnf_manager.invoke(
vim_connection_info.vim_type, 'post_vnf_instantiation',
context=context, vnf_instance=vnf_instance,
vim_connection_info=vim_connection_info)
vim_connection_info=vim_connection_info,
instantiate_vnf_req=instantiate_vnf_request)
except Exception as exc:
with excutils.save_and_reraise_exception() as exc_ctxt:

@ -97,7 +97,7 @@ class VnfAbstractDriver(extensions.PluginInterface, metaclass=abc.ABCMeta):
@abc.abstractmethod
def post_vnf_instantiation(self, context, vnf_instance,
vim_connection_info):
vim_connection_info, instantiate_vnf_req):
pass
@abc.abstractmethod
@ -114,7 +114,8 @@ class VnfAbstractDriver(extensions.PluginInterface, metaclass=abc.ABCMeta):
pass
@abc.abstractmethod
def heal_vnf_wait(self, context, vnf_instance, vim_connection_info):
def heal_vnf_wait(self, context, vnf_instance, vim_connection_info,
heal_vnf_request):
"""Check vnf is healed successfully"""
pass

@ -486,6 +486,12 @@ class Transformer(object):
return sorted_k8s_objs
def get_object_meta(self, content):
must_param = {}
v1_object_meta = client.V1ObjectMeta()
self._init_k8s_obj(v1_object_meta, content, must_param)
return v1_object_meta
# config_labels configures label
def config_labels(self, deployment_name=None, scaling_name=None):
label = dict()

@ -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 oslo_utils import uuidutils
from toscaparser import tosca_template
from tacker._i18n import _
@ -46,6 +47,7 @@ from urllib.parse import urlparse
CNF_TARGET_FILES_KEY = 'lcm-kubernetes-def-files'
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
VNFC_POD_NOT_FOUND = "POD_NOT_FOUND"
OPTS = [
cfg.IntOpt('stack_retries',
@ -1247,12 +1249,16 @@ class Kubernetes(abstract_driver.VnfAbstractDriver,
def _is_match_pod_naming_rule(self, rsc_kind, rsc_name, pod_name):
match_result = None
if rsc_kind == 'Deployment':
if rsc_kind == 'Pod':
# Expected example: name