Enforce baseline Pod Security Standard
[functest-kubernetes.git] / functest_kubernetes / ims / ims.py
1 #!/usr/bin/env python
2
3 # Copyright (c) 2020 Orange and others.
4 #
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9
10 """Deploy and test Clearwater vIMS using Kubernetes"""
11
12 from __future__ import division
13
14 import abc
15 import logging
16 import os
17 import re
18 import subprocess
19 import time
20 import yaml
21
22 from jinja2 import Template
23 from kubernetes import client
24 from kubernetes import config
25 from kubernetes import watch
26 import pkg_resources
27 from xtesting.core import testcase
28
29
30 class Vims(testcase.TestCase):  # pylint: disable=too-many-instance-attributes
31     """Deploy and test Clearwater vIMS using Kubernetes
32
33     It leverage on the Python kubernetes client to apply operation proposed by
34     clearwater-docker.
35
36     See https://github.com/Metaswitch/clearwater-docker for more details
37     """
38     watch_timeout = 1800
39     metadata_name = "env-vars"
40     test_image_name = "ollivier/clearwater-live-test:hunter"
41     test_container_name = "live-test"
42     ns_generate_name = "ims-"
43     dockerhub_repo = os.getenv("MIRROR_REPO", "docker.io")
44     quay_repo = os.getenv("MIRROR_REPO", "quay.io")
45
46     __logger = logging.getLogger(__name__)
47
48     deployment_list = [
49         "astaire", "bono", "cassandra", "chronos", "ellis", "etcd", "homer",
50         "homestead", "homestead-prov", "ralf", "sprout"]
51
52     def __init__(self, **kwargs):
53         super().__init__(**kwargs)
54         config.load_kube_config()
55         self.corev1 = client.CoreV1Api()
56         self.appsv1 = client.AppsV1Api()
57         self.output_log_name = 'functest-kubernetes.log'
58         self.output_debug_log_name = 'functest-kubernetes.debug.log'
59         self.namespace = ""
60         self.zone = ""
61
62     def prepare_vnf(self):
63         """Prepare vIMS as proposed by clearwater-live-test
64
65         It creates a dedicated namespace and the configmap needed.
66
67         See https://github.com/Metaswitch/clearwater-live-test for more details
68         """
69         api_response = self.corev1.create_namespace(
70             client.V1Namespace(metadata=client.V1ObjectMeta(
71                 generate_name=self.ns_generate_name,
72                 labels={"pod-security.kubernetes.io/enforce": "baseline"})))
73         self.namespace = api_response.metadata.name
74         self.__logger.debug("create_namespace: %s", api_response)
75         self.zone = f'{self.namespace}.svc.cluster.local'
76         metadata = client.V1ObjectMeta(
77             name=self.metadata_name, namespace=self.namespace)
78         body = client.V1ConfigMap(
79             metadata=metadata,
80             data={"ADDITIONAL_SHARED_CONFIG": "", "ZONE": self.zone})
81         api_response = self.corev1.create_namespaced_config_map(
82             self.namespace, body=body)
83         self.__logger.debug("create_namespaced_config_map: %s", api_response)
84
85     @abc.abstractmethod
86     def deploy_vnf(self):
87         """Deploy vIMS as proposed by clearwater-docker
88
89         It must be overriden on purpose.
90
91         See https://github.com/Metaswitch/clearwater-docker for more details
92         """
93
94     def wait_vnf(self):
95         """Wait vIMS is up and running"""
96         assert self.namespace
97         status = self.deployment_list.copy()
98         watch_deployment = watch.Watch()
99         for event in watch_deployment.stream(
100                 func=self.appsv1.list_namespaced_deployment,
101                 namespace=self.namespace, timeout_seconds=self.watch_timeout):
102             self.__logger.debug(event)
103             if event["object"].status.ready_replicas == 1:
104                 if event['object'].metadata.name in status:
105                     status.remove(event['object'].metadata.name)
106                     self.__logger.info(
107                         "%s started in %0.2f sec",
108                         event['object'].metadata.name,
109                         time.time()-self.start_time)
110             if not status:
111                 watch_deployment.stop()
112         if not status:
113             self.result = 1/2 * 100
114             return True
115         self.__logger.error("Cannot deploy vIMS")
116         return False
117
118     def test_vnf(self):
119         """Test vIMS as proposed by clearwater-live-test
120
121         It leverages an unofficial Clearwater docker to allow testing from
122         the Kubernetes cluster.
123
124         See https://github.com/Metaswitch/clearwater-live-test for more details
125         """
126         time.sleep(120)
127         assert self.namespace
128         assert self.zone
129         container = client.V1Container(
130             name=self.test_container_name, image=self.test_image_name,
131             command=["rake", f"test[{self.zone}]",
132                      f"PROXY=bono.{self.zone}",
133                      f"ELLIS=ellis.{self.zone}",
134                      "SIGNUP_CODE=secret", "--trace"])
135         spec = client.V1PodSpec(containers=[container], restart_policy="Never")
136         metadata = client.V1ObjectMeta(name=self.test_container_name)
137         body = client.V1Pod(metadata=metadata, spec=spec)
138         api_response = self.corev1.create_namespaced_pod(self.namespace, body)
139         watch_deployment = watch.Watch()
140         for event in watch_deployment.stream(
141                 func=self.corev1.list_namespaced_pod,
142                 namespace=self.namespace, timeout_seconds=self.watch_timeout):
143             self.__logger.debug(event)
144             if event["object"].metadata.name == self.test_container_name:
145                 if event["object"].status.phase in ('Succeeded', 'Failed'):
146                     watch_deployment.stop()
147         api_response = self.corev1.read_namespaced_pod_log(
148             name=self.test_container_name, namespace=self.namespace)
149         self.__logger.info(api_response)
150         vims_test_result = {}
151         try:
152             grp = re.search(
153                 r'^(\d+) failures out of (\d+) tests run.*\n'
154                 r'(\d+) tests skipped$', api_response,
155                 re.MULTILINE | re.DOTALL)
156             assert grp
157             vims_test_result["failures"] = int(grp.group(1))
158             vims_test_result["total"] = int(grp.group(2))
159             vims_test_result["skipped"] = int(grp.group(3))
160             vims_test_result['passed'] = (
161                 int(grp.group(2)) - int(grp.group(3)) - int(grp.group(1)))
162             if vims_test_result['total'] - vims_test_result['skipped'] > 0:
163                 vnf_test_rate = vims_test_result['passed'] / (
164                     vims_test_result['total'] - vims_test_result['skipped'])
165             else:
166                 vnf_test_rate = 0
167             self.result += 1/2 * 100 * vnf_test_rate
168         except Exception:  # pylint: disable=broad-except
169             self.__logger.exception("Cannot parse live tests results")
170
171     def run(self, **kwargs):
172         self.start_time = time.time()
173         try:
174             self.prepare_vnf()
175             self.deploy_vnf()
176             if self.wait_vnf():
177                 self.test_vnf()
178         except client.rest.ApiException:
179             self.__logger.exception("Cannot deploy and test vIms")
180         self.stop_time = time.time()
181
182     def clean(self):
183         try:
184             api_response = self.corev1.delete_namespaced_pod(
185                 name=self.test_container_name, namespace=self.namespace)
186             self.__logger.debug("delete_namespaced_pod: %s", api_response)
187         except client.rest.ApiException:
188             pass
189         try:
190             api_response = self.corev1.delete_namespaced_config_map(
191                 name=self.metadata_name, namespace=self.namespace)
192             self.__logger.debug(
193                 "delete_namespaced_config_map: %s", api_response)
194         except client.rest.ApiException:
195             pass
196         try:
197             api_response = self.corev1.delete_namespace(self.namespace)
198             self.__logger.debug("delete_namespace: %s", self.namespace)
199         except client.rest.ApiException:
200             pass
201
202
203 class K8sVims(Vims):
204     """Deploy vIMS via kubectl as proposed by clearwater-docker
205
206     It leverages unofficial Clearwater dockers as proposed in the
207     documentation.
208
209     See https://github.com/Metaswitch/clearwater-docker for more details
210     """
211
212     __logger = logging.getLogger(__name__)
213
214     def deploy_vnf(self):
215         """Deploy vIMS via kubectl as proposed by clearwater-docker
216
217         See https://github.com/Metaswitch/clearwater-docker for more details
218         """
219         assert self.namespace
220         for deployment in self.deployment_list:
221             with open(pkg_resources.resource_filename(
222                     'functest_kubernetes',
223                     f'ims/{deployment}-depl.yaml'),
224                     encoding='utf-8') as yfile:
225                 template = Template(yfile.read())
226                 body = yaml.safe_load(template.render(
227                     dockerhub_repo=os.getenv(
228                         "DOCKERHUB_REPO", self.dockerhub_repo),
229                     quay_repo=os.getenv(
230                         "QUAY_REPO", self.quay_repo)))
231                 resp = self.appsv1.create_namespaced_deployment(
232                     body=body, namespace=self.namespace)
233                 self.__logger.info("Deployment %s created", resp.metadata.name)
234                 self.__logger.debug(
235                     "create_namespaced_deployment: %s", resp)
236         for service in self.deployment_list:
237             with open(pkg_resources.resource_filename(
238                     'functest_kubernetes', f'ims/{service}-svc.yaml'),
239                     encoding='utf-8') as yfile:
240                 body = yaml.safe_load(yfile)
241                 resp = self.corev1.create_namespaced_service(
242                     body=body, namespace=self.namespace)
243                 self.__logger.info("Service %s created", resp.metadata.name)
244                 self.__logger.debug(
245                     "create_namespaced_service: %s", resp)
246
247     def clean(self):
248         for deployment in self.deployment_list:
249             try:
250                 api_response = self.appsv1.delete_namespaced_deployment(
251                     name=deployment, namespace=self.namespace)
252                 self.__logger.debug(
253                     "delete_namespaced_deployment: %s", api_response)
254             except client.rest.ApiException:
255                 pass
256             try:
257                 api_response = self.corev1.delete_namespaced_service(
258                     name=deployment, namespace=self.namespace)
259                 self.__logger.debug(
260                     "delete_namespaced_service: %s", api_response)
261             except client.rest.ApiException:
262                 pass
263         super().clean()
264
265
266 class HelmVims(Vims):
267     """Deploy vIMS via Helm as proposed by clearwater-docker
268
269     It leverages unofficial Clearwater dockers as proposed in the
270     documentation.
271
272     See https://github.com/Metaswitch/clearwater-docker for more details
273     """
274
275     __logger = logging.getLogger(__name__)
276
277     def deploy_vnf(self):
278         """Deploy vIMS via Helm as proposed by clearwater-docker
279
280         See https://github.com/Metaswitch/clearwater-docker for more details
281         """
282         dockerhub_repo = os.getenv("DOCKERHUB_REPO", self.dockerhub_repo)
283         quay_repo = os.getenv("QUAY_REPO", self.quay_repo)
284         cmd = [
285             "helm", "install", "clearwater", "--set",
286             f"repo.dockerHub={dockerhub_repo},repo.quay={quay_repo}",
287             pkg_resources.resource_filename("functest_kubernetes", "ims/helm"),
288             "-n", self.namespace]
289         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
290         self.__logger.debug(output.decode("utf-8"))
291
292     def clean(self):
293         cmd = ["helm", "uninstall", "clearwater", "-n", self.namespace]
294         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
295         self.__logger.debug(output.decode("utf-8"))
296         super().clean()