Terraform Infra-Driver

This patch provides terraform infra-driver with several unit and
functional tests and is build an environment for terraform. The
supported version of terraform is v1.4.0 or later.

To build the terraform environment that need to install as follow
components:

 - awscli
 - docker
 - localstack or moto server
 - terraform

Implements: blueprint terraform-infra-driver
Change-Id: I14414c42229dcdb8e0083d7c51d6be6b5f2fc841
This commit is contained in:
Hiromu Asahina 2023-04-03 09:39:40 +09:00 committed by Yasufumi Ogawa
parent 1068aedc32
commit 5d59833b20
26 changed files with 3052 additions and 1 deletions

View File

@ -749,6 +749,23 @@
vars:
config_enhanced_policy: true
- job:
name: tacker-functional-devstack-multinode-sol-terraform-v2
parent: tacker-functional-devstack-multinode-legacy
description: |
Multinodes job for SOL Terraform devstack-based functional tests
attempts: 1
host-vars:
controller-tacker:
tox_envlist: dsvm-functional-sol-terraform-v2
devstack_local_conf:
post-config:
$TACKER_CONF:
v2_vnfm:
tf_file_dir: /tmp/tacker/terraform
vars:
terraform_setup: true
- job:
name: tacker-compliance-devstack-multinode-sol
parent: tacker-functional-devstack-multinode-legacy
@ -791,3 +808,4 @@
- tacker-functional-devstack-enhanced-policy-sol
- tacker-functional-devstack-enhanced-policy-sol-kubernetes
- tacker-compliance-devstack-multinode-sol
- tacker-functional-devstack-multinode-sol-terraform-v2

View File

@ -7,6 +7,8 @@
- setup-k8s-oidc
- setup-default-vim
- setup-helm
- role: setup-terraform
when: terraform_setup is defined and terraform_setup | bool
- role: setup-fake-prometheus-server
when: prometheus_setup is defined and prometheus_setup | bool
- role: setup-multi-tenant-vim

View File

@ -0,0 +1,6 @@
---
features:
- |
Support Terraform infra-driver. Using Terraform as a backend tool enables
Tacker to create virtual resources on several platforms that Terraform
has already supported, such as AWS.

View File

@ -0,0 +1,2 @@
[profile localstack_profile]
region = us-east-1

View File

@ -0,0 +1,3 @@
[localstack_profile]
aws_access_key_id = mock_access_key
aws_secret_access_key = mock_secret_key

View File

@ -0,0 +1,61 @@
- block:
- name: Install docker from ubuntu official repository
package:
name: docker.io
state: present
become: yes
- name: Make the user belong to a Docker group
user:
name: stack
groups: docker
append: yes
become: yes
- name: Get gpg key of hashicorp
get_url:
url: https://apt.releases.hashicorp.com/gpg
dest: /usr/share/keyrings/hashicorp-archive-keyring.asc
mode: 0644
force: true
become: yes
- name: Add hashicorp repository
apt_repository:
repo: deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.asc] https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main
filename: hashicorp
state: present
become: yes
- name: Install terraform
apt:
name: terraform
state: present
update_cache: yes
become: yes
- name: Install awscli, LocalStack CLI
pip:
name:
- awscli>=1.29.0
- localstack>=2.0.0
state: present
become: yes
become_user: stack
- name: Check terraform version
shell: terraform --version
register: terraform_version
become: yes
become_user: stack
- name: Start LocalStack
shell: localstack start -d
register: localstack_start
environment:
PATH: "{{ ansible_env.PATH }}:/opt/stack/.local/bin"
become: yes
become_user: stack
when:
- inventory_hostname == 'controller-tacker'

View File

@ -130,7 +130,11 @@ VNFM_OPTS = [
cfg.StrOpt('heat_ca_cert_file',
default='',
help=_('Specifies the root CA certificate to use when the '
'heat_verify_cert option is True.'))
'heat_verify_cert option is True.')),
cfg.StrOpt('tf_file_dir',
default='/var/lib/tacker/terraform/',
help=_('Temporary directory for Terraform infra-driver to '
'store terraform config files'))
]
CONF.register_opts(VNFM_OPTS, 'v2_vnfm')

View File

@ -384,6 +384,11 @@ class HelmOperationFailed(SolHttpError422):
# detail set in the code
class TerraformOperationFailed(SolHttpError422):
title = 'Terraform operation failed'
# detail set in the code
class HelmParameterNotFound(SolHttpError400):
message = _("Helm parameter for scale vdu %(vdu_name)s is not found.")

View File

@ -28,6 +28,7 @@ from tacker.sol_refactored.common import vnf_instance_utils as inst_utils
from tacker.sol_refactored.infra_drivers.kubernetes import helm
from tacker.sol_refactored.infra_drivers.kubernetes import kubernetes
from tacker.sol_refactored.infra_drivers.openstack import openstack
from tacker.sol_refactored.infra_drivers.terraform import terraform
from tacker.sol_refactored.nfvo import nfvo_client
from tacker.sol_refactored import objects
from tacker.sol_refactored.objects.v2 import fields as v2fields
@ -428,6 +429,9 @@ class VnfLcmDriverV2(object):
elif vim_info.vimType == 'ETSINFV.HELM.V_3':
driver = helm.Helm()
driver.instantiate(req, inst, grant_req, grant, vnfd)
elif vim_info.vimType == 'TERRAFORM.V1':
driver = terraform.Terraform()
driver.instantiate(req, inst, grant_req, grant, vnfd)
else:
# should not occur
raise sol_ex.SolException(sol_detail='not support vim type')
@ -447,6 +451,9 @@ class VnfLcmDriverV2(object):
elif vim_info.vimType == 'ETSINFV.HELM.V_3':
driver = helm.Helm()
driver.instantiate_rollback(req, inst, grant_req, grant, vnfd)
elif vim_info.vimType == 'TERRAFORM.V1':
driver = terraform.Terraform()
driver.instantiate_rollback(req, inst, grant_req, grant, vnfd)
else:
# should not occur
raise sol_ex.SolException(sol_detail='not support vim type')
@ -592,6 +599,9 @@ class VnfLcmDriverV2(object):
elif vim_info.vimType == 'ETSINFV.HELM.V_3':
driver = helm.Helm()
driver.terminate(req, inst, grant_req, grant, vnfd)
elif vim_info.vimType == 'TERRAFORM.V1':
driver = terraform.Terraform()
driver.terminate(req, inst, grant_req, grant, vnfd)
else:
# should not occur
raise sol_ex.SolException(sol_detail='not support vim type')

