#!/usr/bin/env python # Copyright (c) 2020 Orange and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 """Deploy and test Clearwater vIMS using Kubernetes""" from __future__ import division import abc import logging import os import re import subprocess import time import yaml from jinja2 import Template from kubernetes import client from kubernetes import config from kubernetes import watch import pkg_resources from xtesting.core import testcase class Vims(testcase.TestCase): # pylint: disable=too-many-instance-attributes """Deploy and test Clearwater vIMS using Kubernetes It leverage on the Python kubernetes client to apply operation proposed by clearwater-docker. See https://github.com/Metaswitch/clearwater-docker for more details """ watch_timeout = 1800 metadata_name = "env-vars" test_image_name = "ollivier/clearwater-live-test:hunter" test_container_name = "live-test" ns_generate_name = "ims-" dockerhub_repo = os.getenv("MIRROR_REPO", "docker.io") quay_repo = os.getenv("MIRROR_REPO", "quay.io") __logger = logging.getLogger(__name__) deployment_list = [ "astaire", "bono", "cassandra", "chronos", "ellis", "etcd", "homer", "homestead", "homestead-prov", "ralf", "sprout"] def __init__(self, **kwargs): super().__init__(**kwargs) config.load_kube_config() self.corev1 = client.CoreV1Api() self.appsv1 = client.AppsV1Api() self.output_log_name = 'functest-kubernetes.log' self.output_debug_log_name = 'functest-kubernetes.debug.log' self.namespace = "" self.zone = "" def prepare_vnf(self): """Prepare vIMS as proposed by clearwater-live-test It creates a dedicated namespace and the configmap needed. See https://github.com/Metaswitch/clearwater-live-test for more details """ api_response = self.corev1.create_namespace( client.V1Namespace(metadata=client.V1ObjectMeta( generate_name=self.ns_generate_name, labels={"pod-security.kubernetes.io/enforce": "baseline"}))) self.namespace = api_response.metadata.name self.__logger.debug("create_namespace: %s", api_response) self.zone = f'{self.namespace}.svc.cluster.local' metadata = client.V1ObjectMeta( name=self.metadata_name, namespace=self.namespace) body = client.V1ConfigMap( metadata=metadata, data={"ADDITIONAL_SHARED_CONFIG": "", "ZONE": self.zone}) api_response = self.corev1.create_namespaced_config_map( self.namespace, body=body) self.__logger.debug("create_namespaced_config_map: %s", api_response) @abc.abstractmethod def deploy_vnf(self): """Deploy vIMS as proposed by clearwater-docker It must be overriden on purpose. See https://github.com/Metaswitch/clearwater-docker for more details """ def wait_vnf(self): """Wait vIMS is up and running""" assert self.namespace status = self.deployment_list.copy() watch_deployment = watch.Watch() for event in watch_deployment.stream( func=self.appsv1.list_namespaced_deployment, namespace=self.namespace, timeout_seconds=self.watch_timeout): self.__logger.debug(event) if event["object"].status.ready_replicas == 1: if event['object'].metadata.name in status: status.remove(event['object'].metadata.name) self.__logger.info( "%s started in %0.2f sec", event['object'].metadata.name, time.time()-self.start_time) if not status: watch_deployment.stop() if not status: self.result = 1/2 * 100 return True self.__logger.error("Cannot deploy vIMS") return False def test_vnf(self): """Test vIMS as proposed by clearwater-live-test It leverages an unofficial Clearwater docker to allow testing from the Kubernetes cluster. See https://github.com/Metaswitch/clearwater-live-test for more details """ time.sleep(120) assert self.namespace assert self.zone container = client.V1Container( name=self.test_container_name, image=self.test_image_name, command=["rake", f"test[{self.zone}]", f"PROXY=bono.{self.zone}", f"ELLIS=ellis.{self.zone}", "SIGNUP_CODE=secret", "--trace"]) spec = client.V1PodSpec(containers=[container], restart_policy="Never") metadata = client.V1ObjectMeta(name=self.test_container_name) body = client.V1Pod(metadata=metadata, spec=spec) api_response = self.corev1.create_namespaced_pod(self.namespace, body) watch_deployment = watch.Watch() for event in watch_deployment.stream( func=self.corev1.list_namespaced_pod, namespace=self.namespace, timeout_seconds=self.watch_timeout): self.__logger.debug(event) if event["object"].metadata.name == self.test_container_name: if event["object"].status.phase in ('Succeeded', 'Failed'): watch_deployment.stop() api_response = self.corev1.read_namespaced_pod_log( name=self.test_container_name, namespace=self.namespace) self.__logger.info(api_response) vims_test_result = {} try: grp = re.search( r'^(\d+) failures out of (\d+) tests run.*\n' r'(\d+) tests skipped$', api_response, re.MULTILINE | re.DOTALL) assert grp vims_test_result["failures"] = int(grp.group(1)) vims_test_result["total"] = int(grp.group(2)) vims_test_result["skipped"] = int(grp.group(3)) vims_test_result['passed'] = ( int(grp.group(2)) - int(grp.group(3)) - int(grp.group(1))) if vims_test_result['total'] - vims_test_result['skipped'] > 0: vnf_test_rate = vims_test_result['passed'] / ( vims_test_result['total'] - vims_test_result['skipped']) else: vnf_test_rate = 0 self.result += 1/2 * 100 * vnf_test_rate except Exception: # pylint: disable=broad-except self.__logger.exception("Cannot parse live tests results") def run(self, **kwargs): self.start_time = time.time() try: self.prepare_vnf() self.deploy_vnf() if self.wait_vnf(): self.test_vnf() except client.rest.ApiException: self.__logger.exception("Cannot deploy and test vIms") self.stop_time = time.time() def clean(self): try: api_response = self.corev1.delete_namespaced_pod( name=self.test_container_name, namespace=self.namespace) self.__logger.debug("delete_namespaced_pod: %s", api_response) except client.rest.ApiException: pass try: api_response = self.corev1.delete_namespaced_config_map( name=self.metadata_name, namespace=self.namespace) self.__logger.debug( "delete_namespaced_config_map: %s", api_response) except client.rest.ApiException: pass try: api_response = self.corev1.delete_namespace(self.namespace) self.__logger.debug("delete_namespace: %s", self.namespace) except client.rest.ApiException: pass class K8sVims(Vims): """Deploy vIMS via kubectl as proposed by clearwater-docker It leverages unofficial Clearwater dockers as proposed in the documentation. See https://github.com/Metaswitch/clearwater-docker for more details """ __logger = logging.getLogger(__name__) def deploy_vnf(self): """Deploy vIMS via kubectl as proposed by clearwater-docker See https://github.com/Metaswitch/clearwater-docker for more details """ assert self.namespace for deployment in self.deployment_list: with open(pkg_resources.resource_filename( 'functest_kubernetes', f'ims/{deployment}-depl.yaml'), encoding='utf-8') as yfile: template = Template(yfile.read()) body = yaml.safe_load(template.render( dockerhub_repo=os.getenv( "DOCKERHUB_REPO", self.dockerhub_repo), quay_repo=os.getenv( "QUAY_REPO", self.quay_repo))) resp = self.appsv1.create_namespaced_deployment( body=body, namespace=self.namespace) self.__logger.info("Deployment %s created", resp.metadata.name) self.__logger.debug( "create_namespaced_deployment: %s", resp) for service in self.deployment_list: with open(pkg_resources.resource_filename( 'functest_kubernetes', f'ims/{service}-svc.yaml'), encoding='utf-8') as yfile: body = yaml.safe_load(yfile) resp = self.corev1.create_namespaced_service( body=body, namespace=self.namespace) self.__logger.info("Service %s created", resp.metadata.name) self.__logger.debug( "create_namespaced_service: %s", resp) def clean(self): for deployment in self.deployment_list: try: api_response = self.appsv1.delete_namespaced_deployment( name=deployment, namespace=self.namespace) self.__logger.debug( "delete_namespaced_deployment: %s", api_response) except client.rest.ApiException: pass try: api_response = self.corev1.delete_namespaced_service( name=deployment, namespace=self.namespace) self.__logger.debug( "delete_namespaced_service: %s", api_response) except client.rest.ApiException: pass super().clean() class HelmVims(Vims): """Deploy vIMS via Helm as proposed by clearwater-docker It leverages unofficial Clearwater dockers as proposed in the documentation. See https://github.com/Metaswitch/clearwater-docker for more details """ __logger = logging.getLogger(__name__) def deploy_vnf(self): """Deploy vIMS via Helm as proposed by clearwater-docker See https://github.com/Metaswitch/clearwater-docker for more details """ dockerhub_repo = os.getenv("DOCKERHUB_REPO", self.dockerhub_repo) quay_repo = os.getenv("QUAY_REPO", self.quay_repo) cmd = [ "helm", "install", "clearwater", "--set", f"repo.dockerHub={dockerhub_repo},repo.quay={quay_repo}", pkg_resources.resource_filename("functest_kubernetes", "ims/helm"), "-n", self.namespace] output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) self.__logger.debug(output.decode("utf-8")) def clean(self): cmd = ["helm", "uninstall", "clearwater", "-n", self.namespace] output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) self.__logger.debug(output.decode("utf-8")) super().clean()