diff options
author | pawel.denst <pawel.denst@external.t-mobile.pl> | 2023-05-04 09:03:17 +0000 |
---|---|---|
committer | pawel.denst <pawel.denst@external.t-mobile.pl> | 2023-05-09 14:28:23 +0000 |
commit | 14c7c32e293fffb92b6aab7a81c446eaeb087cde (patch) | |
tree | 1a71ff3258da2e5bc7164c7f0b288a790e6c8795 | |
parent | a7a3fb72339042b2f455d5e5dfe8f7b685c7ec55 (diff) |
Migration of the healthchecks to gerrit
Changes to set default directory for results if None
Issue-ID: INT-2226
Signed-off-by: pawel.denst <pawel.denst@external.t-mobile.pl>
Change-Id: I5337a55f3271ebb5e58298e1fb1aa3b665713909
21 files changed, 1961 insertions, 43 deletions
diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 6684e1c..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,26 +0,0 @@ -from docs_conf.conf import * - -branch = 'latest' -master_doc = 'index' - -linkcheck_ignore = [ - r'http://localhost:.*', - 'http://CONSUL_SERVER_UI:30270/ui/#/dc1/services', - r'https://.*h=frankfurt', - r'http.*frankfurt.*', - r'http.*simpledemo.onap.org.*', - r'http://ANY_K8S_IP.*', - 'http://so-monitoring:30224', - r'http://SINK_IP_ADDRESS:667.*', - r'http.*K8S_HOST:30227.*', - r'http.*K8S_NODE_IP.*' -] - -intersphinx_mapping = {} - -html_theme = 'sphinx_rtd_theme' -html_last_updated_fmt = '%d-%b-%y %H:%M' -html_theme_options = {'body_max_width': '100%'} - -def setup(app): - app.add_stylesheet("css/ribbon.css") diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt deleted file mode 100644 index b3188dd..0000000 --- a/docs/requirements-docs.txt +++ /dev/null @@ -1,15 +0,0 @@ -tox -Sphinx -doc8 -docutils -setuptools -six -sphinx_rtd_theme>=0.4.3 -sphinxcontrib-blockdiag -sphinxcontrib-needs>=0.2.3 -sphinxcontrib-nwdiag -sphinxcontrib-seqdiag -sphinxcontrib-swaggerdoc -sphinxcontrib-plantuml -sphinx_bootstrap_theme -lfdocs-conf diff --git a/requirements.txt b/requirements.txt index 91c5ebc..7866370 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ openstacksdk>=0.61.0 onapsdk==10.4.3 jinja2>3 kubernetes>=22.6.0 +setuptools==65.3.0 +natural==0.2.0
\ No newline at end of file diff --git a/run_status.py b/run_status.py new file mode 100644 index 0000000..cf36cb6 --- /dev/null +++ b/run_status.py @@ -0,0 +1,3 @@ +from onaptests.scenario.status import Status +status=Status(dir_result="src") +status.run()
\ No newline at end of file @@ -58,3 +58,4 @@ xtesting.testcase = multi_vnf_ubuntu_macro = onaptests.scenario.multi_vnf_macro:MultiVnfUbuntuMacro basic_cnf_macro = onaptests.scenario.basic_cnf_macro:BasicCnfMacro basic_cps = onaptests.scenario.basic_cps:BasicCps + namespace_status = onaptests.scenario.status:Status
\ No newline at end of file diff --git a/src/onaptests/scenario/resources.py b/src/onaptests/scenario/resources.py new file mode 100644 index 0000000..2b0352e --- /dev/null +++ b/src/onaptests/scenario/resources.py @@ -0,0 +1,155 @@ +"""Resources module.""" + + +class K8sResource(): + """K8sResource class.""" + + def __init__(self, k8s=None): + """Init the k8s resource.""" + self.k8s = k8s + self.name = "" + self.events = [] + if self.k8s: + self.name = self.k8s.metadata.name + self.specific_k8s_init() + + def specific_k8s_init(self): + """Do the specific part for k8s resource when k8s object is present.""" + pass + + def __repr__(self): + return self.name + + def __str__(self): + return self.name + + def __eq__(self, other): + if (isinstance(other, K8sResource)): + return self.name == other.name + else: + return False + +class K8sPodParentResource(K8sResource): + """K8sPodParentResource class.""" + + def __init__(self, k8s=None): + """Init the k8s pod parent resource.""" + self.pods = [] + self.failed_pods = 0 + super().__init__(k8s=k8s) + + +class Pod(K8sResource): + """Pod class.""" + + def __init__(self, k8s=None): + """Init the pod.""" + self.containers = [] + self.init_containers = [] + self.running_containers = 0 + self.runned_init_containers = 0 + self.volumes = {} + self.restart_count = 0 + self.init_restart_count = 0 + self.init_done = True + super().__init__(k8s=k8s) + + def specific_k8s_init(self): + """Specific k8s init.""" + self.set_volumes(self.k8s.spec.volumes) + + def set_volumes(self, volumes): + """Generate the volume list.""" + for volume in volumes: + volume_name = volume.name + self.volumes[volume_name] = {} + for volume_type in volume.attribute_map: + if volume_type != "name" and getattr(volume, volume_type): + self._parse_volume_type(volume, volume_name, volume_type) + + def _parse_volume_type(self, volume, name, volume_type): + """Parse volume type informations.""" + self.volumes[name][volume_type] = {} + infos = getattr(volume, volume_type) + for details in infos.attribute_map: + self.volumes[name][volume_type][details] = getattr(infos, details) + + def ready(self): + """Calculate if Pod is ready.""" + if self.init_done and self.running_containers == len(self.containers): + return True + return False + + +class Container(): + """Container class.""" + + def __init__(self, name=""): + """Init the container.""" + self.name = name + self.status = "" + self.ready = False + self.restart_count = 0 + self.image = "" + + def set_status(self, status): + """Generate status for container.""" + if status.running: + self.status = "Running" + else: + if status.terminated: + self.status = "Terminated ({})".format( + status.terminated.reason) + else: + if status.waiting: + self.status = "Waiting ({})".format( + status.waiting.reason) + else: + self.status = "Unknown" + + +class Service(K8sPodParentResource): + """Service class.""" + + def __init__(self, k8s=None): + """Init the service.""" + self.type = "" + super().__init__(k8s=k8s) + + def specific_k8s_init(self): + """Do the specific part for service when k8s object is present.""" + self.type = self.k8s.spec.type + + +class Job(K8sPodParentResource): + """Job class.""" + + +class Deployment(K8sPodParentResource): + """Deployment class.""" + +class ReplicaSet(K8sPodParentResource): + """ReplicaSet class.""" + +class StatefulSet(K8sPodParentResource): + """StatefulSet class.""" + + +class DaemonSet(K8sPodParentResource): + """DaemonSet class.""" + + +class Pvc(K8sResource): + """Pvc class.""" + + +class ConfigMap(K8sResource): + """ConfigMap class.""" + + +class Secret(K8sResource): + """Secret class.""" + + +class Ingress(K8sResource): + """Ingress class."""
\ No newline at end of file diff --git a/src/onaptests/scenario/status.py b/src/onaptests/scenario/status.py new file mode 100644 index 0000000..c8aea74 --- /dev/null +++ b/src/onaptests/scenario/status.py @@ -0,0 +1,739 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +import json +import os +import logging +import re +import time +from natural.date import delta +from xtesting.core import testcase +from kubernetes import client, config +from kubernetes.stream import stream +from urllib3.exceptions import MaxRetryError, NewConnectionError +from jinja2 import Environment, PackageLoader, select_autoescape + +from onaptests.scenario.resources import Pod, Container, Service, Job +from onaptests.scenario.resources import Deployment, StatefulSet, DaemonSet, Pvc, ReplicaSet +from onaptests.scenario.resources import ConfigMap, Secret, Ingress + +NAMESPACE = os.getenv('K8S_NAMESPACE', 'onap') +FULL_LOGS_CONTAINERS = [ + 'dcae-bootstrap', 'dcae-cloudify-manager', 'aai-resources', + 'aai-traversal', 'aai-modelloader', 'sdnc', 'so', 'so-bpmn-infra', + 'so-openstack-adapter', 'so-sdc-controller', 'mariadb-galera', 'sdc-be', + 'sdc-fe' +] + +# patterns to be excluded from the check +WAIVER_LIST = ['integration'] + +SPECIFIC_LOGS_CONTAINERS = { + 'sdc-be': ['/var/log/onap/sdc/sdc-be/error.log'], + 'sdc-onboarding-be': ['/var/log/onap/sdc/sdc-onboarding-be/error.log'], + 'aaf-cm': [ + '/opt/app/osaaf/logs/cm/cm-service.log', + '/opt/app/osaaf/logs/cm/cm-init.log' + ], + 'aaf-fs': [ + '/opt/app/osaaf/logs/fs/fs-service.log', + '/opt/app/osaaf/logs/fs/fs-init.log' + ], + 'aaf-locate': [ + '/opt/app/osaaf/logs/locate/locate-service.log', + '/opt/app/osaaf/logs/locate/locate-init.log' + ], + 'aaf-service': [ + '/opt/app/osaaf/logs/service/authz-service.log', + '/opt/app/osaaf/logs/service/authz-init.log' + ], + 'sdc-be': [ + '/var/log/onap/sdc/sdc-be/debug.log', + '/var/log/onap/sdc/sdc-be/error.log' + ], + 'sdc-fe': [ + '/var/log/onap/sdc/sdc-fe/debug.log', + '/var/log/onap/sdc/sdc-fe/error.log' + ], + 'vid': [ + '/var/log/onap/vid/audit.log', + '/var/log/onap/vid/application.log', + '/var/log/onap/vid/debug.log', + '/var/log/onap/vid/error.log' + ], +} + +DOCKER_REPOSITORIES = [ + 'nexus3.onap.org:10001', 'docker.elastic.co', 'docker.io', 'library', + 'registry.gitlab.com', 'registry.hub.docker.com', 'k8s.gcr.io', 'gcr.io' +] +DOCKER_REPOSITORIES_NICKNAMES = { + 'nexus3.onap.org:10001': 'onap', + 'docker.elastic.co': 'elastic', + 'docker.io': 'dockerHub (docker.io)', + 'registry.hub.docker.com': 'dockerHub (registry)', + 'registry.gitlab.com': 'gitlab', + 'library': 'dockerHub (library)', + 'default': 'dockerHub', + 'k8s.gcr.io': 'google (k8s.gcr)', + 'gcr.io': 'google (gcr)' +} + +GENERIC_NAMES = { + 'postgreSQL': ['crunchydata/crunchy-postgres', 'postgres'], + 'mariadb': ['adfinissygroup/k8s-mariadb-galera-centos', 'mariadb'], + 'elasticsearch': [ + 'bitnami/elasticsearch', 'elasticsearch/elasticsearch', + 'onap/clamp-dashboard-elasticsearch' + ], + 'nginx': ['bitnami/nginx', 'nginx'], + 'cassandra': [ + 'cassandra', 'onap/music/cassandra_3_11', 'onap/music/cassandra_music', + 'onap/aaf/aaf_cass' + ], + 'zookeeper': ['google_samples/k8szk', 'onap/dmaap/zookeeper', 'zookeeper'], + 'redis': [ + 'onap/vfc/db', + 'onap/org.onap.dcaegen2.deployments.redis-cluster-container' + ], + 'consul': ['consul', 'oomk8s/consul'], + 'rabbitmq': ['ansible/awx_rabbitmq', 'rabbitmq'] +} + +MAX_LOG_BYTES = 512000 + + +class Status(testcase.TestCase): + """Retrieve status of Kubernetes resources.""" + + __logger = logging.getLogger(__name__) + + def __init__(self, kubeconfig=None, dir_result=None, **kwargs): + """Init the testcase.""" + if "case_name" not in kwargs: + kwargs["case_name"] = 'namespace_status' + super(Status, self).__init__(**kwargs) + if kubeconfig is not None: + config.load_kube_config(config_file=kubeconfig) + else: + config.load_kube_config() + self.core = client.CoreV1Api() + self.batch = client.BatchV1Api() + self.app = client.AppsV1Api() + self.networking = client.NetworkingV1Api() + if dir_result: + self.res_dir = f"{dir_result}/kubernetes-status" + else: + self.res_dir = f"{self.dir_results}/kubernetes-status" + + self.__logger.debug("namespace status init started") + self.start_time = None + self.stop_time = None + self.result = 0 + self.pods = [] + self.services = [] + self.jobs = [] + self.deployments = [] + self.replicasets =[] + self.statefulsets = [] + self.daemonsets = [] + self.pvcs = [] + self.configmaps = [] + self.secrets = [] + self.ingresses = [] + self.details = {} + + def run(self): + """Run tests.""" + self.start_time = time.time() + os.makedirs(self.res_dir, exist_ok=True) + self.__logger.debug("start test") + try: + self.k8s_pods = self.core.list_namespaced_pod(NAMESPACE).items + self.__logger.info("%4s Pods in the namespace", len(self.k8s_pods)) + + self.k8s_jobs = self.batch.list_namespaced_job(NAMESPACE).items + self.__logger.info("%4s Jobs in the namespace", len(self.k8s_jobs)) + + self.k8s_deployments = self.app.list_namespaced_deployment( + NAMESPACE).items + self.__logger.info("%4s Deployments in the namespace", + len(self.k8s_deployments)) + + self.k8s_replicasets = self.app.list_namespaced_replica_set( + NAMESPACE).items + self.__logger.info("%4s Replicasets in the namespace", + len(self.k8s_replicasets)) + + self.k8s_statefulsets = self.app.list_namespaced_stateful_set( + NAMESPACE).items + self.__logger.info("%4s StatefulSets in the namespace", + len(self.k8s_statefulsets)) + + self.k8s_daemonsets = self.app.list_namespaced_daemon_set( + NAMESPACE).items + self.__logger.info("%4s DaemonSets in the namespace", + len(self.k8s_daemonsets)) + + self.k8s_services = self.core.list_namespaced_service( + NAMESPACE).items + self.__logger.info("%4s Services in the namespace", + len(self.k8s_services)) + + self.k8s_pvcs = self.core.list_namespaced_persistent_volume_claim( + NAMESPACE).items + self.__logger.info("%4s PVCs in the namespace", len(self.pvcs)) + + self.k8s_configmaps = self.core.list_namespaced_config_map( + NAMESPACE).items + self.__logger.info("%4s ConfigMaps in the namespace", + len(self.configmaps)) + + self.k8s_secrets = self.core.list_namespaced_secret( + NAMESPACE).items + self.__logger.info("%4s Secrets in the namespace", + len(self.secrets)) + + self.k8s_ingresses = self.networking.list_namespaced_ingress( + NAMESPACE).items + self.__logger.info("%4s Ingresses in the namespace", + len(self.ingresses)) + except (ConnectionRefusedError, MaxRetryError, NewConnectionError): + self.__logger.error("namespace status test failed.") + self.__logger.error("cannot connect to Kubernetes.") + return testcase.TestCase.EX_TESTCASE_FAILED + + self.failing_statefulsets = [] + self.failing_jobs = [] + self.failing_deployments = [] + self.failing_replicasets = [] + self.failing_daemonsets = [] + self.failing_pvcs = [] + self.failing = False + + self.jinja_env = Environment(autoescape=select_autoescape(['html']), + loader=PackageLoader('onaptests.templates','status')) + self.parse_services() + jobs_pods = self.parse_jobs() + self.parse_pods(excluded_pods=jobs_pods) + self.parse_deployments() + self.parse_replicasets() + self.parse_statefulsets() + self.parse_daemonsets() + self.parse_pvcs() + self.parse_configmaps() + self.parse_secrets() + self.parse_ingresses() + self.parse_versions() + + self.jinja_env.get_template('index.html.j2').stream( + ns=self, + delta=delta).dump('{}/index.html'.format(self.res_dir)) + self.jinja_env.get_template('raw_output.txt.j2').stream( + ns=self, namespace=NAMESPACE).dump('{}/onap-k8s.log'.format( + self.res_dir)) + + self.stop_time = time.time() + if len(self.jobs) > 0: + self.details['jobs'] = { + 'number': len(self.jobs), + 'number_failing': len(self.failing_jobs), + 'failing': self.map_by_name(self.failing_jobs) + } + if len(self.deployments) > 0: + self.details['deployments'] = { + 'number': len(self.deployments), + 'number_failing': len(self.failing_deployments), + 'failing': self.map_by_name(self.failing_deployments) + } + if len(self.replicasets) > 0: + self.details['replicasets'] = { + 'number': len(self.replicasets), + 'number_failing': len(self.failing_replicasets), + 'failing': self.map_by_name(self.failing_replicasets) + } + if len(self.statefulsets) > 0: + self.details['statefulsets'] = { + 'number': len(self.statefulsets), + 'number_failing': len(self.failing_statefulsets), + 'failing': self.map_by_name(self.failing_statefulsets) + } + if len(self.daemonsets) > 0: + self.details['daemonsets'] = { + 'number': len(self.daemonsets), + 'number_failing': len(self.failing_daemonsets), + 'failing': self.map_by_name(self.failing_daemonsets) + } + if len(self.pvcs) > 0: + self.details['pvcs'] = { + 'number': len(self.pvcs), + 'number_failing': len(self.failing_pvcs), + 'failing': self.map_by_name(self.failing_pvcs) + } + if self.failing: + self.__logger.error("namespace status test failed.") + self.__logger.error("number of errored Jobs: %s", + len(self.failing_jobs)) + self.__logger.error("number of errored Deployments: %s", + len(self.failing_deployments)) + self.__logger.error("number of errored Replicasets: %s", + len(self.failing_replicasets)) + self.__logger.error("number of errored StatefulSets: %s", + len(self.failing_statefulsets)) + self.__logger.error("number of errored DaemonSets: %s", + len(self.failing_daemonsets)) + self.__logger.error("number of errored PVCs: %s", + len(self.failing_pvcs)) + return testcase.TestCase.EX_TESTCASE_FAILED + + self.result = 100 + return testcase.TestCase.EX_OK + + def parse_pods(self, excluded_pods=None): + """Parse the pods status.""" + self.__logger.info("%4s pods to parse", len(self.k8s_pods)) + for k8s in self.k8s_pods: + pod = Pod(k8s=k8s) + + if excluded_pods and pod in excluded_pods: + continue + + if k8s.status.init_container_statuses: + for k8s_container in k8s.status.init_container_statuses: + pod.runned_init_containers += self.parse_container( + pod, k8s_container, init=True) + if k8s.status.container_statuses: + for k8s_container in k8s.status.container_statuses: + pod.running_containers += self.parse_container( + pod, k8s_container) + pod.events = self.core.list_namespaced_event( + NAMESPACE, + field_selector="involvedObject.name={}".format(pod.name)).items + self.jinja_env.get_template('pod.html.j2').stream(pod=pod).dump( + '{}/pod-{}.html'.format(self.res_dir, pod.name)) + if any(waiver_elt in pod.name for waiver_elt in WAIVER_LIST): + self.__logger.warn("Waiver pattern found in pod, exclude %s", pod.name) + else: + self.pods.append(pod) + + def parse_container(self, pod, k8s_container, init=False): + """Get the logs of a container.""" + logs = "" + old_logs = "" + prefix = "" + containers_list = pod.containers + container = Container(name=k8s_container.name) + container.restart_count = k8s_container.restart_count + container.set_status(k8s_container.state) + container.ready = k8s_container.ready + container.image = k8s_container.image + if init: + prefix = "init " + containers_list = pod.init_containers + if container.restart_count > pod.init_restart_count: + pod.init_restart_count = container.restart_count + if not container.ready: + pod.init_done = False + else: + if container.restart_count > pod.restart_count: + pod.restart_count = container.restart_count + + try: + log_files = {} + logs = "" + try: + logs = self.core.read_namespaced_pod_log( + pod.name, + NAMESPACE, + container=container.name, + limit_bytes=MAX_LOG_BYTES, + ) + except UnicodeDecodeError: + logs= "{0} has an unicode decode error...".format(pod.name) + self.__logger.error( + "{0} has an unicode decode error in the logs...", pod.name, + ) + with open( + "{}/pod-{}-{}.log".format(self.res_dir, + pod.name, container.name), + 'w') as log_result: + log_result.write(logs) + if (not container.ready) and container.restart_count > 0: + old_logs = self.core.read_namespaced_pod_log( + pod.name, + NAMESPACE, + container=container.name, + previous=True) + with open( + "{}/pod-{}-{}.old.log".format(self.res_dir, + pod.name, + container.name), + 'w') as log_result: + log_result.write(old_logs) + if (container.name in FULL_LOGS_CONTAINERS): + logs = self.core.read_namespaced_pod_log( + pod.name, NAMESPACE, container=container.name) + with open( + "{}/pod-{}-{}.log".format(self.res_dir, + pod.name, container.name), + 'w') as log_result: + log_result.write(logs) + if (container.name in SPECIFIC_LOGS_CONTAINERS): + for log_file in SPECIFIC_LOGS_CONTAINERS[container.name]: + exec_command = ['/bin/sh', '-c', "cat {}".format(log_file)] + log_files[log_file] = stream( + self.core.connect_get_namespaced_pod_exec, + pod.name, + NAMESPACE, + container=container.name, + command=exec_command, + stderr=True, + stdin=False, + stdout=True, + tty=False) + log_file_slug = log_file.split('.')[0].split('/')[-1] + with open( + "{}/pod-{}-{}-{}.log".format( + self.res_dir, pod.name, + container.name, log_file_slug), + 'w') as log_result: + log_result.write(log_files[log_file]) + except client.rest.ApiException as exc: + self.__logger.warning("%scontainer %s of pod %s has an exception: %s", + prefix, container.name, pod.name, exc.reason) + self.jinja_env.get_template('container_log.html.j2').stream( + container=container, + pod_name=pod.name, + logs=logs, + old_logs=old_logs, + log_files=log_files).dump('{}/pod-{}-{}-logs.html'.format( + self.res_dir, pod.name, container.name)) + if any(waiver_elt in container.name for waiver_elt in WAIVER_LIST): + self.__logger.warn( + "Waiver pattern found in container, exclude %s", container.name) + else: + containers_list.append(container) + if k8s_container.ready: + return 1 + return 0 + + def parse_services(self): + """Parse the services.""" + self.__logger.info("%4s services to parse", len(self.k8s_services)) + for k8s in self.k8s_services: + service = Service(k8s=k8s) + + (service.pods, + service.failed_pods) = self._find_child_pods(k8s.spec.selector) + + self.jinja_env.get_template('service.html.j2').stream( + service=service).dump('{}/service-{}.html'.format( + self.res_dir, service.name)) + self.services.append(service) + + def parse_jobs(self): + """Parse the jobs. + Return a list of Pods that were created to perform jobs. + """ + self.__logger.info("%4s jobs to parse", len(self.k8s_jobs)) + jobs_pods = [] + for i in range(len(self.k8s_jobs)): + k8s = self.k8s_jobs[i] + job = Job(k8s=k8s) + job_pods = [] + + if k8s.spec.selector and k8s.spec.selector.match_labels: + (job.pods, job.failed_pods) = self._find_child_pods( + k8s.spec.selector.match_labels) + job_pods += job.pods + field_selector = "involvedObject.name={}".format(job.name) + field_selector += ",involvedObject.kind=Job" + job.events = self.core.list_namespaced_event( + NAMESPACE, + field_selector=field_selector).items + + self.jinja_env.get_template('job.html.j2').stream(job=job).dump( + '{}/job-{}.html'.format(self.res_dir, job.name)) + + # timemout job + if not k8s.status.completion_time: + self.__logger.warning("a Job is in error: {}".format(job.name)) + if any( + waiver_elt not in job.name for waiver_elt in WAIVER_LIST): + self.failing_jobs.append(job) + self.failing = True + # completed job + if any(waiver_elt not in job.name for waiver_elt in WAIVER_LIST): + self.jobs.append(job) + jobs_pods += job_pods + return jobs_pods + + def parse_deployments(self): + """Parse the deployments.""" + self.__logger.info("%4s deployments to parse", + len(self.k8s_deployments)) + for i in range(len(self.k8s_deployments)): + k8s = self.k8s_deployments[i] + deployment = Deployment(k8s=k8s) + + if k8s.spec.selector and k8s.spec.selector.match_labels: + (deployment.pods, + deployment.failed_pods) = self._find_child_pods( + k8s.spec.selector.match_labels) + field_selector = "involvedObject.name={}".format(deployment.name) + field_selector += ",involvedObject.kind=Deployment" + deployment.events = self.core.list_namespaced_event( + NAMESPACE, + field_selector=field_selector).items + + self.jinja_env.get_template('deployment.html.j2').stream( + deployment=deployment).dump('{}/deployment-{}.html'.format( + self.res_dir, deployment.name)) + + if k8s.status.unavailable_replicas: + self.__logger.warning("a Deployment is in error: {}".format(deployment.name)) + self.failing_deployments.append(deployment) + self.failing = True + + self.deployments.append(deployment) + + def parse_replicasets(self): + """Parse the replicasets.""" + self.__logger.info("%4s replicasets to parse", + len(self.k8s_replicasets)) + for i in range(len(self.k8s_replicasets)): + k8s = self.k8s_replicasets[i] + replicaset = ReplicaSet(k8s=k8s) + + if k8s.spec.selector and k8s.spec.selector.match_labels: + (replicaset.pods, + replicaset.failed_pods) = self._find_child_pods( + k8s.spec.selector.match_labels) + field_selector = "involvedObject.name={}".format(replicaset.name) + field_selector += ",involvedObject.kind=ReplicaSet" + replicaset.events = self.core.list_namespaced_event( + NAMESPACE, + field_selector=field_selector).items + + self.jinja_env.get_template('replicaset.html.j2').stream( + replicaset=replicaset).dump('{}/replicaset-{}.html'.format( + self.res_dir, replicaset.name)) + + if (not k8s.status.ready_replicas + or (k8s.status.ready_replicas < k8s.status.replicas)): + self.__logger.warning("a ReplicaSet is in error: {}".format(replicaset.name)) + self.failing_replicasets.append(replicaset) + self.failing = True + + self.replicasets.append(replicaset) + + def parse_statefulsets(self): + """Parse the statefulsets.""" + self.__logger.info("%4s statefulsets to parse", + len(self.k8s_statefulsets)) + for i in range(len(self.k8s_statefulsets)): + k8s = self.k8s_statefulsets[i] + statefulset = StatefulSet(k8s=k8s) + + if k8s.spec.selector and k8s.spec.selector.match_labels: + (statefulset.pods, + statefulset.failed_pods) = self._find_child_pods( + k8s.spec.selector.match_labels) + field_selector = "involvedObject.name={}".format(statefulset.name) + field_selector += ",involvedObject.kind=StatefulSet" + statefulset.events = self.core.list_namespaced_event( + NAMESPACE, + field_selector=field_selector).items + + self.jinja_env.get_template('statefulset.html.j2').stream( + statefulset=statefulset).dump('{}/statefulset-{}.html'.format( + self.res_dir, statefulset.name)) + + if ((not k8s.status.ready_replicas) + or (k8s.status.ready_replicas < k8s.status.replicas)): + self.__logger.warning("a StatefulSet is in error: {}".format(statefulset.name)) + self.failing_statefulsets.append(statefulset) + self.failing = True + + self.statefulsets.append(statefulset) + + def parse_daemonsets(self): + """Parse the daemonsets.""" + self.__logger.info("%4s daemonsets to parse", len(self.k8s_daemonsets)) + for i in range(len(self.k8s_daemonsets)): + k8s = self.k8s_daemonsets[i] + daemonset = DaemonSet(k8s=k8s) + + if k8s.spec.selector and k8s.spec.selector.match_labels: + (daemonset.pods, + daemonset.failed_pods) = self._find_child_pods( + k8s.spec.selector.match_labels) + field_selector = "involvedObject.name={}".format(daemonset.name) + field_selector += ",involvedObject.kind=DaemonSet" + daemonset.events = self.core.list_namespaced_event( + NAMESPACE, + field_selector=field_selector).items + + self.jinja_env.get_template('daemonset.html.j2').stream( + daemonset=daemonset).dump('{}/daemonset-{}.html'.format( + self.res_dir, daemonset.name)) + + if (k8s.status.number_ready < k8s.status.desired_number_scheduled): + self.__logger.warning("a DaemonSet is in error: {}".format(daemonset.name)) + self.failing_daemonsets.append(daemonset) + self.failing = True + + self.daemonsets.append(daemonset) + + def parse_pvcs(self): + """Parse the persistent volume claims.""" + self.__logger.info("%4s pvcs to parse", len(self.k8s_pvcs)) + for k8s in self.k8s_pvcs: + pvc = Pvc(k8s=k8s) + field_selector = f"involvedObject.name={pvc.name},involvedObject.kind=PersistentVolumeClaim" + pvc.events = self.core.list_namespaced_event( + NAMESPACE, + field_selector=field_selector).items + + if k8s.status.phase != "Bound": + self.__logger.warning("a PVC is in error: {}".format(pvc.name)) + self.failing_pvcs.append(pvc) + self.failing = True + + self.pvcs.append(pvc) + + def parse_configmaps(self): + """Parse the config maps.""" + self.__logger.info("%4s config maps to parse", + len(self.k8s_configmaps)) + for k8s in self.k8s_configmaps: + configmap = ConfigMap(k8s=k8s) + self.configmaps.append(configmap) + + def parse_secrets(self): + """Parse the secrets.""" + self.__logger.info("%4s secrets to parse", len(self.k8s_secrets)) + for k8s in self.k8s_secrets: + secret = Secret(k8s=k8s) + self.secrets.append(secret) + + def parse_ingresses(self): + """Parse the ingresses.""" + self.__logger.info("%4s ingresses to parse", len(self.k8s_ingresses)) + for k8s in self.k8s_secrets: + ingress = Ingress(k8s=k8s) + self.ingresses.append(ingress) + + def parse_versions(self): + """Parse the versions of the pods.""" + self.__logger.info("%4s pods to parse", len(self.k8s_pods)) + pod_versions = [] + containers = {} + for pod in self.k8s_pods: + pod_component = pod.metadata.name + if 'app' in pod.metadata.labels: + pod_component = pod.metadata.labels['app'] + else: + if 'app.kubernetes.io/name' in pod.metadata.labels: + pod_component = pod.metadata.labels[ + 'app.kubernetes.io/name'] + else: + self.__logger.error("pod %s has no 'app' or 'app.kubernetes.io/name' in metadata: %s", pod_component, pod.metadata.labels) + + # looks for docker version + for container in pod.spec.containers: + pod_version = {} + pod_container_version = container.image.rsplit(":", 1) + pod_container_image = pod_container_version[0] + pod_container_tag = "latest" + if len(pod_container_version) > 1: + pod_container_tag = pod_container_version[1] + + pod_version.update({ + 'container': container.name, + 'component': pod_component, + 'image': pod_container_image, + 'version': pod_container_tag + }) + pod_versions.append(pod_version) + + search_rule = "^(?P<source>[^/]*)/*(?P<container>[^:]*):*(?P<version>.*)$" + search = re.search(search_rule, container.image) + name = "{}/{}".format(search.group('source'), + search.group('container')) + version = search.group('version') + if name[-1] == '/': + name = name[0:-1] + source = "default" + if search.group('source') in DOCKER_REPOSITORIES: + source = search.group('source') + name = search.group('container') + container_search_rule = "^library/(?P<real_container>[^:]*)$" + container_search = re.search(container_search_rule, name) + if container_search: + name = container_search.group('real_container') + for common_component in GENERIC_NAMES.keys(): + if name in GENERIC_NAMES[common_component]: + version = "{}:{}".format(name, version) + name = common_component + break + + repository = DOCKER_REPOSITORIES_NICKNAMES[source] + if name in containers: + if version in containers[name]['versions']: + if not (pod_component in containers[name]['versions'] + [version]['components']): + containers[name]['versions'][version][ + 'components'].append(pod_component) + containers[name]['number_components'] += 1 + if not (repository in containers[name]['versions'] + [version]['repositories']): + containers[name]['versions'][version][ + 'repositories'].append(repository) + else: + containers[name]['versions'][version] = { + 'repositories': [repository], + 'components': [pod_component] + } + containers[name]['number_components'] += 1 + else: + containers[name] = { + 'versions': { + version: { + 'repositories': [repository], + 'components': [pod_component] + } + }, + 'number_components': 1 + } + + self.jinja_env.get_template('version.html.j2').stream( + pod_versions=pod_versions).dump('{}/versions.html'.format( + self.res_dir)) + self.jinja_env.get_template('container_versions.html.j2').stream( + containers=containers).dump('{}/container_versions.html'.format( + self.res_dir)) + # create a json file for version tracking + with open(self.res_dir + "/onap_versions.json", "w") as write_file: + json.dump(pod_versions, write_file) + + def _find_child_pods(self, selector): + pods_list = [] + failed_pods = 0 + if selector: + raw_selector = '' + for key, value in selector.items(): + raw_selector += key + '=' + value + ',' + raw_selector = raw_selector[:-1] + pods = self.core.list_namespaced_pod( + NAMESPACE, label_selector=raw_selector).items + for pod in pods: + for known_pod in self.pods: + if known_pod.name == pod.metadata.name: + pods_list.append(known_pod) + if not known_pod.ready(): + failed_pods += 1 + return (pods_list, failed_pods) + + def map_by_name(self, resources): + return list(map(lambda resource: resource.name, resources)) diff --git a/src/onaptests/templates/status/base.html.j2 b/src/onaptests/templates/status/base.html.j2 new file mode 100644 index 0000000..41e55de --- /dev/null +++ b/src/onaptests/templates/status/base.html.j2 @@ -0,0 +1,261 @@ +{% macro color(failing, total) %} +{% if failing == 0 %} +is-success +{% else %} +{% if (failing / total) <= 0.1 %} +is-warning +{% else %} +is-danger +{% endif %} +{% endif %} +{% endmacro %} + +{% macro percentage(failing, total) %} +{{ ((total - failing) / total) | round }} +{% endmacro %} + +{% macro statistic(resource_name, failing, total) %} +{% set success = total - failing %} +<div class="level-item has-text-centered"> + <div> + <p class="heading">{{ resource_name | capitalize }}</p> + <p class="title">{{ success }}/{{ total }}</p> + <progress class="progress {{ color(failing, total) }}" value="{{ success }}" max="{{ total }}">{{ percentage(failing, total) }}</progress> + </div> + </div> +{% endmacro %} + +{% macro pods_table(pods) %} +<div id="pods" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Ready</th> + <th>Status</th> + <th>Reason</th> + <th>Restarts</th> + </tr> + </thead> + <tbody> + {% for pod in pods %} + <tr> + <td><a href="./pod-{{ pod.name }}.html" title="{{ pod.name }}">{{ pod.k8s.metadata.name }}</a></td> + {% if pod.init_done %} + <td>{{ pod.running_containers }}/{{ (pod.containers | length) }}</td> + {% else %} + <td>Init:{{ pod.runned_init_containers }}/{{ (pod.init_containers | length) }}</td> + {% endif %} + <td>{{ pod.k8s.status.phase }}</td> + <td>{{ pod.k8s.status.reason }}</td> + {% if pod.init_done %} + <td>{{ pod.restart_count }}</td> + {% else %} + <td>{{ pod.init_restart_count }}</td> + {% endif %} + </tr> + {% endfor %} + </tbody> + </table> +</div> +{% endmacro %} + +{% macro key_value_description_list(title, dict) %} +<dt><strong>{{ title | capitalize }}:</strong></dt> +<dd> + {% if dict %} + {% for key, value in dict.items() %} + {% if loop.first %} + <dl> + {% endif %} + <dt>{{ key }}:</dt> + <dd>{{ value }}</dd> + {% if loop.last %} + </dl> + {% endif %} + {% endfor %} + {% endif %} +</dd> +{% endmacro %} + +{% macro description(k8s) %} +<div class="container"> + <h1 class="title is-1">Description</h1> + <div class="content"> + <dl> + {% if k8s.spec.type %} + <dt><strong>Type:</strong></dt> + <dd>{{ k8s.spec.type }}</dd> + {% if (k8s.spec.type | lower) == "clusterip" %} + <dt><strong>Headless:</strong></dt> + <dd>{% if (k8s.spec.cluster_ip | lower) == "none" %}Yes{% else %}No{% endif %}</dd> + {% endif %} + {% endif %} + {{ key_value_description_list('Labels', k8s.metadata.labels) | indent(width=6) }} + {{ key_value_description_list('Annotations', k8s.metadata.annotations) | indent(width=6) }} + {% if k8s.spec.selector %} + {% if k8s.spec.selector.match_labels %} + {{ key_value_description_list('Selector', k8s.spec.selector.match_labels) | indent(width=6) }} + {% else %} + {{ key_value_description_list('Selector', k8s.spec.selector) | indent(width=6) }} + {% endif %} + {% endif %} + {% if k8s.phase %} + <dt><strong>Status:</strong></dt> + <dd>{{ k8s.phase }}</dd> + {% endif %} + {% if k8s.metadata.owner_references %} + <dt><strong>Controlled By:</strong></dt> + <dd>{{ k8s.metadata.owner_references[0].kind }}/{{ k8s.metadata.owner_references[0].name }}</dd> + {% endif %} + </dl> + </div> +</div> +{% endmacro %} + +{% macro pods_container(pods, parent, has_title=True) %} +<div class="container"> + {% if has_title %} + <h1 class="title is-1">Pods</h1> + {% endif %} + {% if (pods | length) > 0 %} + {{ pods_table(pods) | indent(width=2) }} + {% else %} + <div class="notification is-warning">{{ parent }} has no pods!</div> + {% endif %} +</div> +{% endmacro %} + +{% macro two_level_breadcrumb(title, name) %} +<section class="section"> + <div class="container"> + <nav class="breadcrumb" aria-label="breadcrumbs"> + <ul> + <li><a href="./index.html">Summary</a></li> + <li class="is-active"><a href="#" aria-current="page">{{ title | capitalize }} {{ name }}</a></li> + </ul> + </nav> + </div> +</section> +{% endmacro %} + +{% macro pod_parent_summary(title, name, failed_pods, pods) %} +{{ summary(title, name, [{'title': 'Pod', 'failing': failed_pods, 'total': (pods | length)}]) }} +{% endmacro %} + +{% macro number_ok(number, none_value, total=None) %} +{% if number %} +{% if total and number < total %} +<span class="tag is-warning">{{ number }}</span> +{% else %} +{{ number }} +{% endif %} +{% else %} +<span class="tag is-warning">{{ none_value }}</span> +{% endif %} +{% endmacro %} + +{% macro summary(title, name, statistics) %} +<section class="hero is-light"> + <div class="hero-body"> + <div class="container"> + <h1 class="title is-1"> + {{ title | capitalize }} {{ name }} Summary + </h1> + <nav class="level"> + {% for stat in statistics %} + {% if stat.total > 0 %} + {{ statistic(stat.title, stat.failing, stat.total) | indent(width=8) }} + {% endif %} + {% endfor %} + </nav> + </div> + </div> +</section> +{% endmacro %} + +{% macro events(events) %} +{% if events %} +<div class="container"> + <h1 class="title is-1">Events</h1> + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Type</th> + <th>Count</th> + <th>Reason</th> + <th>Message</th> + </tr> + </thead> + <tbody> + {% for event in events %} + <tr> + <td><span class="tag {% if (event.type | lower) == 'normal' %}is-info{% else %}is-warning{% endif %}">{{ event.type }}</span></td> + <td>{{ event.count }}</td> + <td>{{ event.reason }}</td> + <td>{{ event.message }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> +</div> +{% endif %} +{% endmacro %} + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Tests results - {% block title %}{% endblock %}</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css"> + <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script> + {% block more_head %}{% endblock %} + </head> + <body> + <nav class="navbar" role="navigation" aria-label="main navigation"> + <div class="navbar-brand"> + <a class="navbar-item" href="https://www.onap.org"> + <img src="https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png"> + </a> + + <a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample"> + <span aria-hidden="true"></span> + <span aria-hidden="true"></span> + <span aria-hidden="true"></span> + </a> + </div> + + <div id="navbarBasicExample" class="navbar-menu"> + <div class="navbar-start" href="./index.html"> + <a class="navbar-item"> + Summary + </a> + </div> + </div> + </nav> + + {% block content %}{% endblock %} + + <footer class="footer"> + <div class="container"> + <div class="columns"> + <div class="column"> + <p class="has-text-grey-light"> + <a href="https://bulma.io/made-with-bulma/"> + <img src="https://bulma.io/images/made-with-bulma.png" alt="Made with Bulma" width="128" height="24"> + </a> + </div> + <div class="column"> + <a class="has-text-grey" href="https://gitlab.com/Orange-OpenSource/lfn/tools/kubernetes-status" style="border-bottom: 1px solid currentColor;"> + Improve this page on Gitlab + </a> + </p> + </div> + </div> + </div> + </footer> + </body> +</html> diff --git a/src/onaptests/templates/status/container_log.html.j2 b/src/onaptests/templates/status/container_log.html.j2 new file mode 100644 index 0000000..454dee7 --- /dev/null +++ b/src/onaptests/templates/status/container_log.html.j2 @@ -0,0 +1,104 @@ +{% extends "base.html.j2" %} +{% block title %}Container {{ container.name }} from pod {{ pod_name }} logs{% endblock %} +{% block content %} + <section class="section"> + <div class="container"> + <nav class="breadcrumb" aria-label="breadcrumbs"> + <ul> + <li><a href="./index.html">Summary</a></li> + <li><a href="./pod-{{ pod_name }}.html" title="{{ pod_name }}">Pod {{pod_name }}</a></li> + <li class="is-active"><a href="#" aria-current="page">Container {{ container.name }} logs</a></li> + </ul> + </nav> + </div> + </section> + <section class="section"> + <div class="container"> + <h1 class="title is-1"> + Results + </h1> + <p class="subtitle"> + By type + </p> + <div class="tabs is-centered"> + <ul> + <li id="logs_toggle"><a onclick="toggleVisibility('logs')">Logs</a></li> + {% if old_logs %} + <li id="old_logs_toggle"><a onclick="toggleVisibility('old_logs')">Previous Logs</a></li> + {% endif %} + {% if log_files %} + {% for file in log_files %} + <li id="{{ file.split('.')[0].split('/')[-1] }}_toggle"><a onclick="toggleVisibility('{{ file.split('.')[0].split('/')[-1] }}')">{{ file }}</a></li> + {% endfor %} + {% endif %} + </ul> + </div> + <section class="section"> + <div id="logs" class="container"> + <div class="columns is-mobile is-centered"> + <div class="column is-half"> + <a class="button is-link is-fullwidth" href="./pod-{{ pod_name }}-{{ container.name }}.log">raw version</a> + </div> + </div> + <pre> + <code>{{ logs }}</code> + <pre> + </div> + {% if old_logs %} + <div id="old_logs" class="container"> + <div class="columns is-mobile is-centered"> + <div class="column is-half"> + <a class="button is-link is-fullwidth" href="./pod-{{ pod_name }}-{{ container.name }}.old.log">raw version</a> + </div> + </div> + <pre> + <code>{{ old_logs }}</code> + <pre> + </div> + {% endif %} + {% if log_files %} + {% for file in log_files %} + <div id="{{ file.split('.')[0].split('/')[-1] }}" class="container"> + <div class="columns is-mobile is-centered"> + <div class="column is-half"> + <a class="button is-link is-fullwidth" href="./pod-{{ pod_name }}-{{ container.name }}-{{ file.split('.')[0].split('/')[-1] }}.log">raw version</a> + </div> + </div> + <pre> + <code>{{ log_files[file] }}</code> + <pre> + </div> + {% endfor %} + {% endif %} + </section> +{% endblock %} + +{% block more_head %} + <script language="JavaScript"> + function toggleVisibility(id) { + document.getElementById('logs').style.display = 'none'; + {% if old_logs %} + document.getElementById('old_logs').style.display = 'none'; + {% endif %} + {% if log_files %} + {% for file in log_files %} + document.getElementById('{{ file.split('.')[0].split('/')[-1] }}').style.display = 'none'; + {% endfor %} + {% endif %} + document.getElementById(id).style.display = 'inline'; + document.getElementById('logs_toggle').classList.remove("is-active"); + {% if old_logs %} + document.getElementById('old_logs_toggle').classList.remove("is-active"); + {% endif %} + {% if log_files %} + {% for file in log_files %} + document.getElementById('{{ file.split('.')[0].split('/')[-1] }}_toggle').classList.remove("is-active"); + {% endfor %} + {% endif %} + document.getElementById(id + '_toggle').classList.add("is-active"); + } + document.addEventListener('readystatechange', () => { + if (document.readyState == 'complete') toggleVisibility('logs'); + }); + </script> +{% endblock %} diff --git a/src/onaptests/templates/status/container_versions.html.j2 b/src/onaptests/templates/status/container_versions.html.j2 new file mode 100644 index 0000000..d3d283d --- /dev/null +++ b/src/onaptests/templates/status/container_versions.html.j2 @@ -0,0 +1,38 @@ + +{% extends "base.html.j2" %} +{% block title %}ONAP Docker Versions{% endblock %} + +{% block content %} +<section class="section"> + <div class=container> + <h1 class="title is-1">ONAP Docker versions</h1> + <table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth"> + <thead> + <tr> + <th>Container</th> + <th>Version</th> + <th>Repositories</th> + <th>Components using it</th> + </tr> + </thead> + <tbody> + {% for container in containers.keys()|sort %} + <tr> + <td class="container" rowspan="{{ containers[container]['number_components'] }}">{{ container }}</td> + {% for version in containers[container]['versions'].keys()|sort %} + <td class="version" rowspan="{{ containers[container]['versions'][version]['components']|length }}">{{ version }}</td> + <td class="repositories" rowspan="{{ containers[container]['versions'][version]['components']|length }}">{% for repository in containers[container]['versions'][version]['repositories'] %}{{ repository }}{% if not loop.last %}, {% endif %}{% endfor %}</td> + {% for component in containers[container]['versions'][version]['components']|sort %} + {% if not loop.first %} + <tr> + {% endif %} + <td class="component">{{ component }}</td> + </tr> + {% endfor %} + {% endfor %} + {% endfor %} + </tbody> + </table> + </div> +</section> +{% endblock %} diff --git a/src/onaptests/templates/status/daemonset.html.j2 b/src/onaptests/templates/status/daemonset.html.j2 new file mode 100644 index 0000000..2d76280 --- /dev/null +++ b/src/onaptests/templates/status/daemonset.html.j2 @@ -0,0 +1,15 @@ +{% extends "base.html.j2" %} +{% block title %}DaemonSet {{ daemonset.name }}{% endblock %} +{% block content %} + {{ two_level_breadcrumb('DaemonSet', daemonset.name) | indent(width=4) }} + + {{ pod_parent_summary('DaemonSet', daemonset.name, daemonset.failed_pods, daemonset.pods) }} + + <section class="section"> + {{ description(daemonset.k8s) | indent(width=6) }} + + {{ pods_container(daemonset.pods, "DaemonSet") | indent(width=6) }} + + {{ events(daemonset.events) }} + </section> +{% endblock %} diff --git a/src/onaptests/templates/status/deployment.html.j2 b/src/onaptests/templates/status/deployment.html.j2 new file mode 100644 index 0000000..53a0bbb --- /dev/null +++ b/src/onaptests/templates/status/deployment.html.j2 @@ -0,0 +1,15 @@ +{% extends "base.html.j2" %} +{% block title %}Deployment {{ deployment.name }}{% endblock %} +{% block content %} + {{ two_level_breadcrumb('Deployment', deployment.name) | indent(width=4) }} + + {{ pod_parent_summary('Deployment', deployment.name, deployment.failed_pods, deployment.pods) }} + + <section class="section"> + {{ description(deployment.k8s) | indent(width=6) }} + + {{ pods_container(deployment.pods, "Deployment") | indent(width=6) }} + + {{ events(deployment.events) }} + </section> +{% endblock %} diff --git a/src/onaptests/templates/status/index.html.j2 b/src/onaptests/templates/status/index.html.j2 new file mode 100644 index 0000000..fe49abf --- /dev/null +++ b/src/onaptests/templates/status/index.html.j2 @@ -0,0 +1,376 @@ +{% extends "base.html.j2" %} +{% block title %}Summary{% endblock %} +{% block content %} + <section class="section"> + <div class="container"> + <nav class="breadcrumb" aria-label="breadcrumbs"> + <ul> + <li class="is-active"><a href="./index.html" aria-current="page">Summary</a></li> + <li><a href="./versions.html" aria-current="page">Versions</a></li> + </ul> + </nav> + </div> + </section> + + {{ summary('Results', "", [ + { 'title': 'Jobs', 'failing': (ns.failing_jobs | length), 'total': (ns.jobs | length)}, + { 'title': 'Deployments', 'failing': (ns.failing_deployments | length), 'total': (ns.deployments | length)}, + { 'title': 'Replicasets', 'failing': (ns.failing_replicasets | length), 'total': (ns.replicasets | length)}, + { 'title': 'StatefulSets', 'failing': (ns.failing_statefulsets | length), 'total': (ns.statefulsets | length)}, + { 'title': 'DaemonSets', 'failing': (ns.failing_daemonsets | length), 'total': (ns.daemonsets | length)}, + { 'title': 'Persistent Volume Claims', 'failing': (ns.failing_pvcs | length), 'total': (ns.pvcs | length)}]) + }} + + <section class="section"> + <div class="container"> + <h1 class="title is-1"> + Results + </h1> + <p class="subtitle"> + By type + </p> + <div class="tabs is-centered"> + <ul> + <li id="pods_toggle"><a onclick="toggleVisibility('pods')">Pods</a></li> + <li id="services_toggle"><a onclick="toggleVisibility('services')">Services</a></li> + {% if (ns.jobs | length) > 0 %} + <li id="jobs_toggle"><a onclick="toggleVisibility('jobs')">Jobs</a></li> + {% endif %} + {% if (ns.deployments | length) > 0 %} + <li id="deployments_toggle"><a onclick="toggleVisibility('deployments')">Deployments</a></li> + {% endif %} + {% if (ns.replicasets | length) > 0 %} + <li id="replicasets_toggle"><a onclick="toggleVisibility('replicasets')">Replicasets</a></li> + {% endif %} + {% if (ns.statefulsets | length) > 0 %} + <li id="statefulsets_toggle"><a onclick="toggleVisibility('statefulsets')">StatefulSets</a></li> + {% endif %} + {% if (ns.daemonsets | length) > 0 %} + <li id="daemonsets_toggle"><a onclick="toggleVisibility('daemonsets')">DaemonSets</a></li> + {% endif %} + {% if (ns.pvcs | length) > 0 %} + <li id="pvcs_toggle"><a onclick="toggleVisibility('pvcs')">Persistent Volume Claims</a></li> + {% endif %} + {% if (ns.configmaps | length) > 0 %} + <li id="configmaps_toggle"><a onclick="toggleVisibility('configmaps')">Config Maps</a></li> + {% endif %} + {% if (ns.secrets | length) > 0 %} + <li id="secrets_toggle"><a onclick="toggleVisibility('secrets')">Secrets</a></li> + {% endif %} + {% if (ns.ingresses | length) > 0 %} + <li id="ingresses_toggle"><a onclick="toggleVisibility('ingresses')">Ingresses</a></li> + {% endif %} + </ul> + </div> + + <!-- Pods table --> + {{ pods_container(ns.pods, "Namespace", has_title=False) | indent(width=6) }} + + <!-- Services table --> + <div id="services" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Ports</th> + <th>Pods selected</th> + </tr> + </thead> + <tbody> + {% for service in ns.services %} + <tr> + <td><a href="./service-{{ service.name }}.html" title="{{ service.name }}">{{ service.name }}</a></td> + <td>{{ service.type }}</td> + <td> + {% if service.k8s.spec.ports %} + {% for port in service.k8s.spec.ports %} + {{ port.port }}{% if port.node_port %}:{{ port.node_port }}{% endif %}/{{ port.protocol }}{% if not loop.last %},{% endif %} + {% endfor %} + {% else %} + <span class="tag is-warning">No Ports!</span> + {% endif %} + </td> + <td>{% if (service.pods | length) > 0 %}{{ service.pods | length }}{% else %}<span class="tag is-warning">0</span>{% endif %}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + + {% if (ns.jobs | length) > 0 %} + <!-- Jobs table --> + <div id="jobs" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Completions</th> + <th>Duration</th> + </tr> + </thead> + <tbody> + {% for job in ns.jobs %} + <tr> + <td><a href="./job-{{ job.name }}.html" title="{{ job.name }}">{{ job.name }}</a></td> + <td>{% if job.k8s.status.succeeded %}{{ job.k8s.status.succeeded }}{% else %}0{% endif %}/{{ job.k8s.spec.completions }}</td> + <td>{% if job.k8s.status.completion_time %}{{ delta(job.k8s.status.completion_time, job.k8s.status.start_time)[0] }}{% else %}<span class="tag is-warning">N/A</span>{% endif %}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + {% if (ns.deployments | length) > 0 %} + <!-- Deployments table --> + <div id="deployments" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Ready</th> + <th>Up to Date</th> + <th>Available</th> + </tr> + </thead> + <tbody> + {% for deployment in ns.deployments %} + <tr> + <td><a href="./deployment-{{ deployment.name }}.html" title="{{ deployment.name }}">{{ deployment.name }}</a></td> + <td>{% if deployment.k8s.status.ready_replicas %}{{ deployment.k8s.status.ready_replicas }}{% else %}0{% endif %}/{{ deployment.k8s.spec.replicas }}</td> + <td>{{ number_ok(deployment.k8s.status.updated_replicas, '0', total=deployment.k8s.spec.replicas) }}</td> + <td>{{ number_ok(deployment.k8s.status.available_replicas, '0', total=deployment.k8s.spec.replicas) }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + {% if (ns.replicasets | length) > 0 %} + <!-- ReplicaSets table --> + <div id="replicasets" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Ready</th> + <th>Available</th> + </tr> + </thead> + <tbody> + {% for rs in ns.replicasets %} + <tr> + <td><a href="./replicaset-{{ rs.name }}.html" title="{{ rs.name }}">{{ rs.name }}</a></td> + <td>{% if rs.k8s.status.ready_replicas %}{{ rs.k8s.status.ready_replicas }}{% else %}0{% endif %}/{{ rs.k8s.spec.replicas }}</td> + <td>{{ number_ok(rs.k8s.status.available_replicas, '0', total=rs.k8s.spec.replicas) }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + {% if (ns.statefulsets | length) > 0 %} + <!-- StatefulSets table --> + <div id="statefulsets" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Ready</th> + <th>Up to Date</th> + </tr> + </thead> + <tbody> + {% for sts in ns.statefulsets %} + <tr> + <td><a href="./statefulset-{{ sts.name }}.html" title="{{ sts.name }}">{{ sts.name }}</a></td> + <td>{% if sts.k8s.status.ready_replicas %}{{ sts.k8s.status.ready_replicas }}{% else %}0{% endif %}/{{ sts.k8s.spec.replicas }}</td> + <td>{{ number_ok(sts.k8s.status.updated_replicas, '0', total=sts.k8s.spec.replicas) }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + {% if (ns.daemonsets | length) > 0 %} + <!-- DaemonSets table --> + <div id="daemonsets" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Desired</th> + <th>Current</th> + <th>Ready</th> + <th>Up to Date</th> + <th>Available</th> + </tr> + </thead> + <tbody> + {% for ds in ns.daemonsets %} + <tr> + <td><a href="./daemoset-{{ ds.name }}.html" title="{{ ds.name }}">{{ ds.name }}</a></td> + <td>{{ sts.k8s.status.desired_number_scheduled }}</td> + <td>{{ number_ok(sts.k8s.status.current_number_scheduled, '0', total=sts.k8s.spec.desired_number_scheduled) }}</td> + <td>{{ number_ok(sts.k8s.status.number_ready, '0', total=sts.k8s.spec.desired_number_scheduled) }}</td> + <td>{{ number_ok(sts.k8s.status.updated_number_scheduled, '0', total=sts.k8s.spec.desired_number_scheduled) }}</td> + <td>{{ number_ok(sts.k8s.status.number_available, '0', total=sts.k8s.spec.desired_number_scheduled) }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + {% if (ns.pvcs | length) > 0 %} + <!-- PVCs table --> + <div id="pvcs" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Status</th> + <th>Volume</th> + <th>Capacity</th> + <th>Access Modes</th> + <th>Storage Class</th> + </tr> + </thead> + <tbody> + {% for pvc in ns.pvcs %} + <tr> + <td>{{ pvc.name }}</td> + <td>{% if (pvc.k8s.status.phase | lower) == "bound" %}{{ pvc.k8s.status.phase }}{% else %}<span class="tag is-warning">{{ pvc.k8s.status.phase }}</span>{% endif %}</td> + <td>{% if pvc.k8s.spec.volume_name %}{{ pvc.k8s.spec.volume_name }}{% endif %}</td> + <td>{% if pvc.k8s.status.capacity %}{{ pvc.k8s.status.capacity.storage }}{% endif %}</td> + <td>{% if pvc.k8s.status.access_modes %}{{ pvc.k8s.status.capacity.access_modes | join(', ') }}{% endif %}</td> + <td>{% if pvc.k8s.spec.storage_class_name %}{{ pvc.k8s.spec.storage_class_name }}{% endif %}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + {% if (ns.configmaps | length) > 0 %} + <!-- ConfigMaps table --> + <div id="configmaps" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + </tr> + </thead> + <tbody> + {% for cm in ns.configmaps %} + <tr> + <td>{{ cm.name }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + {% if (ns.secrets | length) > 0 %} + <!-- Secrets table --> + <div id="secrets" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + </tr> + </thead> + <tbody> + {% for secret in ns.secrets %} + <tr> + <td>{{ secret.name }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + {% if (ns.ingresses | length) > 0 %} + <div id="ingresses"></div> + {% endif %} + + </div> +{% endblock %} + +{% block more_head %} + <script language="JavaScript"> + function toggleVisibility(id) { + document.getElementById('pods').style.display = 'none'; + document.getElementById('services').style.display = 'none'; + {% if (ns.jobs | length) > 0 %} + document.getElementById('jobs').style.display = 'none'; + {% endif %} + {% if (ns.deployments | length) > 0 %} + document.getElementById('deployments').style.display = 'none'; + {% endif %} + {% if (ns.replicasets | length) > 0 %} + document.getElementById('replicasets').style.display = 'none'; + {% endif %} + {% if (ns.statefulsets | length) > 0 %} + document.getElementById('statefulsets').style.display = 'none'; + {% endif %} + {% if (ns.daemonsets | length) > 0 %} + document.getElementById('daemonsets').style.display = 'none'; + {% endif %} + {% if (ns.pvcs | length) > 0 %} + document.getElementById('pvcs').style.display = 'none'; + {% endif %} + {% if (ns.configmaps | length) > 0 %} + document.getElementById('configmaps').style.display = 'none'; + {% endif %} + {% if (ns.secrets | length) > 0 %} + document.getElementById('secrets').style.display = 'none'; + {% endif %} + {% if (ns.ingresses | length) > 0 %} + document.getElementById('ingresses').style.display = 'none'; + {% endif %} + document.getElementById(id).style.display = 'inline'; + document.getElementById('pods_toggle').classList.remove("is-active"); + document.getElementById('services_toggle').classList.remove("is-active"); + {% if (ns.jobs | length) > 0 %} + document.getElementById('jobs_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.deployments | length) > 0 %} + document.getElementById('deployments_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.replicasets | length) > 0 %} + document.getElementById('replicasets_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.statefulsets | length) > 0 %} + document.getElementById('statefulsets_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.daemonsets | length) > 0 %} + document.getElementById('daemonsets_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.pvcs | length) > 0 %} + document.getElementById('pvcs_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.configmaps | length) > 0 %} + document.getElementById('configmaps_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.secrets | length) > 0 %} + document.getElementById('secrets_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.ingresses | length) > 0 %} + document.getElementById('ingresses_toggle').classList.remove("is-active"); + {% endif %} + {% if (ns.deployments | length) > 0 %} + document.getElementById(id + '_toggle').classList.add("is-active"); + {% endif %} + } + document.addEventListener('readystatechange', () => { + if (document.readyState == 'complete') toggleVisibility('pods'); + }); + </script> +{% endblock %} diff --git a/src/onaptests/templates/status/job.html.j2 b/src/onaptests/templates/status/job.html.j2 new file mode 100644 index 0000000..7915ff2 --- /dev/null +++ b/src/onaptests/templates/status/job.html.j2 @@ -0,0 +1,15 @@ +{% extends "base.html.j2" %} +{% block title %}Job {{ job.name }}{% endblock %} +{% block content %} + {{ two_level_breadcrumb('Job', job.name) | indent(width=4) }} + + {{ pod_parent_summary('Job', job.name, job.failed_pods, job.pods) }} + + <section class="section"> + {{ description(job.k8s) | indent(width=6) }} + + {{ pods_container(job.pods, "Job") | indent(width=6) }} + + {{ events(job.events) }} + </section> +{% endblock %} diff --git a/src/onaptests/templates/status/pod.html.j2 b/src/onaptests/templates/status/pod.html.j2 new file mode 100644 index 0000000..d922206 --- /dev/null +++ b/src/onaptests/templates/status/pod.html.j2 @@ -0,0 +1,101 @@ +{% macro container_table(title, containers_list) %} +<div class="container"> + <h1 class="title is-1">{{ title }}</h1> + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Image</th> + <th>State</th> + <th>Ready</th> + <th>Restart Count</th> + </tr> + <tbody> + {% for container in containers_list %} + <tr> + <td><a href="./pod-{{ pod.name }}-{{ container.name }}-logs.html" title="{{ pod.name }}-{{ container.name }}-logs">{{ container.name }}</a></td> + <td>{{ container.image }}</td> + <td>{{ container.status }}</td> + <td>{{ container.ready }}</td> + <td>{{ container.restart_count }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> +</div> +{% endmacro %} + +{% extends "base.html.j2" %} +{% block title %}Pod {{ pod.name }}{% endblock %} +{% block content %} + {{ two_level_breadcrumb('Pod', pod.name) | indent(width=4) }} + + {{ summary('Pod', pod.name, [ + { + 'title': 'Init containers', + 'failing': ((pod.init_containers | length) - pod.runned_init_containers), + 'total': (pod.init_containers | length) + }, + { + 'title': 'Containers', + 'failing': ((pod.containers | length) - pod.running_containers), + 'total': (pod.containers | length) + }]) + }} + + <section class="section"> + {{ description(pod.k8s) | indent(width=6) }} + + {% if (pod.init_containers | length) > 0 %} + {{ container_table("Init Containers", pod.init_containers) | indent(width=6) }} + {% endif %} + + + {% if (pod.containers | length) > 0 %} + {{ container_table("Containers", pod.containers) | indent(width=8) }} + {% endif %} + + {% if pod.k8s.spec.volumes %} + <div class="container"> + <h1 class="title is-1">Volumes</h1> + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Properties</th> + </tr> + </thead> + <tbody> + {% for volume_name, volume in pod.volumes.items() %} + {% for volume_type, details in volume.items() %} + <tr> + <td>{{ volume_name }}</td> + <td>{{ volume_type }}</td> + <td> + <table class="table is-narrow"> + <tbody> + {% for key, value in details.items() %} + <tr> + <th>{{ key }}</th> + <td>{{ value }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </td> + </tr> + {% endfor %} + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} + + {{ events(pod.events) }} + </section> +{% endblock %} diff --git a/src/onaptests/templates/status/raw_output.txt.j2 b/src/onaptests/templates/status/raw_output.txt.j2 new file mode 100644 index 0000000..1c52531 --- /dev/null +++ b/src/onaptests/templates/status/raw_output.txt.j2 @@ -0,0 +1,28 @@ +{%- macro statistic(resource_name, failing, total, failing_list) %} +>>> Nb {{ resource_name }}: {{ total }} +>>> Nb Failed {{ resource_name }}: {{ failing }} +{%- if failing > 0 %} +>>> List of Failed {{ resource_name }}: [{{ failing_list | map(attribute='name') | join(", ") }}] +{%- endif %} +{%- endmacro %} +------------------------------------------------ +------- {{ namespace }} kubernetes tests ------------------ +------------------------------------------------ +{%- if (ns.jobs | length) > 0 %} +{{ statistic("Jobs", (ns.failing_jobs | length), (ns.jobs | length), ns.failing_jobs) }} +{%- endif %} +{%- if (ns.deployments | length) > 0 %} +{{ statistic("Deployments", (ns.failing_deployments | length), (ns.deployments | length), ns.failing_deployments) }} +{%- endif %} +{%- if (ns.statefulsets | length) > 0 %} +{{ statistic("StatefulSets", (ns.failing_statefulsets | length), (ns.statefulsets | length), ns.failing_statefulsets) }} +{%- endif %} +{%- if (ns.daemonsets | length) > 0 %} +{{ statistic("DaemonSets", (ns.failing_daemonsets | length), (ns.daemonsets | length), ns.failing_daemonsets) }} +{%- endif %} +{%- if (ns.pvcs | length) > 0 %} +{{ statistic("Persistent Volume Claims", (ns.failing_pvcs | length), (ns.pvcs | length), ns.failing_pvcs) }} +{%- endif %} +------------------------------------------------ +------------------------------------------------ +------------------------------------------------ diff --git a/src/onaptests/templates/status/replicaset.html.j2 b/src/onaptests/templates/status/replicaset.html.j2 new file mode 100644 index 0000000..f26f2fd --- /dev/null +++ b/src/onaptests/templates/status/replicaset.html.j2 @@ -0,0 +1,15 @@ +{% extends "base.html.j2" %} +{% block title %}ReplicaSet {{ replicaset.name }}{% endblock %} +{% block content %} + {{ two_level_breadcrumb('ReplicaSet', replicaset.name) | indent(width=4) }} + + {{ pod_parent_summary('ReplicaSet', replicaset.name, replicaset.failed_pods, replicaset.pods) }} + + <section class="section"> + {{ description(replicaset.k8s) | indent(width=6) }} + + {{ pods_container(replicaset.pods, "ReplicaSet") | indent(width=6) }} + + {{ events(replicaset.events) }} + </section> +{% endblock %} diff --git a/src/onaptests/templates/status/service.html.j2 b/src/onaptests/templates/status/service.html.j2 new file mode 100644 index 0000000..31b239a --- /dev/null +++ b/src/onaptests/templates/status/service.html.j2 @@ -0,0 +1,45 @@ +{% extends "base.html.j2" %} +{% block title %}Service {{ service.name }}{% endblock %} +{% block content %} + {{ two_level_breadcrumb('Service', service.name) | indent(width=4) }} + + {{ pod_parent_summary('Service', service.name, service.failed_pods, service.pods) }} + + <section class="section"> + {{ description(service.k8s) | indent(width=6) }} + + {{ pods_container(service.pods, "Service") | indent(width=6) }} + + <div class="container"> + <h1 class="title is-1">Ports</h1> + {% if service.k8s.spec.ports %} + <div id="ports" class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable"> + <thead> + <tr> + <th>Name</th> + <th>Port</th> + <th>Node Port</th> + <th>Target Port</th> + <th>Protocol</th> + </tr> + </thead> + <tbody> + {% for port in service.k8s.spec.ports %} + <tr> + <td>{{ port.name }}</td> + <td>{{ port.port }}</td> + <td>{{ port.node_port }}</td> + <td>{{ port.target_port }}</td> + <td>{{ port.protocol }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% else %} + <div class="notification is-warning">Service has no ports!</div> + {% endif %} + </div> + </section> +{% endblock %} diff --git a/src/onaptests/templates/status/statefulset.html.j2 b/src/onaptests/templates/status/statefulset.html.j2 new file mode 100644 index 0000000..1aac8eb --- /dev/null +++ b/src/onaptests/templates/status/statefulset.html.j2 @@ -0,0 +1,15 @@ +{% extends "base.html.j2" %} +{% block title %}StatefulSet {{ statefulset.name }}{% endblock %} +{% block content %} + {{ two_level_breadcrumb('StatefulSet', statefulset.name) | indent(width=4) }} + + {{ pod_parent_summary('StatefulSet', statefulset.name, statefulset.failed_pods, statefulset.pods) }} + + <section class="section"> + {{ description(statefulset.k8s) | indent(width=6) }} + + {{ pods_container(statefulset.pods, "StatefulSet") | indent(width=6) }} + + {{ events(statefulset.events) }} + </section> +{% endblock %} diff --git a/src/onaptests/templates/status/version.html.j2 b/src/onaptests/templates/status/version.html.j2 new file mode 100644 index 0000000..40348a4 --- /dev/null +++ b/src/onaptests/templates/status/version.html.j2 @@ -0,0 +1,28 @@ + +{% extends "base.html.j2" %} +{% block title %}ONAP Docker Versions{% endblock %} + +{% block content %} +<h1 class="title is-1">ONAP Docker versions</h1> +<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth"> + <thead> + <tr> + <th>Component</th> + <th>Container</th> + <th>Image</th> + <th>Version</th> + </tr> + </thead> + <tbody> + <tr> + {% for pod in pod_versions %} + <tr> + <td>{{ pod.component }}</td> + <td>{{ pod.container }}</td> + <td>{{ pod.image }}</td> + <td>{{ pod.version }}</td> + </tr> + {% endfor %} + </tbody> + </table> +{% endblock %} @@ -5,9 +5,12 @@ skipsdist = true requires = pip >= 8 [testenv] -basepython = python3 -whitelist_externals = +basepython = python3.8 +allowlist_externals = git + /bin/sh + sh + /bin/bash bash deps = pyyaml == 3.13 |