View File

@ -0,0 +1,289 @@
# Copyright (C) 2023 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from tacker.common import exceptions
from tacker.objects import vnf_package_vnfd
from tacker.sol_refactored.common import config
from tacker.sol_refactored.common import exceptions as sol_ex
from tacker.sol_refactored.common import vnf_instance_utils as inst_utils
from tacker.sol_refactored import objects
import json
import os
import shutil
import subprocess
import tacker.conf
LOG = logging.getLogger(__name__)
CONF = config.CONF
class Terraform():
'''Implements Terraform in Tacker'''
def __init__(self):
pass
def instantiate(self, req, inst, grant_req, grant, vnfd):
'''Implements instantiate using Terraform commands'''
vim_conn_info = inst_utils.select_vim_info(inst.vimConnectionInfo)
tf_dir_path = req.additionalParams.get('tf_dir_path')
tf_var_path = req.additionalParams.get('tf_var_path')
working_dir = self._get_tf_vnfpkg(
inst.id, grant_req.vnfdId, tf_dir_path)
self._generate_provider_tf(vim_conn_info, working_dir)
self._instantiate(vim_conn_info, working_dir, tf_var_path)
self._make_instantiated_vnf_info(req, inst, grant_req,
grant, vnfd, working_dir,
tf_var_path)
def _instantiate(self, vim_conn_info, working_dir, tf_var_path):
'''Executes terraform init, terraform plan, and terraform apply'''
access_info = vim_conn_info.get('accessInfo', {})
try:
init_cmd = self._gen_tf_cmd("init")
self._exec_cmd(init_cmd, cwd=working_dir)
LOG.info("Terraform init completed successfully.")
plan_cmd = self._gen_tf_cmd('plan', access_info, tf_var_path)
self._exec_cmd(plan_cmd, cwd=working_dir)
LOG.info("Terraform plan completed successfully.")
apply_cmd = self._gen_tf_cmd('apply', access_info, tf_var_path)
self._exec_cmd(apply_cmd, cwd=working_dir)
LOG.info("Terraform apply completed successfully.")
except subprocess.CalledProcessError as error:
raise sol_ex.TerraformOperationFailed(sol_detail=str(error))
def terminate(self, req, inst, grant_req, grant, vnfd):
'''Terminates the terraform resources managed by the current project'''
vim_conn_info = inst_utils.select_vim_info(inst.vimConnectionInfo)
working_dir = f"{CONF.v2_vnfm.tf_file_dir}/{inst.id}"
tf_var_path = inst.instantiatedVnfInfo.metadata['tf_var_path']
self._terminate(vim_conn_info, working_dir, tf_var_path)
def _terminate(self, vim_conn_info, working_dir, tf_var_path):
'''Executes Terraform Destroy and removes the working_dir'''
access_info = vim_conn_info.get('accessInfo', {})
try:
# Execute the terraform destroy command (auto-approve)
destroy_cmd = self._gen_tf_cmd('destroy', access_info, tf_var_path)
self._exec_cmd(destroy_cmd, cwd=working_dir)
LOG.info("Terraform destroy completed successfully.")
except subprocess.CalledProcessError as error:
failed_process = error.cmd[0].capitalize()
LOG.error(f"Error running {failed_process}: {error}")
# raise error and leave working_dir for retry
raise sol_ex.TerraformOperationFailed(sol_detail=str(error))
try:
# Remove the working directory and its contents
shutil.rmtree(working_dir)
LOG.info(f"Working directory {working_dir} destroyed successfully")
except OSError as error:
LOG.error(f"Error destroying working directory: {error}")
raise
def instantiate_rollback(self, req, inst, grant_req, grant, vnfd):
'''Calls terminate'''
self.terminate(req, inst, grant_req, grant, vnfd)
def _make_instantiated_vnf_info(self, req, inst, grant_req,
grant, vnfd, working_dir, tf_var_path):
'''Updates Tacker with information on the VNF state'''
# Define inst_vnf_info
flavour_id = req.flavourId
inst.instantiatedVnfInfo = objects.VnfInstanceV2_InstantiatedVnfInfo(
flavourId=flavour_id,
vnfState='STARTED',
metadata={
'tf_var_path': tf_var_path
}
)
# Specify the path to the terraform.tfstate file
tfstate_file = f"{working_dir}/terraform.tfstate"
# Read the contents of the file
with open(tfstate_file, "r", encoding="utf-8") as file:
tfstate_data = json.load(file)
vdu_nodes = vnfd.get_vdu_nodes(flavour_id)
vdu_ids = {value.get('properties').get('name'): key
for key, value in vdu_nodes.items()}
# Extract the required fields from the tfstate data
resources = tfstate_data["resources"]
# Define vnfcResourceInfo and vnfcInfo in a single iteration
vnfc_resource_info_list = [
objects.VnfcResourceInfoV2(
id=resource['name'],
vduId=vdu_ids.get(resource['name']),
computeResource=objects.ResourceHandle(
resourceId=resource['name'],
vimLevelResourceType=resource['type']
),
metadata={}
)
for resource in resources
if (resource["type"] == "aws_instance" and
vdu_ids.get(resource['name']))
]
vnfc_info_list = [
objects.VnfcInfoV2(
id=f"{vnfc_res_info.vduId}-{vnfc_res_info.id}",
vduId=vnfc_res_info.vduId,
vnfcResourceInfoId=vnfc_res_info.id,
vnfcState='STARTED'
)
for vnfc_res_info in vnfc_resource_info_list
]
inst.instantiatedVnfInfo.vnfcResourceInfo = vnfc_resource_info_list
inst.instantiatedVnfInfo.vnfcInfo = vnfc_info_list
def _get_tf_vnfpkg(self, vnf_instance_id, vnfd_id, tf_dir_path):
"""Create a VNF package with given IDs
A path of created package is returned, or failed if vnfd_id is invalid.
"""
# Define variables
context = tacker.context.get_admin_context()
try:
pkg_vnfd = vnf_package_vnfd.VnfPackageVnfd().get_by_id(
context, vnfd_id)
except exceptions.VnfPackageVnfdNotFound as exc:
raise sol_ex.VnfdIdNotFound(vnfd_id=vnfd_id) from exc
csar_path = os.path.join(CONF.vnf_package.vnf_package_csar_path,
pkg_vnfd.package_uuid)
# Assemble paths and copy recursively
vnf_package_path = f"{csar_path}/{tf_dir_path}"
new_tf_dir_path = f"{CONF.v2_vnfm.tf_file_dir}/{vnf_instance_id}"
os.makedirs(new_tf_dir_path, exist_ok=True)
# NOTE: the creation of the directory /var/lib/tacker/terraform
# should be completed during the installation of Tacker.
shutil.copytree(vnf_package_path, new_tf_dir_path, dirs_exist_ok=True)
return new_tf_dir_path
def _generate_provider_tf(self, vim_conn_info, main_tf_path):
'''Creates provider.tf beside main.tf'''
# Read vimConnectionInfo for information
access_info = vim_conn_info.get('accessInfo', {})
interface_info = vim_conn_info.get('interfaceInfo', {})
provider_type = interface_info.get('providerType')
provider_version = interface_info.get('providerVersion')
# Create provider.tf content using the above information
content = {
"variable": {},
"terraform": {
"required_version": ">=0.13",
"required_providers": {
provider_type: {
"source": f"hashicorp/{provider_type}",
"version": f"~> {provider_version}"
}
}
},
"provider": {
provider_type: {}
}
}
# Add accessInfo keys as variables and provider arguments
for key, value in access_info.items():
if key == "endpoints":
content["provider"][provider_type][key] = value
continue
content["variable"][key] = {
"type": "string"
}
content["provider"][provider_type][key] = f"${{var.{key}}}"
# Save the provider.tf file
provider_tf_path = os.path.join(main_tf_path, 'provider.tf.json')
with open(provider_tf_path, 'w', encoding='utf-8') as f:
json.dump(content, f, indent=4)
return provider_tf_path
def _exec_cmd(self, cmd, cwd,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True, text=True):
"""A helper for subprocess.run()
Exec a command through subprocess.run() with common options among
commands used in this package. All the args other than self and cmd
the same as for subprocess.run().
"""
res = subprocess.run(cmd, cwd=cwd, stdout=stdout, stderr=stderr,
check=check, text=text)
if res.returncode != 0:
raise
return res
def _gen_tf_cmd(self, subcmd, access_info=None, tf_var_path=None,
auto_approve=True):
"""Return terraform command of given subcommand as a list
The result is intended to be an arg of supprocess.run().
"""
# NOTE(yasufum): Only following subcommands are supported.
allowed_subcmds = ["init", "plan", "apply", "destroy"]
if subcmd not in allowed_subcmds:
return []
if subcmd == "init":
return ["terraform", "init"]
def _gen_tf_cmd_args(access_info, tf_var_path):
args = []
for key, value in access_info.items():
if key == "endpoints":
continue
args.extend(['-var', f'{key}={value}'])
if tf_var_path:
args.extend(['-var-file', tf_var_path])
return args
# list of subcommands accept "-auto-approve" option.
accept_ap = ["apply", "destroy"]
if auto_approve is True and subcmd in accept_ap:
cmd = ["terraform", subcmd, "-auto-approve"]
else:
cmd = ["terraform", subcmd]
args = _gen_tf_cmd_args(access_info, tf_var_path)
return cmd + args

