From 08ae05a27ab5b12bb6f728fc3e808e7e18f1f19c Mon Sep 17 00:00:00 2001 From: Ayumu Ueha Date: Fri, 20 Aug 2021 14:18:42 +0000 Subject: [PATCH] Support Helm chart as interface for Kubernetes VIM Implements new interface for Kubernetes VIM to handle Helm chart. It enables Users to include Helm chart files as MCIOP in their VNF Packages, to instantiate and to terminate CNF with them. And update sample of MgmtDriver to install and configure Helm package for using Helm cli command in the deployed Kubernetes cluster VNF, and to restore the registered helm repositories and charts after the master node is healed. Implements: blueprint helmchart-k8s-vim Change-Id: I8511b103841d5aba7edcf9ec5bb974bfa3a74bb2 --- ...rt-helmchart-k8s-vim-3604f0070cca6b63.yaml | 10 + samples/mgmt_driver/install_helm.sh | 49 ++ samples/mgmt_driver/kubernetes_mgmt.py | 143 ++++- .../TOSCA-Metadata/TOSCA.meta | 7 +- tacker/db/nfvo/nfvo_db.py | 1 + tacker/db/nfvo/nfvo_db_plugin.py | 5 +- tacker/extensions/vnfm.py | 12 + tacker/objects/vim_connection.py | 9 +- .../Definitions/sample_vnfd_df_helmchart.yaml | 151 ++++++ .../Definitions/sample_vnfd_top.yaml | 31 ++ .../Definitions/sample_vnfd_types.yaml | 53 ++ .../Files/kubernetes/localhelm-0.1.0.tgz | Bin 0 -> 3603 bytes .../TOSCA-Metadata/TOSCA.meta | 9 + .../vnflcm/test_kubernetes_helm.py | 447 +++++++++++++++ tacker/tests/unit/vnflcm/fakes.py | 5 +- tacker/tests/unit/vnflcm/test_controller.py | 9 +- .../vnfm/infra_drivers/kubernetes/fakes.py | 140 +++++ .../kubernetes/test_kubernetes_driver_helm.py | 509 ++++++++++++++++++ tacker/tests/unit/vnfm/test_vim_client.py | 4 +- tacker/vnflcm/utils.py | 8 +- .../infra_drivers/kubernetes/helm/__init__.py | 0 .../kubernetes/helm/helm_client.py | 152 ++++++ .../kubernetes/k8s/translate_outputs.py | 83 ++- .../kubernetes/kubernetes_driver.py | 319 +++++++++-- tacker/vnfm/vim_client.py | 3 +- 25 files changed, 2073 insertions(+), 86 deletions(-) create mode 100644 releasenotes/notes/support-helmchart-k8s-vim-3604f0070cca6b63.yaml create mode 100644 samples/mgmt_driver/install_helm.sh create mode 100644 tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_df_helmchart.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_top.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_types.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Files/kubernetes/localhelm-0.1.0.tgz create mode 100644 tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/TOSCA-Metadata/TOSCA.meta create mode 100644 tacker/tests/functional/sol_kubernetes/vnflcm/test_kubernetes_helm.py create mode 100644 tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver_helm.py create mode 100644 tacker/vnfm/infra_drivers/kubernetes/helm/__init__.py create mode 100644 tacker/vnfm/infra_drivers/kubernetes/helm/helm_client.py diff --git a/releasenotes/notes/support-helmchart-k8s-vim-3604f0070cca6b63.yaml b/releasenotes/notes/support-helmchart-k8s-vim-3604f0070cca6b63.yaml new file mode 100644 index 000000000..3bcf4f1ba --- /dev/null +++ b/releasenotes/notes/support-helmchart-k8s-vim-3604f0070cca6b63.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Add new interface for Kubernetes VIM to handle Helm chart. It enables Users + to include Helm chart files as MCIOP in their VNF Packages, to instantiate + and to terminate CNF with them. + And update sample of MgmtDriver to install and configure Helm package for + using Helm cli command in the deployed Kubernetes cluster VNF, and to + restore the registered helm repositories and charts after the master node is + healed. diff --git a/samples/mgmt_driver/install_helm.sh b/samples/mgmt_driver/install_helm.sh new file mode 100644 index 000000000..cd0968c9b --- /dev/null +++ b/samples/mgmt_driver/install_helm.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -o xtrace + +############################################################################### +# +# This script will install and setting Helm for Tacker. +# +############################################################################### + +declare -g HELM_VERSION="3.5.4" +declare -g HELM_CHART_DIR="/var/tacker/helm" + +# Install Helm +#------------- +function install_helm { + wget -P /tmp https://get.helm.sh/helm-v$HELM_VERSION-linux-amd64.tar.gz + tar zxf /tmp/helm-v$HELM_VERSION-linux-amd64.tar.gz -C /tmp + sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm +} + +# Install sshpass +#---------------- +function install_sshpass { + sudo apt-get install -y sshpass +} + +# Create helm chart directory +#---------------------------- +function create_helm_chart_dir { + sudo mkdir -p $HELM_CHART_DIR +} + +# Set proxy to environment +#------------------------- +function set_env_proxy { + cat </dev/null +http_proxy=${http_proxy//%40/@} +https_proxy=${https_proxy//%40/@} +no_proxy=$no_proxy +EOF +} + +# Main +# ____ +install_helm +install_sshpass +create_helm_chart_dir +set_env_proxy +exit 0 diff --git a/samples/mgmt_driver/kubernetes_mgmt.py b/samples/mgmt_driver/kubernetes_mgmt.py index 20fe3aefe..34af03ec4 100644 --- a/samples/mgmt_driver/kubernetes_mgmt.py +++ b/samples/mgmt_driver/kubernetes_mgmt.py @@ -37,6 +37,10 @@ from tacker.vnfm.mgmt_drivers import vnflcm_abstract_driver LOG = logging.getLogger(__name__) K8S_CMD_TIMEOUT = 30 K8S_INSTALL_TIMEOUT = 2700 +HELM_CMD_TIMEOUT = 30 +HELM_INSTALL_TIMEOUT = 300 +HELM_CHART_DIR = "/var/tacker/helm" +HELM_CHART_CMP_PATH = "/tmp/tacker-helm.tgz" class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): @@ -97,15 +101,22 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): stdout = result.get_stdout() LOG.debug(stdout) LOG.debug(err) - elif type == 'certificate_key' or type == 'install': + elif type in ('certificate_key', 'install', 'scp'): if result.get_return_code() != 0: err = result.get_stderr() LOG.error(err) raise exceptions.MgmtDriverRemoteCommandError(err_info=err) + elif type == 'helm_repo_list': + if result.get_return_code() != 0: + err = result.get_stderr()[0].replace('\n', '') + if err == 'Error: no repositories to show': + return [] + raise exceptions.MgmtDriverRemoteCommandError(err_info=err) return result.get_stdout() def _create_vim(self, context, vnf_instance, server, bearer_token, - ssl_ca_cert, vim_name, project_name, master_vm_dict_list): + ssl_ca_cert, vim_name, project_name, master_vm_dict_list, + masternode_ip_list): # ha: create vim vim_info = { 'vim': { @@ -133,6 +144,16 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): register_ip, server) vim_info['vim']['auth_url'] = server del vim_info['vim']['auth_cred']['ssl_ca_cert'] + extra = {} + if masternode_ip_list: + username = master_vm_dict_list[0].get('ssh').get('username') + password = master_vm_dict_list[0].get('ssh').get('password') + helm_info = { + 'masternode_ip': masternode_ip_list, + 'masternode_username': username, + 'masternode_password': password} + extra['helm_info'] = str(helm_info) + vim_info['vim']['extra'] = extra try: nfvo_plugin = NfvoPlugin() created_vim_info = nfvo_plugin.create_vim(context, vim_info) @@ -149,7 +170,7 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): } vim_connection_info = objects.VimConnectionInfo( id=id, vim_id=vim_id, vim_type=vim_type, - access_info=access_info, interface_info=None + access_info=access_info, interface_info=None, extra=extra ) vim_connection_infos = vnf_instance.vim_connection_info vim_connection_infos.append(vim_connection_info) @@ -304,7 +325,8 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): return hosts_str def _init_commander_and_send_install_scripts(self, user, password, host, - vnf_package_path=None, script_path=None): + vnf_package_path=None, script_path=None, + helm_inst_script_path=None): retry = 4 while retry > 0: try: @@ -320,6 +342,10 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): "../../../samples/mgmt_driver/" "create_admin_token.yaml"), "/tmp/create_admin_token.yaml") + if helm_inst_script_path: + sftp.put(os.path.join( + vnf_package_path, helm_inst_script_path), + "/tmp/install_helm.sh") connect.close() commander = cmd_executer.RemoteCommandExecutor( user=user, password=password, host=host, @@ -377,9 +403,23 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): self._execute_command( commander, ssh_command, K8S_INSTALL_TIMEOUT, 'install', 0) + def _install_helm(self, commander, proxy): + ssh_command = "" + if proxy.get("http_proxy") and proxy.get("https_proxy"): + ssh_command += ("export http_proxy={http_proxy}; " + "export https_proxy={https_proxy}; " + "export no_proxy={no_proxy}; ").format( + http_proxy=proxy.get('http_proxy'), + https_proxy=proxy.get('https_proxy'), + no_proxy=proxy.get('no_proxy')) + ssh_command += "bash /tmp/install_helm.sh;" + self._execute_command( + commander, ssh_command, HELM_INSTALL_TIMEOUT, 'install', 0) + def _install_k8s_cluster(self, context, vnf_instance, proxy, script_path, - master_vm_dict_list, worker_vm_dict_list): + master_vm_dict_list, worker_vm_dict_list, + helm_inst_script_path): # instantiate: pre /etc/hosts hosts_str = self._get_hosts( master_vm_dict_list, worker_vm_dict_list) @@ -399,6 +439,16 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): raise exceptions.MgmtDriverOtherError( error_message="The path of install script is invalid") + # check helm install and get helm install script_path + masternode_ip_list = [] + if helm_inst_script_path: + abs_helm_inst_script_path = os.path.join( + vnf_package_path, helm_inst_script_path) + if not os.path.exists(abs_helm_inst_script_path): + LOG.error('The path of helm install script is invalid.') + raise exceptions.MgmtDriverParamInvalid( + param='helm_installation_script_path') + # set no proxy project_name = '' if proxy.get("http_proxy") and proxy.get("https_proxy"): @@ -446,7 +496,7 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): k8s_cluster = vm_dict.get('k8s_cluster', {}) commander = self._init_commander_and_send_install_scripts( user, password, host, - vnf_package_path, script_path) + vnf_package_path, script_path, helm_inst_script_path) # set /etc/hosts for each node ssh_command = "> /tmp/tmp_hosts" @@ -562,6 +612,9 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): bearer_token = self._execute_command( commander, ssh_command, K8S_CMD_TIMEOUT, 'common', 0)[0].replace('\n', '') + if helm_inst_script_path: + self._install_helm(commander, proxy) + masternode_ip_list.append(host) commander.close_session() # install worker node @@ -597,7 +650,8 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): cluster_ip, kubeadm_token, ssl_ca_cert_hash) commander.close_session() - return server, bearer_token, ssl_ca_cert, project_name + return (server, bearer_token, ssl_ca_cert, project_name, + masternode_ip_list) def _check_values(self, additional_param): for key, value in additional_param.items(): @@ -654,6 +708,8 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): master_node = additional_param.get('master_node', {}) worker_node = additional_param.get('worker_node', {}) proxy = additional_param.get('proxy', {}) + helm_inst_script_path = additional_param.get( + 'helm_installation_script_path', None) # check script_path if not script_path: LOG.error('The script_path in the ' @@ -695,15 +751,16 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): worker_vm_dict_list = self._get_install_info_for_k8s_node( nest_stack_id, worker_node, instantiate_vnf_request.additional_params, 'worker', access_info) - server, bearer_token, ssl_ca_cert, project_name = \ + server, bearer_token, ssl_ca_cert, project_name, masternode_ip_list = \ self._install_k8s_cluster(context, vnf_instance, proxy, script_path, master_vm_dict_list, - worker_vm_dict_list) + worker_vm_dict_list, + helm_inst_script_path) # register vim with kubernetes cluster info self._create_vim(context, vnf_instance, server, bearer_token, ssl_ca_cert, vim_name, project_name, - master_vm_dict_list) + master_vm_dict_list, masternode_ip_list) def terminate_start(self, context, vnf_instance, terminate_vnf_request, grant, @@ -1580,6 +1637,50 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): return target_physical_resource_ids + def _prepare_for_restoring_helm(self, commander, master_ip): + helm_info = {} + # get helm repo list + ssh_command = "helm repo list -o json" + result = self._execute_command( + commander, ssh_command, K8S_CMD_TIMEOUT, 'helm_repo_list', 0) + if result: + helmrepo_list = json.loads(result) + helm_info['ext_helmrepo_list'] = helmrepo_list + # compress local helm chart + ssh_command = ("sudo tar -zcf {cmp_path} -P {helm_chart_dir}" + .format(cmp_path=HELM_CHART_CMP_PATH, + helm_chart_dir=HELM_CHART_DIR)) + self._execute_command( + commander, ssh_command, HELM_INSTALL_TIMEOUT, 'common', 0) + helm_info['local_repo_src_ip'] = master_ip + + return helm_info + + def _restore_helm_repo(self, commander, master_username, master_password, + local_repo_src_ip, ext_repo_list): + # restore local helm chart + ssh_command = ( + "sudo sshpass -p {master_password} " + "scp -o StrictHostKeyChecking=no " + "{master_username}@{local_repo_src_ip}:{helm_chart_cmp_path} " + "{helm_chart_cmp_path};").format( + master_password=master_password, + master_username=master_username, + local_repo_src_ip=local_repo_src_ip, + helm_chart_cmp_path=HELM_CHART_CMP_PATH + ) + ssh_command += "sudo tar -Pzxf {helm_chart_cmp_path};".format( + helm_chart_cmp_path=HELM_CHART_CMP_PATH) + self._execute_command( + commander, ssh_command, HELM_CMD_TIMEOUT, 'scp', 0) + # restore external helm repository + if ext_repo_list: + for ext_repo in ext_repo_list: + ssh_command += "helm repo add {name} {url};".format( + name=ext_repo.get('name'), url=ext_repo.get('url')) + self._execute_command( + commander, ssh_command, HELM_CMD_TIMEOUT, 'common', 0) + def heal_start(self, context, vnf_instance, heal_vnf_request, grant, grant_request, **kwargs): @@ -1629,7 +1730,7 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): fixed_master_infos, proxy, master_username, master_password, vnf_package_path, script_path, cluster_ip, pod_cidr, cluster_cidr, - kubeadm_token, ssl_ca_cert_hash, ha_flag): + kubeadm_token, ssl_ca_cert_hash, ha_flag, helm_info): not_fixed_master_nic_ips = [ master_ips.get('master_nic_ip') for master_ips in not_fixed_master_infos.values()] @@ -1656,7 +1757,8 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): commander = self._init_commander_and_send_install_scripts( master_username, master_password, fixed_master_info.get('master_ssh_ip'), - vnf_package_path, script_path) + vnf_package_path, script_path, + helm_info.get('script_path', None)) self._set_node_ip_in_hosts( commander, 'heal_end', hosts_str=hosts_str) if proxy.get('http_proxy') and proxy.get('https_proxy'): @@ -1699,6 +1801,12 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): certificate_key=certificate_key) self._execute_command( commander, ssh_command, K8S_INSTALL_TIMEOUT, 'install', 0) + if helm_info: + self._install_helm(commander, proxy) + self._restore_helm_repo( + commander, master_username, master_password, + helm_info.get('local_repo_src_ip'), + helm_info.get('ext_helmrepo_list', '')) commander.close_session() for not_fixed_master_name, not_fixed_master in \ not_fixed_master_infos.items(): @@ -1814,6 +1922,15 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): ssl_ca_cert_hash = self._execute_command( commander, ssh_command, K8S_CMD_TIMEOUT, 'common', 3)[0].replace('\n', '') + + # prepare for restoring helm repository + helm_inst_script_path = k8s_cluster_installation_param.get( + 'helm_installation_script_path', None) + helm_info = {} + if helm_inst_script_path: + helm_info = self._prepare_for_restoring_helm(commander, master_ip) + helm_info['script_path'] = helm_inst_script_path + commander.close_session() if len(fixed_master_infos) + len(not_fixed_master_ssh_ips) == 1: ha_flag = False @@ -1829,7 +1946,7 @@ class KubernetesMgmtDriver(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver): fixed_master_infos, proxy, master_username, master_password, vnf_package_path, script_path, cluster_ip, pod_cidr, cluster_cidr, - kubeadm_token, ssl_ca_cert_hash, ha_flag) + kubeadm_token, ssl_ca_cert_hash, ha_flag, helm_info) if flag_worker: self._fix_worker_node( fixed_worker_infos, diff --git a/samples/mgmt_driver/kubernetes_vnf_package/TOSCA-Metadata/TOSCA.meta b/samples/mgmt_driver/kubernetes_vnf_package/TOSCA-Metadata/TOSCA.meta index 3fc942162..ea339e91e 100644 --- a/samples/mgmt_driver/kubernetes_vnf_package/TOSCA-Metadata/TOSCA.meta +++ b/samples/mgmt_driver/kubernetes_vnf_package/TOSCA-Metadata/TOSCA.meta @@ -11,7 +11,12 @@ Content-Type: application/sh Algorithm: SHA-256 Hash: bc859fb8ffb9f92a19139553bdd077428a2c9572196e5844f1c912a7a822c249 +Name: Scripts/install_helm.sh +Content-Type: application/sh +Algorithm: SHA-256 +Hash: 4af332b05e3e85662d403208e1e6d82e5276cbcd3b82a3562d2e3eb80d1ef714 + Name: Scripts/kubernetes_mgmt.py Content-Type: text/x-python Algorithm: SHA-256 -Hash: b8c558cad30f219634a668f84d6e04998949e941f3909b5c60374b84dff58545 +Hash: bf651994ca7422aadeb0a12fed179f44ab709029c2eee9b2b9c7e8cbf339a66d diff --git a/tacker/db/nfvo/nfvo_db.py b/tacker/db/nfvo/nfvo_db.py index 795e22b0d..6b28bb87f 100644 --- a/tacker/db/nfvo/nfvo_db.py +++ b/tacker/db/nfvo/nfvo_db.py @@ -38,6 +38,7 @@ class Vim(model_base.BASE, ), nullable=False) vim_auth = orm.relationship('VimAuth') status = sa.Column(sa.String(255), nullable=False) + extra = sa.Column(types.Json, nullable=True) __table_args__ = ( schema.UniqueConstraint( diff --git a/tacker/db/nfvo/nfvo_db_plugin.py b/tacker/db/nfvo/nfvo_db_plugin.py index 3d1c65811..edc9b51c0 100644 --- a/tacker/db/nfvo/nfvo_db_plugin.py +++ b/tacker/db/nfvo/nfvo_db_plugin.py @@ -31,7 +31,7 @@ from tacker.plugins.common import constants VIM_ATTRIBUTES = ('id', 'type', 'tenant_id', 'name', 'description', 'placement_attr', 'shared', 'is_default', - 'created_at', 'updated_at', 'status') + 'created_at', 'updated_at', 'status', 'extra') VIM_AUTH_ATTRIBUTES = ('auth_url', 'vim_project', 'password', 'auth_cred') @@ -87,6 +87,7 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin): placement_attr=vim.get('placement_attr'), is_default=vim.get('is_default'), status=vim.get('status'), + extra=vim.get('extra'), deleted_at=datetime.min) context.session.add(vim_db) vim_auth_db = nfvo_db.VimAuth( @@ -158,6 +159,8 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin): if 'placement_attr' in vim: vim_db.update( {'placement_attr': vim.get('placement_attr')}) + if 'extra' in vim: + vim_db.update({'extra': vim.get('extra')}) vim_auth_db = (self._model_query( context, nfvo_db.VimAuth).filter( nfvo_db.VimAuth.vim_id == vim_id).with_for_update().one()) diff --git a/tacker/extensions/vnfm.py b/tacker/extensions/vnfm.py index 4cbdda1b4..08972d8d3 100644 --- a/tacker/extensions/vnfm.py +++ b/tacker/extensions/vnfm.py @@ -129,6 +129,18 @@ class CNFHealWaitFailed(exceptions.TackerException): message = _('%(reason)s') +class InvalidVimConnectionInfo(exceptions.TackerException): + message = _('Invalid vim_connection_info: %(reason)s') + + +class HelmClientRemoteCommandError(exceptions.TackerException): + message = _('Failed to execute remote command.') + + +class HelmClientOtherError(exceptions.TackerException): + message = _('An error occurred in HelmClient: %(error_message)s.') + + class ServiceTypeNotFound(exceptions.NotFound): message = _('service type %(service_type_id)s could not be found') diff --git a/tacker/objects/vim_connection.py b/tacker/objects/vim_connection.py index f803bfbea..7d4256089 100644 --- a/tacker/objects/vim_connection.py +++ b/tacker/objects/vim_connection.py @@ -30,6 +30,8 @@ class VimConnectionInfo(base.TackerObject, base.TackerPersistentObject): default={}), 'access_info': fields.DictOfNullableStringsField(nullable=True, default={}), + 'extra': fields.DictOfNullableStringsField(nullable=True, + default={}), } @classmethod @@ -39,11 +41,13 @@ class VimConnectionInfo(base.TackerObject, base.TackerPersistentObject): vim_type = data_dict.get('vim_type') access_info = data_dict.get('access_info', {}) interface_info = data_dict.get('interface_info', {}) + extra = data_dict.get('extra', {}) obj = cls(id=id, vim_id=vim_id, vim_type=vim_type, interface_info=interface_info, - access_info=access_info) + access_info=access_info, + extra=extra) return obj @classmethod @@ -62,4 +66,5 @@ class VimConnectionInfo(base.TackerObject, base.TackerPersistentObject): 'vim_id': self.vim_id, 'vim_type': self.vim_type, 'interface_info': self.interface_info, - 'access_info': self.access_info} + 'access_info': self.access_info, + 'extra': self.extra} diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_df_helmchart.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_df_helmchart.yaml new file mode 100644 index 000000000..3b1d855a8 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_df_helmchart.yaml @@ -0,0 +1,151 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample CNF with helmchart + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - sample_vnfd_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: helmchart + requirements: + virtual_link_external: [] + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_description: A flavour for single resources + + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: vdu1-localhelm + description: kubernetes resource as VDU1 + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 3 + + VDU2: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: vdu2-apache + description: kubernetes resource as VDU2 + 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: [ VDU1 ] + diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_top.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_top.yaml new file mode 100644 index 000000000..51f8ca18a --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_top.yaml @@ -0,0 +1,31 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample CNF with Helm + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - sample_vnfd_types.yaml + - sample_vnfd_df_helmchart.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 CNF + 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_helmchart/Definitions/sample_vnfd_types.yaml b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_types.yaml new file mode 100644 index 000000000..e0c6757ff --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Definitions/sample_vnfd_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 CNF' ] ] + default: 'Sample CNF' + 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: [ helmchart ] ] + default: helmchart + 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_helmchart/Files/kubernetes/localhelm-0.1.0.tgz b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/Files/kubernetes/localhelm-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..42c5674ffcc47ba90cdc002860291324069b6492 GIT binary patch literal 3603 zcmV+u4(#zCiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PH+#ZyPz1`P{#vPWB>?i&`ze5;Twld`@oG*u)X+OcsN|AfUQS z631+^hh)pvD8Ao*AlYxVY$fs7w+r!u-D(w!RYlf=Rb?{~5jI1X3{GZ5YJWiz_T-*N z5Cp-|!GZl91VQ_EFno3JWOxu99Rvsad&B)F!Ek@LA3TBJz6t3XnbJf)34VC3Hgo?X zgQWB`N<{?^VZP^)G%Y_4{o#=xcrmI-(p1}b$MAP#2}I_sz(h#+M>a;uQKRxWNic-+ zKzVwRVhAKn8I6dU?#&C-fj{&E@3DE`jweql{ht$-q53Nfz#aO(e|UJ<*8lxi2V4Dr zh_M52aY8bt!R^0Q>he(d-VS`4Q3X^1fnSb4yzfthOo-MP!-O(40BPlE!A;8G^+FnoQwpCKN)Pvq#<6&T>^|&iB9o zhtTWgG>kIFE-;d)RS3WSX3bi$gaw;qjIF%%6v{b`@HmPrOO~>W6eBvJsNiabdWI4R zVI_6wxYeTJYNgSJBhMyKX4}es#AuMm~{7*9k zLo$d-*DQl9Q|MdR(+O~)L7@i1NKAGW5APx~#Fh!2iPz;%(d}?h?gFDsQoT44T;sJash+5xq{z|`_JSZV67B|zNJ_?(QB6@fxdX&fq{VX| zj^Dqx&k{-ek+a3Q5c+>8L$y#ElbW1m#*fvR;0B=m`e%hQgkesKm7iI`dASpoDUI@U zWDcv4I)vAO=TSbDs4P*)jcUXpObAohxlLxJ%hH|tR1EU@e#D4Ul{TybYNq-mvSKzw z_dI}^P};6+1Ykad?(FyRnj|Sh-zvHN9v~%pmIE7b^D%@${uy9QqD$m)SiXSb1fAKv zU5~g1Ix55hpqW!GyFYzUR@*vI^)*xZCRY6H6jXMt>Pb{0lMz;D|DOn%$_Qf%kQj+1 zK_1%^t_3Kopo`K-l(X&8*8&)lIf6DOkQfsf39ce4GDlW46Gn@S$ux&QQz(6BF9}lu zC^yQZP#IEr;P0P6BRy)lUjEO7CrxrV-+<=hy_e|OE6SDxv2$dpo@zU}yF%gz~gea42 z0b@z5Y$h5dJgr3i)X$M7wA!O6&8)g4`R4=^Ytx5&uRqY7#XmAso6hh^riF?KqkP(_ zQ3>Ve&Z{WnQ2-KhU3(GOlSkwf^~vbxpEYIlPpf|;lo4`m+>bk+J2}_~yZ(AekBb;D zkYS{SbkR*5JJTjS3PW8fm{n*aFV^r~9>755>A3wdj@b$I6r#0@1|MvIy zTk+q)(f-~x{(Fcq^x-FT7NT}jgP+ge!+2o~70+F}LNMlp@K}Z3a@hwu0sphRGZcO* zS4p_Ju@I8*DZ;L?+z`H5fwmUizyegzzI8?J?M&-*xikyyw&!YQVAOI6#+u*UgoD9y z>6Ws1;pS$!bjYd|9H`fRWD3n1k>Hw8uAp}&V(YDXCCRx9^Nq82`0(1ec{uy{_T87! z$Ma8bcApv39BBqq)X?vns7IwFLNJ7!BQu`BKP%tf+(2Kz?@Dk(1OgMh zGCm;F`7Oqr={ia{9pyI@GAERoqw3ef6!=AC5D^YnL`O3hCR;vD&|K4nr{BEKY92xV zYNOqITJ7G&yYtVdCmkDT(QUHsCJREpUl@MqEtkFJGFQZBl*f1t#a^?TAPX&o;JMW^ z-zu9^soqV;38+{J4Y93_wcM^%#xkllBRw$x;7Kv zbPKZ*G!SXku~a!gTDNd_4Pfe0uioyvyRGBHoP0-9k#8 z_9quSN=r*=jmp=~B$Yed`%D$p#YPYJUipC^_`~pZ@H)tEmG16eb1vp~Qc%7T#fFOh zM93>5;~M#9jc~Uql)7<$Y}fiPF}mWvFEeB*O6BYHUQ&T~#eWCGy;l5pc(^y*#(xhn zmdn8l@7;B3@4Kxmk-e?@-itwQoMW6&-eLaw)%W%m?YmNvwRDBet|!dkD`=VV2#)sc zBTX){$%I}*uitA0Xr86-#O0t!xq|Uw3-}`=j7})V#=2O_eea*>a9en7Rxn&B7-K|? zF;XJITG7JsJE6#81w^8|!(yIZqY9pl7giQ;&n^tC2>^_peC~Uv6OhhYIV7daaw$fa zCDU+488b0QrVO3R#3>nLIkmTlwsO;jcxwT7I|W%tZSD7)NZh#I{CU?>AHLaqODh^n zIXEP3$LHv5y!=MuQc1a9YbPdW^tepQo9|Y`sj_-#0)>?+U*V5TXlyaQiX|bOOfl|{ z7hxmfxX4rDU4koeJR69v&1b)p2$vGSE2Qk0ScH_}O0;y_32qzq?)3ldY}Kn9XY1@z z6U@(R2QByT?Dk8^^L5yGoy(d%DDd|3hn0&$CxR87e{4_uIHN26i!o(lks!aPc91*c zzoVm8{CBi>xUK&_$f);!kTg|;`Ov$hJPzS)c?!JZN*b2cyiHOI9_04Y)s3vFSe?U4 zhwv3RQfNfERUACu12Y_Ojw!OTKqy1)>!#$O)I)%rD7@Kwzm zP5NxIsmX64u$k6rs^PUs$+K_hZT*jS28QGMt$=~G<9XYqAXf7az&x^Ue~GBV=Jh7 zk@ovmdu*?Lf1@k@o2BGl+W_x~|At5V?fl=t(f;8!{(Fegy1=c8tQt{b1u~fLjZu># zV*I<1^iN}!*+|64IT*^v4IeEHpKL$JIZhm|uAtYvUkQAz5&Ra48$BpB&sUdg4JMyd z?^9Vfse;KTp_VkN3#WS*Z|`_1tXn_tV>{0shu8Etm6>$Xh|;Wl;}$rcqa;(TEwYxC z%_wTw_B%PerCH}YnRM+(Kg#C@bUkK1qRajlO&0g%1MaZ@d%;2b?>`O?xAy;G#vPqJ zMUHRRSh};kC~$`%Ea#|sWP;G3QNM>tKss<5paq&#EOWI6`%U%d(#WHGWmtBhr;?)#&y#^h^I8%$UxGu}J_HUa)`1Pp)efnEb zmXcFtUT(tsEq~l#@kumiO&5jHGn}J*Y~xYszi)ohDHjs&d5t^$|NiR#f7t5(!;Bpm z5v@_We=Oj(AFgJ|VVqGGQ$B^1M3-cWs{CgVDobtT4Qhsr!IX)y>zko``Vu5EqUjuM zj-&RT@Yve{j#Kv+hR;%o6MBuYbJ+j!+=q{xEkJNP(~txyO8b|}zURNa_;R6zL~jT3 zHrCH47Z6jaJby~{!2WdPd;a)OIj}#A7qjWW{3t%EIUiJr#w5DTQv2^Ll=s3{SE={H zACpV(g|8FyEF_(JFaFotfzL!zktsNR`%Za&D#h;@Y0sxIl7R!4;&;!Vt4PE+cx>I? Zj_ufv?YM3HUjP6A|NreAF8KgX003sF6Tbie literal 0 HcmV?d00001 diff --git a/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/TOSCA-Metadata/TOSCA.meta b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/TOSCA-Metadata/TOSCA.meta new file mode 100644 index 000000000..0de4977ff --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_cnf_helmchart/TOSCA-Metadata/TOSCA.meta @@ -0,0 +1,9 @@ +TOSCA-Meta-File-Version: 1.0 +Created-by: dummy_user +CSAR-Version: 1.1 +Entry-Definitions: Definitions/sample_vnfd_top.yaml + +Name: Files/kubernetes/localhelm-0.1.0.tgz +Content-Type: application/tar+gzip +Algorithm: SHA-256 +Hash: 837fcfb73e5fc58572851a80a0143373d9d28ec37bd3bdf52c4d7d34b97592d5 diff --git a/tacker/tests/functional/sol_kubernetes/vnflcm/test_kubernetes_helm.py b/tacker/tests/functional/sol_kubernetes/vnflcm/test_kubernetes_helm.py new file mode 100644 index 000000000..48d3ece45 --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes/vnflcm/test_kubernetes_helm.py @@ -0,0 +1,447 @@ +# 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 +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/{}".format( + 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/{}/package_content'.format( + 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 {} seconds".format( + VNF_PACKAGE_UPLOAD_TIMEOUT)) + + time.sleep(RETRY_WAIT_TIME) + + # remove temporarily created CSAR file + os.remove(file_path) + return vnf_package['id'], vnfd_id + + +class VnfLcmKubernetesHelmTest(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_helmchart", + {"key": "sample_helmchart_functional"}) + cls.vnf_instance_ids = [] + super(VnfLcmKubernetesHelmTest, 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/{}'.format(package_id) + cls.tacker_client.do_request(url, "DELETE") + + super(VnfLcmKubernetesHelmTest, cls).tearDownClass() + + def setUp(self): + super(VnfLcmKubernetesHelmTest, 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 '{}' is missing".format(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 _delete_wait_vnf_instance(self, id): + url = os.path.join("/vnflcm/v1/vnf_instances", id) + start_time = int(time.time()) + while True: + resp, body = self.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 {} seconds".format( + VNF_TERMINATE_TIMEOUT)) + + time.sleep(RETRY_WAIT_TIME) + + def _show_vnf_instance(self, id): + show_url = os.path.join("/vnflcm/v1/vnf_instances", id) + resp, vnf_instance = self.tacker_client.do_request(show_url, "GET") + + return vnf_instance + + def _vnf_instance_wait( + self, 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 = self.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 {} seconds".format(timeout)) + + time.sleep(RETRY_WAIT_TIME) + + 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) + self._vnf_instance_wait(id) + + def _create_and_instantiate_vnf_instance(self, flavour_id, + additional_params): + # create vnf instance + vnf_instance_name = "test_vnf_instance_for_cnf_heal-{}".format( + 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 = self._show_vnf_instance(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) + self._vnf_instance_wait( + id, + instantiation_state=fields.VnfInstanceState.NOT_INSTANTIATED, + timeout=VNF_TERMINATE_TIMEOUT) + + def _delete_vnf_instance(self, id): + self._delete_wait_vnf_instance(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 _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 _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_scale_cnf(self, vnf_instance): + """Test scale in/out CNF""" + def _test_scale(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 = self._show_vnf_instance(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 + + 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 = _test_scale( + vnf_instance['id'], 'SCALE_OUT', aspect_id, previous_level) + + # test scale in + previous_level = _test_scale( + vnf_instance['id'], 'SCALE_IN', aspect_id, previous_level) + + def _test_heal_cnf_with_sol002(self, vnf_instance): + """Test heal as per SOL002 for CNF""" + vnf_instance = self._show_vnf_instance(vnf_instance['id']) + before_vnfc_rscs = self._get_vnfc_resource_info(vnf_instance) + + # get vnfc_instance_id of heal target + before_pod_name = dict() + vnfc_instance_id = list() + for vnfc_rsc in before_vnfc_rscs: + if vnfc_rsc['vduId'] == "vdu1": + before_pod_name['vdu1'] = \ + vnfc_rsc['computeResource']['resourceId'] + elif vnfc_rsc['vduId'] == "vdu2": + before_pod_name['vdu2'] = \ + vnfc_rsc['computeResource']['resourceId'] + vnfc_instance_id.append(vnfc_rsc['id']) + + # test heal SOL-002 (partial heal) + 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 = self._show_vnf_instance(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['vduId'] == "vdu1": + # check stored pod name is changed (vdu1) + compute_resource = vnfc_rsc['computeResource'] + before_pod_name = compute_resource['resourceId'] + self.assertNotEqual(after_pod_name, before_pod_name['vdu1']) + elif vnfc_rsc['vduId'] == "vdu2": + # check stored pod name is changed (vdu2) + compute_resource = vnfc_rsc['computeResource'] + before_pod_name = compute_resource['resourceId'] + self.assertNotEqual(after_pod_name, before_pod_name['vdu2']) + + def _test_heal_cnf_with_sol003(self, vnf_instance): + """Test heal as per SOL003 for CNF""" + vnf_instance = self._show_vnf_instance(vnf_instance['id']) + 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 = self._show_vnf_instance(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']) + + def test_vnflcm_with_helmchart(self): + # use def-files of singleton Pod and Deployment (replicas=2) + helmchartfile_path = "Files/kubernetes/localhelm-0.1.0.tgz" + inst_additional_param = { + "namespace": "default", + "use_helm": "true", + "using_helm_install_param": [ + { + "exthelmchart": "false", + "helmchartfile_path": helmchartfile_path, + "helmreleasename": "vdu1", + "helmparameter": [ + "service.port=8081" + ] + }, + { + "exthelmchart": "true", + "helmreleasename": "vdu2", + "helmrepositoryname": "bitnami", + "helmchartname": "apache", + "exthelmrepo_url": "https://charts.bitnami.com/bitnami" + } + ] + } + vnf_instance = self._create_and_instantiate_vnf_instance( + "helmchart", inst_additional_param) + self._test_scale_cnf(vnf_instance) + self._test_heal_cnf_with_sol002(vnf_instance) + self._test_heal_cnf_with_sol003(vnf_instance) + + self._terminate_vnf_instance(vnf_instance['id']) + self._delete_vnf_instance(vnf_instance['id']) diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 6a1810578..f2c74c09b 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -719,7 +719,7 @@ def get_dummy_vim_connection_info(): 'user_domain_name': 'Default', 'username': 'admin'}, 'created_at': '', 'deleted': False, 'deleted_at': '', 'id': 'fake_id', 'updated_at': '', - 'vim_id': 'fake_vim_id', 'vim_type': 'openstack'} + 'vim_id': 'fake_vim_id', 'vim_type': 'openstack', 'extra': {}} def get_dummy_instantiate_vnf_request(**updates): @@ -1428,7 +1428,8 @@ VNFLCMOPOCC_RESPONSE = { "vimId": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', "vimType": 'openstack', 'interfaceInfo': {}, - "accessInfo": {"key1": 'value1', "key2": 'value2'}}], + "accessInfo": {"key1": 'value1', "key2": 'value2'}, + "extra": {}}], 'vimConnectionInfoDeleteIds': ['f8c35bd0-4d67-4436-9f11-14b8a84c92bb'], 'vnfPkgId': 'f26f181d-7891-4720-b022-b074ec1733ef', 'vnfInstanceName': 'fake_name', diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 6b2621165..e6f260ef3 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -198,7 +198,8 @@ class TestController(base.TestCase): 'vim_type': 'test', 'vim_auth': {'username': 'test', 'password': 'test'}, 'placement_attr': {'region': 'TestRegionOne'}, - 'tenant': 'test' + 'tenant': 'test', + 'extra': {} } self.context = context.get_admin_context() @@ -1237,14 +1238,16 @@ class TestController(base.TestCase): "region": "RegionOne", "password": "devstack", "tenant": "85d12da99f8246dfae350dbc7334a473", - } + }, + "extra": {} } vim_connection_info = objects.VimConnectionInfo( id=vim_info['id'], vim_id=vim_info['vim_id'], vim_type=vim_info['vim_type'], access_info=vim_info['access_info'], - interface_info=vim_info['interface_info']) + interface_info=vim_info['interface_info'], + extra=vim_info['extra']) mock_vnf_by_id.return_value = fakes.return_vnf_instance( fields.VnfInstanceState.INSTANTIATED, diff --git a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py index 9900322a9..da1c38f61 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py +++ b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py @@ -1117,3 +1117,143 @@ def fake_vim_connection_info(): return vim_connection.VimConnectionInfo( vim_type="kubernetes", access_info=access_info) + + +def fake_vim_connection_info_with_extra(del_field=None, multi_ip=False): + access_info = { + 'auth_url': 'http://fake_url:6443', + 'ssl_ca_cert': None} + masternode_ip = ["192.168.0.1"] + if multi_ip: + masternode_ip.append("192.168.0.2") + + helm_info = { + 'masternode_ip': masternode_ip, + 'masternode_username': 'dummy_user', + 'masternode_password': 'dummy_pass' + } + if del_field and helm_info.get(del_field): + del helm_info[del_field] + extra = { + 'helm_info': str(helm_info) + } + return vim_connection.VimConnectionInfo( + vim_type="kubernetes", + access_info=access_info, + extra=extra) + + +def fake_inst_vnf_req_for_helmchart(external=True, local=True, namespace=None): + additional_params = {"use_helm": "true"} + using_helm_install_param = list() + if external: + using_helm_install_param.append( + { + "exthelmchart": "true", + "helmreleasename": "myrelease-ext", + "helmrepositoryname": "sample-charts", + "helmchartname": "mychart-ext", + "exthelmrepo_url": "http://helmrepo.example.com/sample-charts" + } + ) + if local: + using_helm_install_param.append( + { + "exthelmchart": "false", + "helmchartfile_path": "Files/kubernetes/localhelm-0.1.0.tgz", + "helmreleasename": "myrelease-local", + "helmparameter": [ + "key1=value1", + "key2=value2" + ] + } + ) + additional_params['using_helm_install_param'] = using_helm_install_param + if namespace: + additional_params['namespace'] = namespace + + return objects.InstantiateVnfRequest(additional_params=additional_params) + + +def execute_cmd_helm_client(*args, **kwargs): + ssh_command = args[0] + if 'helm get manifest' in ssh_command: + result = [ + '---\n', + '# Source: localhelm/templates/deployment.yaml\n', + 'apiVersion: apps/v1\n', + 'kind: Deployment\n', + 'metadata:\n', + ' name: vdu1\n', + 'spec:\n', + ' replicas: 1\n', + ' selector:\n', + ' matchLabels:\n', + ' app: webserver\n', + ' template:\n', + ' metadata:\n' + ' labels:\n' + ' app: webserver\n' + ' spec:\n', + ' containers:\n', + ' - name: nginx\n' + ] + else: + result = "" + return result + + +def fake_k8s_objs_deployment_for_helm(): + obj = [ + { + 'status': 'Creating', + 'object': fake_v1_deployment_for_helm() + } + ] + + return obj + + +def fake_v1_deployment_for_helm(): + return client.V1Deployment( + api_version='apps/v1', + kind='Deployment', + metadata=client.V1ObjectMeta( + name='vdu1', + ), + status=client.V1DeploymentStatus( + replicas=1, + ready_replicas=1 + ), + spec=client.V1DeploymentSpec( + replicas=1, + selector=client.V1LabelSelector( + match_labels={'app': 'webserver'} + ), + template=client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta( + labels={'app': 'webserver'} + ), + spec=client.V1PodSpec( + containers=[ + client.V1Container( + name='nginx' + ) + ] + ) + ) + ) + ) + + +def fake_k8s_vim_obj(): + vim_obj = {'vim_id': '76107920-e588-4865-8eca-f33a0f827071', + 'vim_name': 'fake_k8s_vim', + 'vim_auth': { + 'auth_url': 'http://localhost:6443', + 'password': 'test_pw', + 'username': 'test_user', + 'project_name': 'test_project'}, + 'vim_type': 'kubernetes', + 'extra': {}} + return vim_obj diff --git a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver_helm.py b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver_helm.py new file mode 100644 index 000000000..49832cd2b --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_kubernetes_driver_helm.py @@ -0,0 +1,509 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import eventlet +import os +import paramiko + +from ddt import ddt +from kubernetes import client +from oslo_serialization import jsonutils +from tacker import context +from tacker.db.db_sqlalchemy import models +from tacker.extensions import common_services as cs +from tacker.extensions import vnfm +from tacker import objects +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 +from tacker.vnfm.infra_drivers.kubernetes.helm import helm_client +from tacker.vnfm.infra_drivers.kubernetes import kubernetes_driver +from tacker.vnfm import vim_client +from unittest import mock + + +class FakeRemoteCommandExecutor(mock.Mock): + def close_session(self): + return + + +class FakeCommander(mock.Mock): + def config(self, is_success, errmsg=None): + self.is_success = is_success + self.errmsg = errmsg + + def execute_command(self, *args, **kwargs): + is_success = self.is_success + fake_result = FakeCmdResult() + stderr = '' + stdout = '' + return_code = (0) if is_success else (1) + stderr, stdout = ('', '') if is_success else ('err', '') + if self.errmsg: + stderr = [self.errmsg] + fake_result.set_std(stderr, stdout, return_code) + return fake_result + + +class FakeCmdResult(mock.Mock): + def set_std(self, stderr, stdout, return_code): + self.stderr = stderr + self.stdout = stdout + self.return_code = return_code + + def get_stderr(self): + return self.stderr + + def get_stdout(self): + return self.stdout + + def get_return_code(self): + return self.return_code + + +class FakeTransport(mock.Mock): + pass + + +@ddt +class TestKubernetesHelm(base.TestCase): + def setUp(self): + super(TestKubernetesHelm, self).setUp() + self.kubernetes = kubernetes_driver.Kubernetes() + self.kubernetes.STACK_RETRIES = 1 + self.kubernetes.STACK_RETRY_WAIT = 5 + self.k8s_client_dict = fakes.fake_k8s_client_dict() + self.context = context.get_admin_context() + self.vnf_instance = fd_utils.get_vnf_instance_object() + self.package_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../../../etc/samples/etsi/nfv/test_cnf_helmchart") + self._mock_remote_command_executor() + self._mock_transport() + self.helm_client = helm_client.HelmClient('127.0.0.1', 'user', 'pass') + self.helm_client.commander = FakeCommander() + + def _mock_remote_command_executor(self): + self.commander = mock.Mock(wraps=FakeRemoteCommandExecutor()) + fake_commander = mock.Mock() + fake_commander.return_value = self.commander + self._mock( + 'tacker.common.cmd_executer.RemoteCommandExecutor', + fake_commander) + + def _mock_transport(self): + self.transport = mock.Mock(wraps=FakeTransport()) + fake_transport = mock.Mock() + fake_transport.return_value = self.transport + self._mock('paramiko.Transport', fake_transport) + + def _mock(self, target, new=mock.DEFAULT): + patcher = mock.patch(target, new) + return patcher.start() + + @mock.patch.object(eventlet, 'monkey_patch') + def test_execute_command_success(self, mock_monkey_patch): + self.helm_client.commander.config(True) + ssh_command = 'helm install' + timeout = 120 + retry = 1 + self.helm_client._execute_command( + ssh_command, timeout, retry) + + @mock.patch.object(eventlet, 'monkey_patch') + def test_execute_command_failed(self, mock_monkey_patch): + self.helm_client.commander.config(False) + ssh_command = 'helm install' + timeout = 120 + retry = 1 + self.assertRaises(vnfm.HelmClientRemoteCommandError, + self.helm_client._execute_command, + ssh_command, timeout, retry) + + @mock.patch.object(eventlet, 'monkey_patch') + @mock.patch.object(FakeCommander, 'execute_command') + def test_execute_command_timeout(self, mock_execute_command, + mock_monkey_patch): + mock_execute_command.side_effect = eventlet.timeout.Timeout + ssh_command = 'helm install' + timeout = 120 + retry = 1 + self.assertRaises(vnfm.HelmClientOtherError, + self.helm_client._execute_command, + ssh_command, timeout, retry) + + def test_pre_instantiation_vnf_helm(self): + vnf_instance = fd_utils.get_vnf_instance_object() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + vnf_software_images = None + vnf_package_path = self.package_path + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart() + vnf_resources = self.kubernetes.pre_instantiation_vnf( + self.context, vnf_instance, vim_connection_info, + vnf_software_images, + instantiate_vnf_req, vnf_package_path) + self.assertEqual(vnf_resources, {}) + + def test_pre_helm_install_with_bool_param(self): + vnf_instance = fd_utils.get_vnf_instance_object() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + vnf_software_images = None + vnf_package_path = self.package_path + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart() + instantiate_vnf_req.additional_params['use_helm'] = True + using_helm_inst_params = instantiate_vnf_req.additional_params[ + 'using_helm_install_param'] + using_helm_inst_params[0]['exthelmchart'] = True + using_helm_inst_params[1]['exthelmchart'] = False + vnf_resources = self.kubernetes.pre_instantiation_vnf( + self.context, vnf_instance, vim_connection_info, + vnf_software_images, + instantiate_vnf_req, vnf_package_path) + self.assertEqual(vnf_resources, {}) + + def test_pre_helm_install_invaid_vimconnectioninfo_no_helm_info(self): + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + del vim_connection_info.extra['helm_info'] + vnf_package_path = self.package_path + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart() + exc = self.assertRaises(vnfm.InvalidVimConnectionInfo, + self.kubernetes._pre_helm_install, + vim_connection_info, instantiate_vnf_req, + vnf_package_path) + msg = ("Invalid vim_connection_info: " + "helm_info is missing in vim_connection_info.extra.") + self.assertEqual(msg, exc.format_message()) + + def test_pre_helm_install_invaid_vimconnectioninfo_no_masternode_ip(self): + vim_connection_info = fakes.fake_vim_connection_info_with_extra( + del_field='masternode_ip') + vnf_package_path = self.package_path + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart() + exc = self.assertRaises(vnfm.InvalidVimConnectionInfo, + self.kubernetes._pre_helm_install, + vim_connection_info, instantiate_vnf_req, + vnf_package_path) + msg = ("Invalid vim_connection_info: " + "content of helm_info is invalid.") + self.assertEqual(msg, exc.format_message()) + + def test_pre_helm_install_invalid_helm_param(self): + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + vnf_package_path = self.package_path + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=True) + using_helm_inst_params = instantiate_vnf_req.additional_params[ + 'using_helm_install_param'] + del using_helm_inst_params[0]['exthelmchart'] + exc = self.assertRaises(cs.InputValuesMissing, + self.kubernetes._pre_helm_install, + vim_connection_info, instantiate_vnf_req, + vnf_package_path) + msg = ("Parameter input values missing for the key '{param}'".format( + param='exthelmchart')) + self.assertEqual(msg, exc.format_message()) + + def test_pre_helm_install_empty_helm_param(self): + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + vnf_package_path = self.package_path + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=False, local=False) + exc = self.assertRaises(cs.InputValuesMissing, + self.kubernetes._pre_helm_install, + vim_connection_info, instantiate_vnf_req, + vnf_package_path) + msg = ("Parameter input values missing for the key '{param}'".format( + param='using_helm_install_param')) + self.assertEqual(msg, exc.format_message()) + + def test_pre_helm_install_invalid_chartfile_path(self): + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + vnf_package_path = self.package_path + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=False) + using_helm_inst_params = instantiate_vnf_req.additional_params[ + 'using_helm_install_param'] + using_helm_inst_params[0]['helmchartfile_path'] = 'invalid_path' + exc = self.assertRaises(vnfm.CnfDefinitionNotFound, + self.kubernetes._pre_helm_install, + vim_connection_info, instantiate_vnf_req, + vnf_package_path) + msg = _("CNF definition file with path {path} is not found " + "in vnf_artifacts.").format( + path=using_helm_inst_params[0]['helmchartfile_path']) + self.assertEqual(msg, exc.format_message()) + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(paramiko.Transport, 'close') + @mock.patch.object(paramiko.SFTPClient, 'put') + @mock.patch.object(paramiko.SFTPClient, 'from_transport') + @mock.patch.object(paramiko.Transport, 'connect') + @mock.patch.object(helm_client.HelmClient, '_execute_command') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + def test_instantiate_vnf_using_helmchart( + self, mock_read_namespaced_deployment, mock_command, + mock_connect, mock_from_transport, mock_put, mock_close, + mock_vnf_resource_create): + vnf_instance = fd_utils.get_vnf_instance_object() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + deployment_obj = fakes.fake_v1_deployment_for_helm() + mock_read_namespaced_deployment.return_value = deployment_obj + vnfd_dict = fakes.fake_vnf_dict() + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=False) + grant_response = None + base_hot_dict = None + vnf_package_path = self.package_path + mock_command.side_effect = fakes.execute_cmd_helm_client + result = self.kubernetes.instantiate_vnf( + self.context, vnf_instance, vnfd_dict, vim_connection_info, + instantiate_vnf_req, grant_response, vnf_package_path, + base_hot_dict) + self.assertEqual( + result, + "{'namespace': '', 'name': 'vdu1', " + + "'apiVersion': 'apps/v1', 'kind': 'Deployment', " + + "'status': 'Create_complete'}") + self.assertEqual(mock_read_namespaced_deployment.call_count, 1) + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(paramiko.Transport, 'close') + @mock.patch.object(paramiko.SFTPClient, 'put') + @mock.patch.object(paramiko.SFTPClient, 'from_transport') + @mock.patch.object(paramiko.Transport, 'connect') + @mock.patch.object(helm_client.HelmClient, '_execute_command') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + def test_instantiate_vnf_using_helmchart_with_namespace( + self, mock_read_namespaced_deployment, mock_command, + mock_connect, mock_from_transport, mock_put, mock_close, + mock_vnf_resource_create): + vnf_instance = fd_utils.get_vnf_instance_object() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + deployment_obj = fakes.fake_v1_deployment_for_helm() + mock_read_namespaced_deployment.return_value = deployment_obj + vnfd_dict = fakes.fake_vnf_dict() + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + local=False, namespace='dummy_namespace') + grant_response = None + base_hot_dict = None + vnf_package_path = self.package_path + mock_command.side_effect = fakes.execute_cmd_helm_client + result = self.kubernetes.instantiate_vnf( + self.context, vnf_instance, vnfd_dict, vim_connection_info, + instantiate_vnf_req, grant_response, vnf_package_path, + base_hot_dict) + self.assertEqual( + result, + "{'namespace': 'dummy_namespace', 'name': 'vdu1', " + + "'apiVersion': 'apps/v1', 'kind': 'Deployment', " + + "'status': 'Create_complete'}") + self.assertEqual(mock_read_namespaced_deployment.call_count, 1) + + @mock.patch.object(objects.VnfResource, 'create') + @mock.patch.object(paramiko.Transport, 'close') + @mock.patch.object(paramiko.SFTPClient, 'put') + @mock.patch.object(paramiko.SFTPClient, 'from_transport') + @mock.patch.object(paramiko.Transport, 'connect') + @mock.patch.object(helm_client.HelmClient, '_execute_command') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + def test_instantiate_vnf_using_helmchart_multiple_ips( + self, mock_read_namespaced_deployment, mock_command, + mock_connect, mock_from_transport, mock_put, mock_close, + mock_vnf_resource_create): + vnf_instance = fd_utils.get_vnf_instance_object() + vim_connection_info = fakes.fake_vim_connection_info_with_extra( + multi_ip=True) + deployment_obj = fakes.fake_v1_deployment_for_helm() + mock_read_namespaced_deployment.return_value = deployment_obj + vnfd_dict = fakes.fake_vnf_dict() + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=False) + grant_response = None + base_hot_dict = None + vnf_package_path = self.package_path + mock_command.side_effect = fakes.execute_cmd_helm_client + result = self.kubernetes.instantiate_vnf( + self.context, vnf_instance, vnfd_dict, vim_connection_info, + instantiate_vnf_req, grant_response, vnf_package_path, + base_hot_dict) + self.assertEqual( + result, + "{'namespace': '', 'name': 'vdu1', " + + "'apiVersion': 'apps/v1', 'kind': 'Deployment', " + + "'status': 'Create_complete'}") + self.assertEqual(mock_read_namespaced_deployment.call_count, 1) + + @mock.patch.object(paramiko.Transport, 'close') + @mock.patch.object(paramiko.SFTPClient, 'put') + @mock.patch.object(paramiko.SFTPClient, 'from_transport') + @mock.patch.object(paramiko.Transport, 'connect') + @mock.patch.object(helm_client.HelmClient, '_execute_command') + def test_instantiate_vnf_using_helmchart_put_helmchart_fail( + self, mock_command, + mock_connect, mock_from_transport, mock_put, mock_close): + vnf_instance = fd_utils.get_vnf_instance_object() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + vnfd_dict = fakes.fake_vnf_dict() + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=False) + grant_response = None + base_hot_dict = None + vnf_package_path = self.package_path + mock_command.side_effect = fakes.execute_cmd_helm_client + mock_from_transport.side_effect = paramiko.SSHException() + self.assertRaises(paramiko.SSHException, + self.kubernetes.instantiate_vnf, + self.context, vnf_instance, vnfd_dict, vim_connection_info, + instantiate_vnf_req, grant_response, vnf_package_path, + base_hot_dict) + + @mock.patch.object(helm_client.HelmClient, '_execute_command') + @mock.patch.object(client.CoreV1Api, 'list_namespaced_pod') + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch('tacker.vnflcm.utils._get_vnfd_dict') + def test_post_vnf_instantiation_using_helmchart( + self, mock_vnfd_dict, mock_vnf_package_vnfd_get_by_id, + mock_list_namespaced_pod, mock_command): + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + mock_vnfd_dict.return_value = vnflcm_fakes.vnfd_dict_cnf() + mock_vnf_package_vnfd_get_by_id.return_value = \ + vnflcm_fakes.return_vnf_package_vnfd() + mock_list_namespaced_pod.return_value =\ + client.V1PodList(items=[ + fakes.get_fake_pod_info(kind='Deployment', name='vdu1')]) + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=False) + mock_command.side_effect = fakes.execute_cmd_helm_client + self.kubernetes.post_vnf_instantiation( + context=self.context, + vnf_instance=self.vnf_instance, + vim_connection_info=vim_connection_info, + instantiate_vnf_req=instantiate_vnf_req) + self.assertEqual(mock_list_namespaced_pod.call_count, 1) + # validate stored VnfcResourceInfo + vnfc_resource_info_after = \ + self.vnf_instance.instantiated_vnf_info.vnfc_resource_info + self.assertEqual(len(vnfc_resource_info_after), 1) + expected_pod = fakes.get_fake_pod_info('Deployment', 'vdu1') + self.assertEqual( + vnfc_resource_info_after[0].compute_resource.resource_id, + expected_pod.metadata.name) + self.assertEqual(vnfc_resource_info_after[0].compute_resource. + vim_level_resource_type, 'Deployment') + self.assertEqual(vnfc_resource_info_after[0].vdu_id, 'VDU1') + metadata_after = vnfc_resource_info_after[0].metadata + self.assertEqual(jsonutils.loads( + metadata_after.get('Deployment')).get('name'), 'vdu1') + + @mock.patch.object(helm_client.HelmClient, '_execute_command') + @mock.patch.object(vim_client.VimClient, 'get_vim') + def test_delete_using_helmchart( + self, mock_get_vim, mock_command): + vnf_id = 'fake_vnf_id' + mock_get_vim.return_value = fakes.fake_k8s_vim_obj() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=False) + vnf_instance = copy.deepcopy(self.vnf_instance) + vnf_instance.vim_connection_info = [vim_connection_info] + vnf_instance.instantiated_vnf_info.additional_params = \ + instantiate_vnf_req.additional_params + terminate_vnf_req = objects.TerminateVnfRequest() + mock_command.side_effect = fakes.execute_cmd_helm_client + self.kubernetes.delete(plugin=None, context=self.context, + vnf_id=vnf_id, + auth_attr=utils.get_vim_auth_obj(), + vnf_instance=vnf_instance, + terminate_vnf_req=terminate_vnf_req) + + @mock.patch.object(helm_client.HelmClient, '_execute_command') + @mock.patch.object(vim_client.VimClient, 'get_vim') + def test_delete_using_helmchart_with_namespace( + self, mock_get_vim, mock_command): + vnf_id = 'fake_vnf_id' + mock_get_vim.return_value = fakes.fake_k8s_vim_obj() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + local=False, namespace='dummy_namespace') + vnf_instance = copy.deepcopy(self.vnf_instance) + vnf_instance.vim_connection_info = [vim_connection_info] + vnf_instance.instantiated_vnf_info.additional_params = \ + instantiate_vnf_req.additional_params + terminate_vnf_req = objects.TerminateVnfRequest() + mock_command.side_effect = fakes.execute_cmd_helm_client + self.kubernetes.delete(plugin=None, context=self.context, + vnf_id=vnf_id, + auth_attr=utils.get_vim_auth_obj(), + vnf_instance=vnf_instance, + terminate_vnf_req=terminate_vnf_req) + + @mock.patch.object(helm_client.HelmClient, '_execute_command') + @mock.patch.object(vim_client.VimClient, 'get_vim') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + @mock.patch.object(objects.VnfResourceList, 'get_by_vnf_instance_id') + def test_delete_wait_using_helmchart( + self, mock_vnf_resource_list, mock_read_namespaced_deployment, + mock_get_vim, mock_command): + vnf_id = 'fake_vnf_id' + mock_get_vim.return_value = fakes.fake_k8s_vim_obj() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + external=False) + vnf_instance = copy.deepcopy(self.vnf_instance) + vnf_instance.vim_connection_info = [vim_connection_info] + vnf_instance.instantiated_vnf_info.additional_params = \ + instantiate_vnf_req.additional_params + vnf_resource = models.VnfResource() + vnf_resource.vnf_instance_id = vnf_instance.id + vnf_resource.resource_name = 'default,vdu1' + vnf_resource.resource_type = 'apps/v1,Deployment' + mock_vnf_resource_list.return_value = [vnf_resource] + mock_command.side_effect = fakes.execute_cmd_helm_client + self.kubernetes.delete_wait(plugin=None, context=self.context, + vnf_id=vnf_id, + auth_attr=utils.get_vim_auth_obj(), + region_name=None, + vnf_instance=vnf_instance) + self.assertEqual(mock_read_namespaced_deployment.call_count, 1) + + @mock.patch.object(helm_client.HelmClient, '_execute_command') + @mock.patch.object(vim_client.VimClient, 'get_vim') + @mock.patch.object(client.AppsV1Api, 'read_namespaced_deployment') + @mock.patch.object(objects.VnfResourceList, 'get_by_vnf_instance_id') + def test_delete_wait_using_helmchart_unknown_apiversion( + self, mock_vnf_resource_list, mock_read_namespaced_deployment, + mock_get_vim, mock_command): + vnf_id = 'fake_vnf_id' + mock_get_vim.return_value = fakes.fake_k8s_vim_obj() + vim_connection_info = fakes.fake_vim_connection_info_with_extra() + instantiate_vnf_req = fakes.fake_inst_vnf_req_for_helmchart( + local=False) + vnf_instance = copy.deepcopy(self.vnf_instance) + vnf_instance.vim_connection_info = [vim_connection_info] + vnf_instance.instantiated_vnf_info.additional_params = \ + instantiate_vnf_req.additional_params + vnf_resource = models.VnfResource() + vnf_resource.vnf_instance_id = vnf_instance.id + vnf_resource.resource_name = 'default,vdu1' + vnf_resource.resource_type = 'apps/v1unknown,Deployment' + mock_vnf_resource_list.return_value = [vnf_resource] + mock_command.side_effect = fakes.execute_cmd_helm_client + self.kubernetes.delete_wait(plugin=None, context=self.context, + vnf_id=vnf_id, + auth_attr=utils.get_vim_auth_obj(), + region_name=None, + vnf_instance=vnf_instance) + self.assertEqual(mock_read_namespaced_deployment.call_count, 0) diff --git a/tacker/tests/unit/vnfm/test_vim_client.py b/tacker/tests/unit/vnfm/test_vim_client.py index cb549a428..099c1ed7f 100644 --- a/tacker/tests/unit/vnfm/test_vim_client.py +++ b/tacker/tests/unit/vnfm/test_vim_client.py @@ -74,7 +74,7 @@ class TestVIMClient(base.TestCase): vim_expect = {'vim_auth': {'password': '****'}, 'vim_id': 'aaaa', 'vim_name': 'VIM0', 'vim_type': 'test_vim', 'placement_attr': {'regions': ['TestRegionOne']}, - 'tenant': 'test'} + 'tenant': 'test', 'extra': {}} self.assertEqual(vim_expect, vim_result) def test_get_vim_with_default_name(self): @@ -91,7 +91,7 @@ class TestVIMClient(base.TestCase): vim_expect = {'vim_auth': {'password': '****'}, 'vim_id': 'aaaa', 'vim_name': 'aaaa', 'vim_type': 'test_vim', 'placement_attr': {'regions': ['TestRegionOne']}, - 'tenant': 'test'} + 'tenant': 'test', 'extra': {}} self.assertEqual(vim_expect, vim_result) def test_find_vim_key_with_key_not_found_exception(self): diff --git a/tacker/vnflcm/utils.py b/tacker/vnflcm/utils.py index 05ef8936f..fbaa403b9 100644 --- a/tacker/vnflcm/utils.py +++ b/tacker/vnflcm/utils.py @@ -45,9 +45,11 @@ def _get_vim(context, vim_connection_info): region_name = access_info.get('region') else: region_name = None + extra = vim_connection_info[0].extra else: vim_id = None region_name = None + extra = {} try: vim_res = vim_client_obj.get_vim( @@ -56,9 +58,13 @@ def _get_vim(context, vim_connection_info): raise exceptions.VimConnectionNotFound(vim_id=vim_id) vim_res['vim_auth'].update({'region': region_name}) + if extra: + for key, value in extra.items(): + vim_res['extra'][key] = value vim_info = {'id': vim_res['vim_id'], 'vim_id': vim_res['vim_id'], 'vim_type': vim_res['vim_type'], - 'access_info': vim_res['vim_auth']} + 'access_info': vim_res['vim_auth'], + 'extra': vim_res.get('extra', {})} return vim_info diff --git a/tacker/vnfm/infra_drivers/kubernetes/helm/__init__.py b/tacker/vnfm/infra_drivers/kubernetes/helm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/vnfm/infra_drivers/kubernetes/helm/helm_client.py b/tacker/vnfm/infra_drivers/kubernetes/helm/helm_client.py new file mode 100644 index 000000000..fbcb787cf --- /dev/null +++ b/tacker/vnfm/infra_drivers/kubernetes/helm/helm_client.py @@ -0,0 +1,152 @@ +# 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 eventlet +from oslo_log import log as logging +import paramiko + +from tacker.common import cmd_executer +from tacker.extensions import vnfm + +LOG = logging.getLogger(__name__) +HELM_CMD_TIMEOUT = 30 +HELM_INSTALL_TIMEOUT = 120 +TRANSPORT_RETRIES = 2 +TRANSPORT_WAIT = 15 + + +class HelmClient(object): + """Helm client for hosting containerized vnfs""" + + def __init__(self, ip, username, password): + self.host_ip = ip + self.username = username + self.password = password + self.commander = cmd_executer.RemoteCommandExecutor( + user=username, + password=password, + host=ip, + timeout=HELM_CMD_TIMEOUT) + + def _execute_command(self, ssh_command, timeout=HELM_CMD_TIMEOUT, retry=0): + eventlet.monkey_patch() + while retry >= 0: + try: + with eventlet.Timeout(timeout, True): + result = self.commander.execute_command( + ssh_command, input_data=None) + break + except eventlet.timeout.Timeout: + error_message = ('It is time out, When execute command: {}.' + .format(ssh_command)) + LOG.debug(error_message) + retry -= 1 + if retry < 0: + self.close_session() + LOG.error(error_message) + raise vnfm.HelmClientOtherError( + error_message=error_message) + time.sleep(30) + if result.get_return_code(): + self.close_session() + err = result.get_stderr() + LOG.error(err) + raise vnfm.HelmClientRemoteCommandError(message=err) + return result.get_stdout() + + def add_repository(self, repo_name, repo_url): + # execute helm repo add command + ssh_command = "helm repo add {} {}".format(repo_name, repo_url) + self._execute_command(ssh_command) + + def remove_repository(self, repo_name): + # execute helm repo remove command + ssh_command = "helm repo remove {}".format(repo_name) + self._execute_command(ssh_command) + + def _transport_helmchart(self, source_path, target_path): + # transfer helm chart file + retry = TRANSPORT_RETRIES + while retry > 0: + try: + connect = paramiko.Transport(self.host_ip, 22) + connect.connect(username=self.username, password=self.password) + sftp = paramiko.SFTPClient.from_transport(connect) + # put helm chart file + sftp.put(source_path, target_path) + connect.close() + return + except paramiko.SSHException as e: + LOG.debug(e) + retry -= 1 + if retry == 0: + self.close_session() + LOG.error(e) + raise paramiko.SSHException() + time.sleep(TRANSPORT_WAIT) + + def put_helmchart(self, source_path, target_dir): + # create helm chart directory and change permission + ssh_command = ("if [ ! -d {target_dir} ]; then " + "`sudo mkdir -p {target_dir}; " + "sudo chown -R {username} {target_dir};`; fi").format( + target_dir=target_dir, username=self.username) + self._execute_command(ssh_command) + # get helm chart name and target path + chartfile_name = source_path[source_path.rfind(os.sep) + 1:] + target_path = os.path.join(target_dir, chartfile_name) + # transport helm chart file + self._transport_helmchart(source_path, target_path) + # decompress helm chart file + ssh_command = "tar -zxf {} -C {}".format(target_path, target_dir) + self._execute_command(ssh_command) + + def delete_helmchart(self, target_path): + # delete helm chart folder + ssh_command = "sudo rm -rf {}".format(target_path) + self._execute_command(ssh_command) + + def install(self, release_name, chart_name, namespace, parameters): + # execute helm install command + ssh_command = "helm install {} {}".format(release_name, chart_name) + if namespace: + ssh_command += " --namespace {}".format(namespace) + if parameters: + for param in parameters: + ssh_command += " --set {}".format(param) + self._execute_command(ssh_command, timeout=HELM_INSTALL_TIMEOUT) + + def uninstall(self, release_name, namespace): + # execute helm uninstall command + ssh_command = "helm uninstall {}".format(release_name) + if namespace: + ssh_command += " --namespace {}".format(namespace) + self._execute_command(ssh_command, timeout=HELM_INSTALL_TIMEOUT) + + def get_manifest(self, release_name, namespace): + # execute helm get manifest command + ssh_command = "helm get manifest {}".format(release_name) + if namespace: + ssh_command += " --namespace {}".format(namespace) + result = self._execute_command(ssh_command) + # convert manifest to text format + mf_content = ''.join(result) + return mf_content + + def close_session(self): + self.commander.close_session() diff --git a/tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py b/tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py index 99d8d9d0e..453a0be48 100644 --- a/tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py +++ b/tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py @@ -325,6 +325,35 @@ class Transformer(object): self._init_k8s_obj(k8s_obj, file_content_dict, must_param) return k8s_obj + def _get_k8s_obj_from_file_content_dict(self, file_content_dict, + namespace=None): + k8s_obj = {} + kind = file_content_dict.get('kind', '') + try: + k8s_obj['object'] = self._create_k8s_object( + kind, file_content_dict) + except Exception as e: + if isinstance(e, client.rest.ApiException): + msg = '{kind} create failure. Reason={reason}'.format( + kind=file_content_dict.get('kind', ''), reason=e.body) + else: + msg = '{kind} create failure. Reason={reason}'.format( + kind=file_content_dict.get('kind', ''), reason=e) + LOG.error(msg) + raise exceptions.InitApiFalse(error=msg) + if not file_content_dict.get('metadata', '') and not namespace: + k8s_obj['namespace'] = '' + elif file_content_dict.get('metadata', '').\ + get('namespace', ''): + k8s_obj['namespace'] = \ + file_content_dict.get('metadata', '').get( + 'namespace', '') + elif namespace: + k8s_obj['namespace'] = namespace + else: + k8s_obj['namespace'] = '' + return k8s_obj + def get_k8s_objs_from_yaml(self, artifact_files, vnf_package_path): k8s_objs = [] for artifact_file in artifact_files: @@ -339,33 +368,33 @@ class Transformer(object): file_content = f.read() file_content_dicts = list(yaml.safe_load_all(file_content)) for file_content_dict in file_content_dicts: - k8s_obj = {} - kind = file_content_dict.get('kind', '') - try: - k8s_obj['object'] = self._create_k8s_object( - kind, file_content_dict) - except Exception as e: - if isinstance(e, client.rest.ApiException): - msg = \ - _('{kind} create failure. Reason={reason}'.format( - kind=file_content_dict.get('kind', ''), - reason=e.body)) - else: - msg = \ - _('{kind} create failure. Reason={reason}'.format( - kind=file_content_dict.get('kind', ''), - reason=e)) - LOG.error(msg) - raise exceptions.InitApiFalse(error=msg) - if not file_content_dict.get('metadata', ''): - k8s_obj['namespace'] = '' - elif file_content_dict.get('metadata', '').\ - get('namespace', ''): - k8s_obj['namespace'] = \ - file_content_dict.get('metadata', '').get( - 'namespace', '') - else: - k8s_obj['namespace'] = '' + k8s_obj = self._get_k8s_obj_from_file_content_dict( + file_content_dict) + k8s_objs.append(k8s_obj) + return k8s_objs + + def get_k8s_objs_from_manifest(self, mf_content, namespace=None): + mkobj_kind_list = [ + "Pod", + "Service", + "PersistentVolumeClaim", + "Namespace", + "Node", + "PersistentVolume", + "DaemonSet", + "Deployment", + "ReplicaSet", + "StatefulSet", + "Job" + ] + k8s_objs = [] + mf_content_dicts = list(yaml.safe_load_all(mf_content)) + for mf_content_dict in mf_content_dicts: + kind = mf_content_dict.get('kind', '') + if kind in mkobj_kind_list: + k8s_obj = self._get_k8s_obj_from_file_content_dict( + file_content_dict=mf_content_dict, + namespace=namespace) k8s_objs.append(k8s_obj) return k8s_objs diff --git a/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py b/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py index 338ab6dad..e7e36389c 100644 --- a/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py +++ b/tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py @@ -31,6 +31,7 @@ from tacker.common.container import kubernetes_utils from tacker.common import exceptions from tacker.common import log from tacker.common import utils +from tacker.extensions import common_services as cs from tacker.extensions import vnfm from tacker import objects from tacker.objects.fields import ErrorPoint as EP @@ -39,6 +40,7 @@ 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.helm import helm_client from tacker.vnfm.infra_drivers.kubernetes.k8s import translate_outputs from tacker.vnfm.infra_drivers.kubernetes import translate_template from tacker.vnfm.infra_drivers import scale_driver @@ -71,6 +73,8 @@ def config_opts(): SCALING_POLICY = 'tosca.policies.tacker.Scaling' COMMA_CHARACTER = ',' +HELM_CHART_DIR_BASE = "/var/tacker/helm" + def get_scaling_policy_name(action, policy_name): return '%s_scale_%s' % (policy_name, action) @@ -804,6 +808,37 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, LOG.debug(e) pass + def _get_helm_info(self, vim_connection_info): + # replace single quote to double quote + helm_info = vim_connection_info.extra.get('helm_info') + helm_info_dq = helm_info.replace("'", '"') + helm_info_dict = jsonutils.loads(helm_info_dq) + return helm_info_dict + + def _helm_uninstall(self, context, vnf_instance): + inst_vnf_info = vnf_instance.instantiated_vnf_info + additional_params = inst_vnf_info.additional_params + namespace = additional_params.get('namespace', '') + helm_inst_param_list = additional_params.get( + 'using_helm_install_param') + vim_info = vnflcm_utils._get_vim(context, + vnf_instance.vim_connection_info) + vim_connection_info = objects.VimConnectionInfo.obj_from_primitive( + vim_info, context) + helm_info = self._get_helm_info(vim_connection_info) + ip_list = helm_info.get('masternode_ip') + username = helm_info.get('masternode_username') + password = helm_info.get('masternode_password') + k8s_objs = [] + # initialize HelmClient + helmclient = helm_client.HelmClient(ip_list[0], username, password) + for helm_inst_params in helm_inst_param_list: + release_name = helm_inst_params.get('helmreleasename') + # execute `helm uninstall` command + helmclient.uninstall(release_name, namespace) + helmclient.close_session() + return k8s_objs + @log.log def delete(self, plugin, context, vnf_id, auth_attr, region_name=None, vnf_instance=None, terminate_vnf_req=None): @@ -814,6 +849,11 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, # execute legacy delete method self._delete_legacy(vnf_id, auth_cred) else: + # check use_helm flag + inst_vnf_info = vnf_instance.instantiated_vnf_info + if self._is_use_helm_flag(inst_vnf_info.additional_params): + self._helm_uninstall(context, vnf_instance) + return # initialize Kubernetes APIs k8s_client_dict = self.kubernetes.\ get_k8s_client_dict(auth=auth_cred) @@ -962,6 +1002,35 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, return response + def _post_helm_uninstall(self, context, vnf_instance): + inst_vnf_info = vnf_instance.instantiated_vnf_info + additional_params = inst_vnf_info.additional_params + helm_inst_param_list = additional_params.get( + 'using_helm_install_param') + vim_info = vnflcm_utils._get_vim(context, + vnf_instance.vim_connection_info) + vim_connection_info = objects.VimConnectionInfo.obj_from_primitive( + vim_info, context) + helm_info = self._get_helm_info(vim_connection_info) + ip_list = helm_info.get('masternode_ip') + username = helm_info.get('masternode_username') + password = helm_info.get('masternode_password') + del_dir = os.path.join(HELM_CHART_DIR_BASE, vnf_instance.id) + for ip in ip_list: + local_helm_del_flag = False + # initialize HelmClient + helmclient = helm_client.HelmClient(ip, username, password) + for inst_params in helm_inst_param_list: + if self._is_exthelmchart(inst_params): + repo_name = inst_params.get('helmrepositoryname') + # execute `helm repo add` command + helmclient.remove_repository(repo_name) + else: + local_helm_del_flag = True + if local_helm_del_flag: + helmclient.delete_helmchart(del_dir) + helmclient.close_session() + @log.log def delete_wait(self, plugin, context, vnf_id, auth_attr, region_name=None, vnf_instance=None): @@ -1001,6 +1070,8 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, kind = vnf_resource.resource_type.\ split(COMMA_CHARACTER)[1] + if not k8s_client_dict.get(api_version): + continue try: self._select_k8s_obj_read_api( k8s_client_dict=k8s_client_dict, @@ -1019,6 +1090,11 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, time.sleep(self.STACK_RETRY_WAIT) else: keep_going = False + + # check use_helm flag + inst_vnf_info = vnf_instance.instantiated_vnf_info + if self._is_use_helm_flag(inst_vnf_info.additional_params): + self._post_helm_uninstall(context, vnf_instance) except Exception as e: LOG.error('Deleting wait VNF got an error due to %s', e) raise @@ -1138,6 +1214,7 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, vdu_defs = policy['vdu_defs'] is_found = False error_reason = None + target_kinds = ["Deployment", "ReplicaSet", "StatefulSet"] for vnf_resource in vnf_resources: # The resource that matches the following is the resource # to be scaled: @@ -1154,8 +1231,9 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, split(COMMA_CHARACTER)[0] kind = vnf_resource.resource_type.\ split(COMMA_CHARACTER)[1] - is_found = True - break + if kind in target_kinds: + is_found = True + break if is_found: break else: @@ -1165,13 +1243,6 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, aspect_id=aspect_id) raise vnfm.CNFScaleFailed(reason=error_reason) - 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) - scale_info = self._call_read_scale_api( app_v1_api_client=app_v1_api_client, namespace=namespace, @@ -1304,6 +1375,7 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, vdu_defs = policy['vdu_defs'] is_found = False error_reason = None + target_kinds = ["Deployment", "ReplicaSet", "StatefulSet"] for vnf_resource in vnf_resources: name = vnf_resource.resource_name.\ split(COMMA_CHARACTER)[1] @@ -1314,8 +1386,9 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, split(COMMA_CHARACTER)[0] kind = vnf_resource.resource_type.\ split(COMMA_CHARACTER)[1] - is_found = True - break + if kind in target_kinds: + is_found = True + break if is_found: break else: @@ -1407,6 +1480,70 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, def heal_vdu(self, plugin, context, vnf_dict, heal_request_data): pass + def _is_use_helm_flag(self, additional_params): + if not additional_params: + return False + use_helm = additional_params.get('use_helm') + if type(use_helm) == str: + return use_helm.lower() == 'true' + return bool(use_helm) + + def _is_exthelmchart(self, helm_install_params): + exthelmchart = helm_install_params.get('exthelmchart') + if type(exthelmchart) == str: + return exthelmchart.lower() == 'true' + return bool(exthelmchart) + + def _pre_helm_install(self, vim_connection_info, + instantiate_vnf_req, vnf_package_path): + def _check_param_exists(params_dict, check_param): + if check_param not in params_dict.keys(): + LOG.error("{check_param} is not found".format( + check_param=check_param)) + raise cs.InputValuesMissing(key=check_param) + + # check helm info in vim_connection_info + if 'helm_info' not in vim_connection_info.extra.keys(): + reason = "helm_info is missing in vim_connection_info.extra." + LOG.error(reason) + raise vnfm.InvalidVimConnectionInfo(reason=reason) + helm_info = self._get_helm_info(vim_connection_info) + ip_list = helm_info.get('masternode_ip', []) + username = helm_info.get('masternode_username', '') + password = helm_info.get('masternode_username', '') + if not (ip_list and username and password): + reason = "content of helm_info is invalid." + LOG.error(reason) + raise vnfm.InvalidVimConnectionInfo(reason=reason) + + # check helm install params + additional_params = instantiate_vnf_req.additional_params + _check_param_exists(additional_params, 'using_helm_install_param') + helm_install_param_list = additional_params.get( + 'using_helm_install_param', []) + if not helm_install_param_list: + LOG.error("using_helm_install_param is empty.") + raise cs.InputValuesMissing(key='using_helm_install_param') + for helm_install_params in helm_install_param_list: + # common parameter check + _check_param_exists(helm_install_params, 'exthelmchart') + _check_param_exists(helm_install_params, 'helmreleasename') + if self._is_exthelmchart(helm_install_params): + # parameter check (case: external helm chart) + _check_param_exists(helm_install_params, 'helmchartname') + _check_param_exists(helm_install_params, 'exthelmrepo_url') + _check_param_exists(helm_install_params, 'helmrepositoryname') + else: + # parameter check (case: local helm chart) + _check_param_exists(helm_install_params, 'helmchartfile_path') + chartfile_path = helm_install_params.get('helmchartfile_path') + abs_helm_chart_path = os.path.join( + vnf_package_path, chartfile_path) + if not os.path.exists(abs_helm_chart_path): + LOG.error('Helm chart file {path} is not found.'.format( + path=chartfile_path)) + raise vnfm.CnfDefinitionNotFound(path=chartfile_path) + def _get_target_k8s_files(self, instantiate_vnf_req): if instantiate_vnf_req.additional_params and\ CNF_TARGET_FILES_KEY in\ @@ -1417,9 +1554,39 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, target_k8s_files = list() return target_k8s_files + def _create_vnf_resource(self, context, vnf_instance, file_content_dict, + namespace=None): + vnf_resource = vnf_resource_obj.VnfResource( + context=context) + vnf_resource.vnf_instance_id = vnf_instance.id + metadata = file_content_dict.get('metadata', {}) + if metadata and metadata.get('namespace', ''): + namespace = metadata.get('namespace', '') + elif namespace: + namespace = namespace + else: + namespace = '' + vnf_resource.resource_name = ','.join([ + namespace, metadata.get('name', '')]) + vnf_resource.resource_type = ','.join([ + file_content_dict.get('apiVersion', ''), + file_content_dict.get('kind', '')]) + vnf_resource.resource_identifier = '' + vnf_resource.resource_status = '' + return vnf_resource + def pre_instantiation_vnf(self, context, vnf_instance, vim_connection_info, vnf_software_images, instantiate_vnf_req, vnf_package_path): + # check use_helm flag + if self._is_use_helm_flag(instantiate_vnf_req.additional_params): + # parameter check + self._pre_helm_install( + vim_connection_info, instantiate_vnf_req, vnf_package_path) + # NOTE: In case of using helm, vnf_resources is created + # after `helm install` command is executed. + return {} + vnf_resources = dict() target_k8s_files = self._get_target_k8s_files(instantiate_vnf_req) if not target_k8s_files: @@ -1470,19 +1637,8 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, file_content_dict_list = yaml.safe_load_all(file_content) vnf_resources_temp = [] for file_content_dict in file_content_dict_list: - vnf_resource = vnf_resource_obj.VnfResource( - context=context) - vnf_resource.vnf_instance_id = vnf_instance.id - vnf_resource.resource_name = ','.join([ - file_content_dict.get('metadata', {}).get( - 'namespace', ''), - file_content_dict.get('metadata', {}).get( - 'name', '')]) - vnf_resource.resource_type = ','.join([ - file_content_dict.get('apiVersion', ''), - file_content_dict.get('kind', '')]) - vnf_resource.resource_identifier = '' - vnf_resource.resource_status = '' + vnf_resource = self._create_vnf_resource( + context, vnf_instance, file_content_dict) vnf_resources_temp.append(vnf_resource) vnf_resources[target_k8s_index] = vnf_resources_temp return vnf_resources @@ -1491,13 +1647,76 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, vim_connection_info, vnf_resource): pass + def _helm_install(self, context, vnf_instance, vim_connection_info, + instantiate_vnf_req, vnf_package_path, transformer): + additional_params = instantiate_vnf_req.additional_params + namespace = additional_params.get('namespace', '') + helm_inst_param_list = additional_params.get( + 'using_helm_install_param') + helm_info = self._get_helm_info(vim_connection_info) + ip_list = helm_info.get('masternode_ip') + username = helm_info.get('masternode_username') + password = helm_info.get('masternode_password') + vnf_resources = [] + k8s_objs = [] + for ip_idx, ip in enumerate(ip_list): + # initialize HelmClient + helmclient = helm_client.HelmClient(ip, username, password) + for inst_params in helm_inst_param_list: + release_name = inst_params.get('helmreleasename') + parameters = inst_params.get('helmparameter') + if self._is_exthelmchart(inst_params): + # prepare using external helm chart + chart_name = inst_params.get('helmchartname') + repo_url = inst_params.get('exthelmrepo_url') + repo_name = inst_params.get('helmrepositoryname') + # execute `helm repo add` command + helmclient.add_repository(repo_name, repo_url) + install_chart_name = '/'.join([repo_name, chart_name]) + else: + # prepare using local helm chart + chartfile_path = inst_params.get('helmchartfile_path') + src_path = os.path.join(vnf_package_path, chartfile_path) + dst_dir = os.path.join( + HELM_CHART_DIR_BASE, vnf_instance.id) + # put helm chart file to Kubernetes controller node + helmclient.put_helmchart(src_path, dst_dir) + chart_file_name = src_path[src_path.rfind(os.sep) + 1:] + chart_name = "-".join(chart_file_name.split("-")[:-1]) + install_chart_name = os.path.join(dst_dir, chart_name) + if ip_idx == 0: + # execute `helm install` command + helmclient.install(release_name, install_chart_name, + namespace, parameters) + # get manifest by using `helm get manifest` command + mf_content = helmclient.get_manifest( + release_name, namespace) + k8s_objs_tmp = transformer.get_k8s_objs_from_manifest( + mf_content, namespace) + for k8s_obj in k8s_objs_tmp: + # set status in k8s_obj to 'Creating' + k8s_obj['status'] = 'Creating' + k8s_objs.extend(k8s_objs_tmp) + mf_content_dicts = list(yaml.safe_load_all(mf_content)) + for mf_content_dict in mf_content_dicts: + vnf_resource = self._create_vnf_resource( + context, vnf_instance, mf_content_dict, namespace) + vnf_resources.append(vnf_resource) + helmclient.close_session() + # save the vnf resources in the db + for vnf_resource in vnf_resources: + vnf_resource.create() + return k8s_objs + def instantiate_vnf(self, context, vnf_instance, vnfd_dict, vim_connection_info, instantiate_vnf_req, grant_response, vnf_package_path, plugin=None): target_k8s_files = self._get_target_k8s_files(instantiate_vnf_req) auth_attr = vim_connection_info.access_info - if not target_k8s_files: + use_helm_flag = self._is_use_helm_flag( + instantiate_vnf_req.additional_params) + if not target_k8s_files and not use_helm_flag: # The case is based on TOSCA for CNF operation. # It is out of the scope of this patch. instance_id = self.create( @@ -1509,9 +1728,14 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, transformer = translate_outputs.Transformer( None, None, None, k8s_client_dict) deployment_dict_list = list() - k8s_objs = transformer.\ - get_k8s_objs_from_yaml(target_k8s_files, vnf_package_path) - k8s_objs = transformer.deploy_k8s(k8s_objs) + if use_helm_flag: + k8s_objs = self._helm_install( + context, vnf_instance, vim_connection_info, + instantiate_vnf_req, vnf_package_path, transformer) + else: + k8s_objs = transformer.\ + get_k8s_objs_from_yaml(target_k8s_files, vnf_package_path) + k8s_objs = transformer.deploy_k8s(k8s_objs) vnfd_dict['current_error_point'] = EP.POST_VIM_CONTROL k8s_objs = self.create_wait_k8s( k8s_objs, k8s_client_dict, vnf_instance) @@ -1536,6 +1760,29 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, vnfd_dict['instance_id'] = resource_info_str return resource_info_str + def _post_helm_install(self, context, vim_connection_info, + instantiate_vnf_req, transformer): + additional_params = instantiate_vnf_req.additional_params + namespace = additional_params.get('namespace', '') + helm_inst_param_list = additional_params.get( + 'using_helm_install_param') + helm_info = self._get_helm_info(vim_connection_info) + ip_list = helm_info.get('masternode_ip') + username = helm_info.get('masternode_username') + password = helm_info.get('masternode_password') + k8s_objs = [] + # initialize HelmClient + helmclient = helm_client.HelmClient(ip_list[0], username, password) + for helm_inst_params in helm_inst_param_list: + release_name = helm_inst_params.get('helmreleasename') + # get manifest by using `helm get manifest` command + mf_content = helmclient.get_manifest(release_name, namespace) + k8s_objs_tmp = transformer.get_k8s_objs_from_manifest( + mf_content, namespace) + k8s_objs.extend(k8s_objs_tmp) + helmclient.close_session() + return k8s_objs + def post_vnf_instantiation(self, context, vnf_instance, vim_connection_info, instantiate_vnf_req): """Initially store VnfcResourceInfo after instantiation @@ -1554,9 +1801,13 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, # initialize Transformer transformer = translate_outputs.Transformer( None, None, None, None) - # get Kubernetes object - k8s_objs = transformer.get_k8s_objs_from_yaml( - target_k8s_files, vnf_package_path) + if self._is_use_helm_flag(instantiate_vnf_req.additional_params): + k8s_objs = self._post_helm_install(context, + vim_connection_info, instantiate_vnf_req, transformer) + else: + # get Kubernetes object + k8s_objs = transformer.get_k8s_objs_from_yaml( + target_k8s_files, vnf_package_path) # get TOSCA node templates vnfd_dict = vnflcm_utils._get_vnfd_dict( context, vnf_instance.vnfd_id, @@ -2094,6 +2345,7 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, aspect_id=scale_vnf_request.aspect_id, tosca=tosca) is_found = False + target_kinds = ["Deployment", "ReplicaSet", "StatefulSet"] for vnf_resource in vnf_resources: # For CNF operations, Kubernetes resource information is # stored in vnfc_resource as follows: @@ -2103,11 +2355,12 @@ class Kubernetes(abstract_driver.VnfAbstractDriver, for vdu_id, vdu_def in vdu_defs.items(): vdu_properties = vdu_def.get('properties') if rsc_name == vdu_properties.get('name'): - is_found = True namespace = vnf_resource.resource_name.split(',')[0] rsc_kind = vnf_resource.resource_type.split(',')[1] target_vdu_id = vdu_id - break + if rsc_kind in target_kinds: + is_found = True + break if is_found: break # extract stored Pod names by vdu_id diff --git a/tacker/vnfm/vim_client.py b/tacker/vnfm/vim_client.py index c99b23512..a91755804 100644 --- a/tacker/vnfm/vim_client.py +++ b/tacker/vnfm/vim_client.py @@ -65,7 +65,8 @@ class VimClient(object): 'vim_name': vim_info.get('name', vim_info['id']), 'vim_type': vim_info['type'], 'tenant': vim_info['tenant_id'], - 'placement_attr': vim_info.get('placement_attr', {})} + 'placement_attr': vim_info.get('placement_attr', {}), + 'extra': vim_info.get('extra', {})} return vim_res @staticmethod