View File

@ -0,0 +1,119 @@
# Copyright (C) 2023 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import shutil
import tempfile
import time
from oslo_config import cfg
from oslo_utils import uuidutils
from tacker.sol_refactored.common import http_client
from tacker.sol_refactored import objects
from tacker.tests.functional import base_v2
from tacker.tests.functional.common.fake_server import FakeServerManager
from tacker.tests.functional.sol_v2_common import utils
from tacker import version
FAKE_SERVER_MANAGER = FakeServerManager()
VNF_PACKAGE_UPLOAD_TIMEOUT = 300
class BaseVnfLcmTerraformV2Test(base_v2.BaseTackerTestV2):
@classmethod
def setUpClass(cls):
super(BaseVnfLcmTerraformV2Test, cls).setUpClass()
"""Base test case class for SOL v2 terraform functional tests."""
cfg.CONF(args=['--config-file', '/etc/tacker/tacker.conf'],
project='tacker',
version='%%prog %s' % version.version_info.release_string())
objects.register_all()
vim_info = cls.get_vim_info()
auth = http_client.KeystonePasswordAuthHandle(
auth_url=vim_info.interfaceInfo['endpoint'],
username=vim_info.accessInfo['username'],
password=vim_info.accessInfo['password'],
project_name=vim_info.accessInfo['project'],
user_domain_name=vim_info.accessInfo['userDomain'],
project_domain_name=vim_info.accessInfo['projectDomain']
)
cls.tacker_client = http_client.HttpClient(auth)
def assert_notification_get(self, callback_url):
notify_mock_responses = FAKE_SERVER_MANAGER.get_history(
callback_url)
FAKE_SERVER_MANAGER.clear_history(
callback_url)
self.assertEqual(1, len(notify_mock_responses))
self.assertEqual(204, notify_mock_responses[0].status_code)
def _check_notification(self, callback_url, notify_type):
notify_mock_responses = FAKE_SERVER_MANAGER.get_history(
callback_url)
FAKE_SERVER_MANAGER.clear_history(
callback_url)
self.assertEqual(1, len(notify_mock_responses))
self.assertEqual(204, notify_mock_responses[0].status_code)
self.assertEqual(notify_type, notify_mock_responses[0].request_body[
'notificationType'])
@classmethod
def create_vnf_package(cls, sample_path, user_data=None, image_path=None):
vnfd_id = uuidutils.generate_uuid()
tmp_dir = tempfile.mkdtemp()
utils.make_zip(sample_path, tmp_dir, vnfd_id, image_path)
zip_file_name = os.path.basename(os.path.abspath(sample_path)) + ".zip"
zip_file_path = os.path.join(tmp_dir, zip_file_name)
path = "/vnfpkgm/v1/vnf_packages"
if user_data is not None:
req_body = {'userDefinedData': user_data}
else:
req_body = {}
resp, body = cls.tacker_client.do_request(
path, "POST", expected_status=[201], body=req_body)
pkg_id = body['id']
with open(zip_file_path, 'rb') as fp:
path = f"/vnfpkgm/v1/vnf_packages/{pkg_id}/package_content"
resp, body = cls.tacker_client.do_request(
path, "PUT", body=fp, content_type='application/zip',
expected_status=[202])
# wait for onboard
timeout = VNF_PACKAGE_UPLOAD_TIMEOUT
start_time = int(time.time())
path = f"/vnfpkgm/v1/vnf_packages/{pkg_id}"
while True:
resp, body = cls.tacker_client.do_request(
path, "GET", expected_status=[200])
if body['onboardingState'] == "ONBOARDED":
break
if (int(time.time()) - start_time) > timeout:
raise Exception("Failed to onboard vnf package")
time.sleep(5)
shutil.rmtree(tmp_dir)
return pkg_id, vnfd_id

View File

@ -0,0 +1,79 @@
# Copyright (C) 2023 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_utils import uuidutils
def create_req_by_vnfd_id(vnfd_id):
return {
"vnfdId": vnfd_id,
"vnfInstanceName": "test_terraform_instantiate",
"vnfInstanceDescription": "test_terraform_instantiate",
"metadata": {"dummy-key": "dummy-val"}
}
def instantiate_req():
# All attributes are set.
# NOTE: All of the following cardinality attributes are set.
# In addition, 0..N or 1..N attributes are set to 2 or more.
# - 0..1 (1)
# - 0..N (2 or more)
# - 1
# - 1..N (2 or more)
auth_url = "http://localhost:4566"
vim = {
"vimId": uuidutils.generate_uuid(),
"vimType": "TERRAFORM.V1",
"interfaceInfo": {
"providerType": "aws",
"providerVersion": "4.0"
},
"accessInfo": {
"region": "ap-northeast-1",
"access_key": "mock_access_key",
"secret_key": "mock_secret_key",
"skip_credentials_validation": "true",
"skip_metadata_api_check": "true",
"skip_requesting_account_id": "true",
"endpoints": {
"ec2": auth_url
}
}
}
return {
"flavourId": "simple",
"vimConnectionInfo": {
"vim1": vim
},
"additionalParams": {"tf_dir_path": "Files/terraform"}
}
def terminate_req():
# All attributes are set.
# NOTE: All of the following cardinality attributes are set.
# In addition, 0..N or 1..N attributes are set to 2 or more.
# - 0..1 (1)
# - 0..N (2 or more)
# - 1
# - 1..N (2 or more)
return {
"terminationType": "GRACEFUL",
"gracefulTerminationTimeout": 5,
"additionalParams": {"dummy-key": "dummy-val"}
}

View File

@ -0,0 +1,202 @@
tosca_definitions_version: tosca_simple_yaml_1_2
description: ETSI NFV SOL 001 common types definitions version 2.6.1
metadata:
template_name: etsi_nfv_sol001_common_types
template_author: ETSI_NFV
template_version: 2.6.1
data_types:
tosca.datatypes.nfv.L2AddressData:
derived_from: tosca.datatypes.Root
description: Describes the information on the MAC addresses to be assigned to a connection point.
properties:
mac_address_assignment:
type: boolean
description: Specifies if the address assignment is the responsibility of management and orchestration function or not. If it is set to True, it is the management and orchestration function responsibility
required: true
tosca.datatypes.nfv.L3AddressData:
derived_from: tosca.datatypes.Root
description: Provides information about Layer 3 level addressing scheme and parameters applicable to a CP
properties:
ip_address_assignment:
type: boolean
description: Specifies if the address assignment is the responsibility of management and orchestration function or not. If it is set to True, it is the management and orchestration function responsibility
required: true
floating_ip_activated:
type: boolean
description: Specifies if the floating IP scheme is activated on the Connection Point or not
required: true
ip_address_type:
type: string
description: Defines address type. The address type should be aligned with the address type supported by the layer_protocols properties of the parent VnfExtCp
required: false
constraints:
- valid_values: [ ipv4, ipv6 ]
number_of_ip_address:
type: integer
description: Minimum number of IP addresses to be assigned
required: false
constraints:
- greater_than: 0
tosca.datatypes.nfv.AddressData:
derived_from: tosca.datatypes.Root
description: Describes information about the addressing scheme and parameters applicable to a CP
properties:
address_type:
type: string
description: Describes the type of the address to be assigned to a connection point. The content type shall be aligned with the address type supported by the layerProtocol property of the connection point
required: true
constraints:
- valid_values: [ mac_address, ip_address ]
l2_address_data:
type: tosca.datatypes.nfv.L2AddressData
description: Provides the information on the MAC addresses to be assigned to a connection point.
required: false
l3_address_data:
type: tosca.datatypes.nfv.L3AddressData
description: Provides the information on the IP addresses to be assigned to a connection point
required: false
tosca.datatypes.nfv.ConnectivityType:
derived_from: tosca.datatypes.Root
description: describes additional connectivity information of a virtualLink
properties:
layer_protocols:
type: list
description: Identifies the protocol a virtualLink gives access to (ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire).The top layer protocol of the virtualLink protocol stack shall always be provided. The lower layer protocols may be included when there are specific requirements on these layers.
required: true
entry_schema:
type: string
constraints:
- valid_values: [ ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire ]
flow_pattern:
type: string
description: Identifies the flow pattern of the connectivity
required: false
constraints:
- valid_values: [ line, tree, mesh ]
tosca.datatypes.nfv.LinkBitrateRequirements:
derived_from: tosca.datatypes.Root
description: describes the requirements in terms of bitrate for a virtual link
properties:
root:
type: integer # in bits per second
description: Specifies the throughput requirement in bits per second of the link (e.g. bitrate of E-Line, root bitrate of E-Tree, aggregate capacity of E-LAN).
required: true
constraints:
- greater_or_equal: 0
leaf:
type: integer # in bits per second
description: Specifies the throughput requirement in bits per second of leaf connections to the link when applicable to the connectivity type (e.g. for E-Tree and E LAN branches).
required: false
constraints:
- greater_or_equal: 0
tosca.datatypes.nfv.CpProtocolData:
derived_from: tosca.datatypes.Root
description: Describes and associates the protocol layer that a CP uses together with other protocol and connection point information
properties:
associated_layer_protocol:
type: string
required: true
description: One of the values of the property layer_protocols of the CP
constraints:
- valid_values: [ ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire ]
address_data:
type: list
description: Provides information on the addresses to be assigned to the CP
entry_schema:
type: tosca.datatypes.nfv.AddressData
required: false
tosca.datatypes.nfv.VnfProfile:
derived_from: tosca.datatypes.Root
description: describes a profile for instantiating VNFs of a particular NS DF according to a specific VNFD and VNF DF.
properties:
instantiation_level:
type: string
description: Identifier of the instantiation level of the VNF DF to be used for instantiation. If not present, the default instantiation level as declared in the VNFD shall be used.
required: false
min_number_of_instances:
type: integer
description: Minimum number of instances of the VNF based on this VNFD that is permitted to exist for this VnfProfile.
required: true
constraints:
- greater_or_equal: 0
max_number_of_instances:
type: integer
description: Maximum number of instances of the VNF based on this VNFD that is permitted to exist for this VnfProfile.
required: true
constraints:
- greater_or_equal: 0
tosca.datatypes.nfv.Qos:
derived_from: tosca.datatypes.Root
description: describes QoS data for a given VL used in a VNF deployment flavour
properties:
latency:
type: scalar-unit.time #Number
description: Specifies the maximum latency
required: true
constraints:
- greater_than: 0 s
packet_delay_variation:
type: scalar-unit.time #Number
description: Specifies the maximum jitter
required: true
constraints:
- greater_or_equal: 0 s
packet_loss_ratio:
type: float
description: Specifies the maximum packet loss ratio
required: false
constraints:
- in_range: [ 0.0, 1.0 ]
capability_types:
tosca.capabilities.nfv.VirtualLinkable:
derived_from: tosca.capabilities.Node
description: A node type that includes the VirtualLinkable capability indicates that it can be pointed by tosca.relationships.nfv.VirtualLinksTo relationship type
relationship_types:
tosca.relationships.nfv.VirtualLinksTo:
derived_from: tosca.relationships.DependsOn
description: Represents an association relationship between the VduCp and VnfVirtualLink node types
valid_target_types: [ tosca.capabilities.nfv.VirtualLinkable ]
node_types:
tosca.nodes.nfv.Cp:
derived_from: tosca.nodes.Root
description: Provides information regarding the purpose of the connection point
properties:
layer_protocols:
type: list
description: Identifies which protocol the connection point uses for connectivity purposes
required: true
entry_schema:
type: string
constraints:
- valid_values: [ ethernet, mpls, odu2, ipv4, ipv6, pseudo-wire ]
role: #Name in ETSI NFV IFA011 v0.7.3: cpRole
type: string
description: Identifies the role of the port in the context of the traffic flow patterns in the VNF or parent NS
required: false
constraints:
- valid_values: [ root, leaf ]
description:
type: string
description: Provides human-readable information on the purpose of the connection point
required: false
protocol:
type: list
description: Provides information on the addresses to be assigned to the connection point(s) instantiated from this Connection Point Descriptor
required: false
entry_schema:
type: tosca.datatypes.nfv.CpProtocolData
trunk_mode:
type: boolean
description: Provides information about whether the CP instantiated from this Cp is in Trunk mode (802.1Q or other), When operating in "trunk mode", the Cp is capable of carrying traffic for several VLANs. Absence of this property implies that trunkMode is not configured for the Cp i.e. It is equivalent to boolean value "false".
required: false

View File

@ -0,0 +1,177 @@
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
- sample_tf_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
interfaces:
Vnflcm:
instantiate_start:
implementation: sample-script
instantiate_end:
implementation: sample-script
terminate_start:
implementation: sample-script
terminate_end:
implementation: sample-script
scale_start:
implementation: sample-script
scale_end:
implementation: sample-script
heal_start:
implementation: sample-script
heal_end:
implementation: sample-script
modify_information_start:
implementation: sample-script
modify_information_end:
implementation: sample-script
artifacts:
sample-script:
description: Sample script
type: tosca.artifacts.Implementation.Python
file: ../Scripts/sample_script.py
VDU1:
type: tosca.nodes.nfv.Vdu.Compute
properties:
name: vdu1
description: VDU1 compute node
vdu_profile:
min_number_of_instances: 1
max_number_of_instances: 3
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:
vdu1_aspect:
name: vdu1_aspect
description: vdu1 scaling aspect
max_scale_level: 2
step_deltas:
- delta_1
vdu2_aspect:
name: vdu2_aspect
description: vdu2 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 ]
- 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:
vdu1_aspect:
scale_level: 0
vdu2_aspect:
scale_level: 0
instantiation_level_2:
description: Largest size
scale_info:
vdu1_aspect:
scale_level: 2
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: 3
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 ]

View File

@ -0,0 +1,31 @@
tosca_definitions_version: tosca_simple_yaml_1_2
description: Sample Terraform VNF
imports:
- etsi_nfv_sol001_common_types.yaml
- etsi_nfv_sol001_vnfd_types.yaml
- sample_tf_types.yaml
- sample_tf_df_simple.yaml
topology_template:
inputs:
selected_flavour:
type: string
description: VNF deployment flavour selected by the consumer. It is provided in the API
node_templates:
VNF:
type: company.provider.VNF
properties:
flavour_id: { get_input: selected_flavour }
descriptor_id: b1bb0ce7-ebca-4fa7-95ed-4840d7000000
provider: Company
product_name: Sample Terraform 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

View File

@ -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-4840d7000000 ] ]
default: b1bb0ce7-ebca-4fa7-95ed-4840d7000000
descriptor_version:
type: string
constraints: [ valid_values: [ '1.0' ] ]
default: '1.0'
provider:
type: string
constraints: [ valid_values: [ 'Company' ] ]
default: 'Company'
product_name:
type: string
constraints: [ valid_values: [ 'Sample Terraform VNF' ] ]
default: 'Sample Terraform 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

View File

@ -0,0 +1,8 @@
resource "aws_instance" "vdu1"{
ami = "ami-785db401"
instance_type = "t2.micro"
tags = {
Name = "hoge01"
}
}

View File

@ -0,0 +1,67 @@
# Copyright (C) 2023 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import pickle
import sys
class SampleScript(object):
def __init__(self, req, inst, grant_req, grant, csar_dir):
self.req = req
self.inst = inst
self.grant_req = grant_req
self.grant = grant
self.csar_dir = csar_dir
def instantiate_start(self):
pass
def instantiate_end(self):
pass
def terminate_start(self):
pass
def terminate_end(self):
pass
def main():
script_dict = pickle.load(sys.stdin.buffer)
operation = script_dict['operation']
req = script_dict['request']
inst = script_dict['vnf_instance']
grant_req = script_dict['grant_request']
grant = script_dict['grant_response']
csar_dir = script_dict['tmp_csar_dir']
script = SampleScript(req, inst, grant_req, grant, csar_dir)
try:
getattr(script, operation)()
except AttributeError:
raise Exception("{} is not included in the script.".format(operation))
if __name__ == "__main__":
try:
main()
os._exit(0)
except Exception as ex:
sys.stderr.write(str(ex))
sys.stderr.flush()
os._exit(1)

View File

@ -0,0 +1,9 @@
TOSCA-Meta-File-Version: 1.0
Created-by: dummy_user
CSAR-Version: 1.1
Entry-Definitions: Definitions/sample_tf_top.vnfd.yaml
Name: Files/terraform/main.tf
Content-Type: test-data
Algorithm: SHA-256
Hash: 8b781c33326f6383316f24464371de38724898215bc04653e1985e3c9514c87d

View File

@ -0,0 +1,48 @@
# Copyright (C) 2023 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
import shutil
import tempfile
from oslo_utils import uuidutils
from tacker.tests.functional.sol_terraform_v2 import paramgen as tf_paramgen
from tacker.tests.functional.sol_v2_common import utils
zip_file_name = os.path.basename(os.path.abspath(".")) + '.zip'
tmp_dir = tempfile.mkdtemp()
vnfd_id = uuidutils.generate_uuid()
# tacker/tests/functional/sol_terraform_v2/samples/{package_name}
utils.make_zip(".", tmp_dir, vnfd_id)
shutil.move(os.path.join(tmp_dir, zip_file_name), ".")
shutil.rmtree(tmp_dir)
create_req = tf_paramgen.create_req_by_vnfd_id(vnfd_id)
instantiate_req = tf_paramgen.instantiate_req()
terminate_req = tf_paramgen.terminate_req()
with open("create_req", "w", encoding='utf-8') as f:
f.write(json.dumps(create_req, indent=2))
with open("instantiate_req", "w", encoding='utf-8') as f:
f.write(json.dumps(instantiate_req, indent=2))
with open("terminate_req", "w", encoding='utf-8') as f:
f.write(json.dumps(terminate_req, indent=2))

View File

@ -0,0 +1,153 @@
# Copyright (C) 2023 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import tacker.conf
from tacker.objects import fields
from tacker.tests.functional.sol_terraform_v2 import base_v2
from tacker.tests.functional.sol_terraform_v2 import paramgen as tf_paramgen
CONF = tacker.conf.CONF
class VnfLcmTerraformTest(base_v2.BaseVnfLcmTerraformV2Test):
@classmethod
def setUpClass(cls):
super(VnfLcmTerraformTest, cls).setUpClass()
cur_dir = os.path.dirname(__file__)
sample_pkg = "samples/test_terraform_basic"
pkg_dir_path = os.path.join(cur_dir, sample_pkg)
cls.basic_pkg, cls.basic_vnfd_id = cls.create_vnf_package(pkg_dir_path)
@classmethod
def tearDownClass(cls):
super(VnfLcmTerraformTest, cls).tearDownClass()
cls.delete_vnf_package(cls.basic_pkg)
def setUp(self):
super(VnfLcmTerraformTest, self).setUp()
def instantiate_vnf_instance(self, inst_id, req_body):
path = "/vnflcm/v2/vnf_instances/{}/instantiate".format(inst_id)
return self.tacker_client.do_request(
path, "POST", body=req_body, version="2.0.0")
def terminate_vnf_instance(self, inst_id, req_body):
path = "/vnflcm/v2/vnf_instances/{}/terminate".format(inst_id)
return self.tacker_client.do_request(
path, "POST", body=req_body, version="2.0.0")
def test_basic_lcms(self):
self._get_basic_lcms_procedure()
def _get_basic_lcms_procedure(self, use_register_vim=False):
"""Test basic LCM operations
* About LCM operations:
This test includes the following operations.
- 1. Create VNF instance
- 2. Instantiate VNF
- 3. Show VNF instance
- 4. Terminate VNF
- 5. Delete a VNF instance
"""
# 1. Create a new VNF instance resource
# NOTE: extensions and vnfConfigurableProperties are omitted
# because they are commented out in etsi_nfv_sol001.
expected_inst_attrs = [
'id',
'vnfInstanceName',
'vnfInstanceDescription',
'vnfdId',
'vnfProvider',
'vnfProductName',
'vnfSoftwareVersion',
'vnfdVersion',
# 'vnfConfigurableProperties', # omitted
# 'vimConnectionInfo', # omitted
'instantiationState',
# 'instantiatedVnfInfo', # omitted
'metadata',
# 'extensions', # omitted
'_links'
]
create_req = tf_paramgen.create_req_by_vnfd_id(self.basic_vnfd_id)
resp, body = self.create_vnf_instance(create_req)
self.assertEqual(201, resp.status_code)
self.check_resp_headers_in_create(resp)
self.check_resp_body(body, expected_inst_attrs)
inst_id = body['id']
self.check_package_usage(self.basic_pkg, state='IN_USE')
# 2. Instantiate VNF
instantiate_req = tf_paramgen.instantiate_req()
resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req)
self.assertEqual(202, resp.status_code)
self.check_resp_headers_in_operation_task(resp)
lcmocc_id = os.path.basename(resp.headers['Location'])
self.wait_lcmocc_complete(lcmocc_id)
# 3. Show VNF instance
additional_inst_attrs = [
'vimConnectionInfo',
'instantiatedVnfInfo'
]
expected_inst_attrs.extend(additional_inst_attrs)
resp, body = self.show_vnf_instance(inst_id)
self.assertEqual(200, resp.status_code)
self.check_resp_headers_in_get(resp)
self.check_resp_body(body, expected_inst_attrs)
# check instantiationState of VNF
self.assertEqual(fields.VnfInstanceState.INSTANTIATED,
body['instantiationState'])
# check vnfState of VNF
self.assertEqual(fields.VnfOperationalStateType.STARTED,
body['instantiatedVnfInfo']['vnfState'])
# 4. Terminate VNF instance
terminate_req = tf_paramgen.terminate_req()
resp, body = self.terminate_vnf_instance(inst_id, terminate_req)
self.assertEqual(202, resp.status_code)
self.check_resp_headers_in_operation_task(resp)
lcmocc_id = os.path.basename(resp.headers['Location'])
self.wait_lcmocc_complete(lcmocc_id)
# check instantiationState of VNF
resp, body = self.show_vnf_instance(inst_id)
self.assertEqual(200, resp.status_code)
self.assertEqual(fields.VnfInstanceState.NOT_INSTANTIATED,
body['instantiationState'])
# 5. Delete a VNF instance
resp, body = self.delete_vnf_instance(inst_id)
self.assertEqual(204, resp.status_code)
self.check_resp_headers_in_delete(resp)
# check deletion of VNF instance
resp, body = self.show_vnf_instance(inst_id)
self.assertEqual(404, resp.status_code)
self.check_package_usage(self.basic_pkg, state='NOT_IN_USE')
# TODO(yasufum) consider to add a test for instantiate_rollback here.

View File

@ -0,0 +1,236 @@
# Copyright (C) 2023 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
import tempfile
from oslo_utils import uuidutils
from unittest import mock
from tacker import context
from tacker.sol_refactored.common import vnfd_utils
from tacker.sol_refactored.infra_drivers.terraform import terraform
from tacker.sol_refactored import objects
from tacker.sol_refactored.objects.v2 import fields
from tacker.tests import base
SAMPLE_VNFD_ID = "65b62a2a-c207-423f-9b01-f399c9ab5629"
SAMPLE_FLAVOUR_ID = "simple"
_vim_connection_info_example = {
"vimId": "terraform_provider_aws_v4_tokyo",
"vimType": "ETSINFV.TERRAFORM.V_1",
"interfaceInfo": {
"providerType": "aws",
"providerVersion": "4.0"
},
"accessInfo": {
"region": "ap-northeast-1",
"access_key": "example_access_key",
"secret_key": "example_secret_key"
}
}
_instantiate_req_example = {
# instantiateVnfRequest example
"flavourId": SAMPLE_FLAVOUR_ID,
"vimConnectionInfo": {
"vim1": _vim_connection_info_example
},
"additionalParams": {
"tf_dir_path": "Files/terraform",
"tf_var_path": "Files/terraform/variables.tf"
}
}
class TestTerraform(base.BaseTestCase):
def setUp(self):
super(TestTerraform, self).setUp()
objects.register_all()
self.driver = terraform.Terraform()
self.context = context.get_admin_context()
cur_dir = os.path.dirname(__file__)
sample_dir = os.path.join(cur_dir, "../..", "samples")
self.vnfd_2 = vnfd_utils.Vnfd(SAMPLE_VNFD_ID)
self.vnfd_2.init_from_csar_dir(os.path.join(sample_dir, "sample2"))
@mock.patch.object(terraform.Terraform, '_get_tf_vnfpkg')
@mock.patch.object(terraform.Terraform, '_generate_provider_tf')
@mock.patch.object(terraform.Terraform, '_make_instantiated_vnf_info')
@mock.patch.object(terraform.Terraform, '_instantiate')
def test_instantiate(self, mock_instantiate,
mock_make_instantiated_vnf_info,
mock_generate_provider_tf,
mock_tf_files):
'''Verifies instantiate is called once'''
req = objects.InstantiateVnfRequest.from_dict(_instantiate_req_example)
inst = objects.VnfInstanceV2(
# Required fields
id=uuidutils.generate_uuid(),
vnfdId=SAMPLE_VNFD_ID,
vnfProvider='provider',
vnfProductName='product name',
vnfSoftwareVersion='software version',
vnfdVersion='vnfd version',
instantiationState='INSTANTIATED',
vimConnectionInfo=req['vimConnectionInfo']
)
grant_req = objects.GrantRequestV1(
operation=fields.LcmOperationType.INSTANTIATE,
vnfdId=SAMPLE_VNFD_ID
)
grant = objects.GrantV1()
# Set the desired return value for _get_tf_vnfpkg
mock_tf_files.return_value = f"/var/lib/tacker/terraform/{inst.id}"
# Execute
self.driver.instantiate(req, inst, grant_req, grant, self.vnfd_2)
# TODO(yasufum) Test _instantiate mock subprocess
# Verify _instantiate is called once
mock_instantiate.assert_called_once_with(
req.vimConnectionInfo['vim1'],
f"/var/lib/tacker/terraform/{inst.id}",
req.additionalParams.get('tf_var_path'))
def test_make_instantiated_vnf_info(self):
'''Verifies instantiated vnf info is correct'''
req = objects.InstantiateVnfRequest.from_dict(_instantiate_req_example)
inst = objects.VnfInstanceV2(
# Required fields
id=uuidutils.generate_uuid(),
vnfdId=SAMPLE_VNFD_ID,
vnfProvider='provider',
vnfProductName='product name',
vnfSoftwareVersion='software version',
vnfdVersion='vnfd version',
instantiationState='INSTANTIATED',
vimConnectionInfo=req['vimConnectionInfo']
)
grant_req = objects.GrantRequestV1(
operation='INSTANTIATE'
)
grant = objects.GrantV1()
# Expected results
_expected_inst_info = {
"flavourId": "simple",
"vnfState": "STARTED",
"vnfcResourceInfo": [
{
"id": "vdu1",
"vduId": "VDU1",
"computeResource": {
"resourceId": "vdu1",
"vimLevelResourceType": "aws_instance"
},
"metadata": {}
},
{
"id": "vdu2",
"vduId": "VDU2",
"computeResource": {
"resourceId": "vdu2",
"vimLevelResourceType": "aws_instance"
},
"metadata": {}
}
],
"vnfcInfo": [
{
"id": "VDU1-vdu1",
"vduId": "VDU1",
"vnfcResourceInfoId": "vdu1",
"vnfcState": "STARTED"
},
{
"id": "VDU2-vdu2",
"vduId": "VDU2",
"vnfcResourceInfoId": "vdu2",
"vnfcState": "STARTED"
}
]
}
# Create a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
# Create the tfstate content
provider = "provider[\"registry.terraform.io/hashicorp/aws\"]"
tfstate_content = {
"version": 4,
"terraform_version": "1.4.4",
"serial": 4,
"lineage": "5745b992-04a2-5811-2e02-19d64f6f4b44",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "vdu1",
"provider": provider
},
{
"mode": "managed",
"type": "aws_instance",
"name": "vdu2",
"provider": provider
},
{
"mode": "managed",
"type": "aws_subnet",
"name": "hoge-subnet01",
"provider": provider
}
]
}
# Write the tfstate content to a temporary file
tfstate_file_path = f"{temp_dir}/terraform.tfstate"
with open(tfstate_file_path, "w") as tfstate_file:
json.dump(tfstate_content, tfstate_file)
# Execute the test with the temporary tfstate_file
self.driver._make_instantiated_vnf_info(req, inst,
grant_req, grant,
self.vnfd_2, temp_dir,
req.additionalParams.get(
'tf_var_path')
)
# check
result = inst.to_dict()["instantiatedVnfInfo"]
expected = _expected_inst_info
# vnfcResourceInfo is sorted by creation_time (reverse)
self.assertIn("vnfcResourceInfo", result)
self.assertEqual(expected["vnfcResourceInfo"],
result["vnfcResourceInfo"])
# order of vnfcInfo is same as vnfcResourceInfo
self.assertIn("vnfcInfo", result)
self.assertEqual(expected["vnfcInfo"], result["vnfcInfo"])

View File

@ -145,6 +145,12 @@ commands_post =
allowlist_externals =
sudo
[testenv:dsvm-functional-sol-terraform-v2]
setenv = {[testenv]setenv}
commands =
stestr --test-path=./tacker/tests/functional/sol_terraform_v2 run --slowest --concurrency 1 {posargs}
[testenv:dsvm-compliance-sol-api]
passenv =
{[testenv]passenv}