diff options
author | Michal Jagiello <michal.jagiello@t-mobile.pl> | 2023-04-19 09:53:38 +0000 |
---|---|---|
committer | Michal Jagiello <michal.jagiello@t-mobile.pl> | 2023-04-19 09:58:02 +0000 |
commit | 6e88d548362b32a15a094fdf8d83f082107c7962 (patch) | |
tree | 8603bba306091d8a323ac4ed32920bc075998af5 /test | |
parent | c57b24365c08afe394e52808d55e9b70ac878205 (diff) |
Fix security versions script
That script was usused on security versions tests, so I updated it with
the latest changes from repo which was really used, created needed files
and after we merge it we could use that on security tests.
Issue-ID: TEST-394
Signed-off-by: Michal Jagiello <michal.jagiello@t-mobile.pl>
Change-Id: I8e5daa7d43e2723bbe3308cf85b1cae2b2f587ad
Diffstat (limited to 'test')
19 files changed, 815 insertions, 310 deletions
diff --git a/test/security/check_versions/.gitignore b/test/security/check_versions/.gitignore index db6444b3c..2b574f8c0 100644 --- a/test/security/check_versions/.gitignore +++ b/test/security/check_versions/.gitignore @@ -1,5 +1,4 @@ .pytest_cache/ __pycache__/ -/env/.vagrant /temp/ /.tox/ diff --git a/test/security/check_versions/README.md b/test/security/check_versions/README.md index 3934ca77a..399d10443 100644 --- a/test/security/check_versions/README.md +++ b/test/security/check_versions/README.md @@ -6,25 +6,12 @@ in the kubernetes cluster containers. ## Commands -### Creating environment - -All development and testing process, should be done in prepared virtual machine, -that is containing development environment for this project. Vagrant plugins, -that are required to start virtual machine: `vagrant-libvirt`, `vagrant-reload`, -`vagrant-sshfs`. - -```bash -cd env -vagrant up -vagrant ssh -``` - ### Install dependencies To install dependencies for normal usage of script, run this command. ```bash -pip3 install -r env/requirements.txt +pip3 install -r requirements.txt ``` ### Code formatting diff --git a/test/security/check_versions/env/Vagrantfile b/test/security/check_versions/env/Vagrantfile deleted file mode 100644 index 9753a74ad..000000000 --- a/test/security/check_versions/env/Vagrantfile +++ /dev/null @@ -1,36 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant.configure("2") do |config| - - config.vm.provider :libvirt do |libvirt| - libvirt.default_prefix = "k8s_bin_versions_inspector"; - libvirt.driver = "kvm"; - libvirt.cpus = 6; - libvirt.memory = 12288; - end - - config.vm.box = "generic/ubuntu1804"; - config.vm.hostname = "k8s-bin-versions-inspector"; - config.vm.synced_folder ".", "/vagrant", disabled: true; - config.vm.synced_folder "..", "/home/vagrant/k8s_bin_versions_inspector", type: :sshfs; - - config.vm.provision "shell", inline: <<-end - export DEBIAN_FRONTEND=noninteractive &&\ - apt-get update &&\ - apt-get upgrade -y &&\ - apt-get dist-upgrade -y &&\ - apt-get install -y python3 python3-pip snap git vim net-tools htop &&\ - pip3 install --system -r /home/vagrant/k8s_bin_versions_inspector/env/requirements-dev.txt &&\ - snap install --classic microk8s &&\ - usermod -a -G microk8s vagrant - end - config.vm.provision :reload; - config.vm.provision "shell", privileged: false, inline: <<-end - microk8s start &&\ - microk8s status --wait-ready &&\ - microk8s config > /home/vagrant/.kube/config &&\ - microk8s kubectl apply -f /home/vagrant/k8s_bin_versions_inspector/env/configuration - end -end - diff --git a/test/security/check_versions/env/configuration/namespaces.yaml b/test/security/check_versions/env/configuration/namespaces.yaml deleted file mode 100644 index f300cc7da..000000000 --- a/test/security/check_versions/env/configuration/namespaces.yaml +++ /dev/null @@ -1,45 +0,0 @@ ---- -apiVersion: v1 -kind: Namespace -metadata: - name: ingress-nginx - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-ingress-nginx - namespace: ingress-nginx -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-ingress-nginx - template: - metadata: - labels: - app: kbvi-test-ingress-nginx - spec: - containers: - - name: echo-server - image: jmalloc/echo-server - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-kube-system - namespace: kube-system -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-kube-system - template: - metadata: - labels: - app: kbvi-test-kube-system - spec: - containers: - - name: echo-server - image: jmalloc/echo-server diff --git a/test/security/check_versions/env/configuration/terminated.yaml b/test/security/check_versions/env/configuration/terminated.yaml deleted file mode 100644 index dd6ce829d..000000000 --- a/test/security/check_versions/env/configuration/terminated.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-terminated -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-terminated - template: - metadata: - labels: - app: kbvi-test-terminated - spec: - containers: - - name: python - image: python diff --git a/test/security/check_versions/env/configuration/versions.yaml b/test/security/check_versions/env/configuration/versions.yaml deleted file mode 100644 index 75b7f7b85..000000000 --- a/test/security/check_versions/env/configuration/versions.yaml +++ /dev/null @@ -1,112 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-python-jupyter -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-python-jupyter - template: - metadata: - labels: - app: kbvi-test-python-jupyter - spec: - containers: - - name: jupyter - image: jupyter/base-notebook - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-python-jupyter-old -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-python-jupyter-old - template: - metadata: - labels: - app: kbvi-test-python-jupyter-old - spec: - containers: - - name: jupyter-old - image: jupyter/base-notebook:ff922f8f533a - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-python-stderr-filebeat -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-python-stderr-filebeat - template: - metadata: - labels: - app: kbvi-test-python-stderr-filebeat - spec: - containers: - - name: filebeat - image: docker.elastic.co/beats/filebeat:5.5.0 - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-java-keycloak -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-java-keycloak - template: - metadata: - labels: - app: kbvi-test-java-keycloak - spec: - containers: - - name: keycloak - image: jboss/keycloak - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-java-keycloak-old -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-java-keycloak-old - template: - metadata: - labels: - app: kbvi-test-java-keycloak-old - spec: - containers: - - name: keycloak-old - image: jboss/keycloak:8.0.0 - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-java-keycloak-very-old -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-java-keycloak-very-old - template: - metadata: - labels: - app: kbvi-test-java-keycloak-very-old - spec: - containers: - - name: keycloak-very-old - image: jboss/keycloak:2.0.0.Final diff --git a/test/security/check_versions/env/requirements-dev.txt b/test/security/check_versions/env/requirements-dev.txt deleted file mode 100644 index 1ced42c04..000000000 --- a/test/security/check_versions/env/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -cerberus -dataclasses -kubernetes -pyyaml -tabulate -black -pylint -pytest - diff --git a/test/security/check_versions/env/requirements.txt b/test/security/check_versions/env/requirements.txt deleted file mode 100644 index e81358f72..000000000 --- a/test/security/check_versions/env/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -cerberus -dataclasses -kubernetes -pyyaml -tabulate - diff --git a/test/security/check_versions/pyproject.toml b/test/security/check_versions/pyproject.toml new file mode 100644 index 000000000..a9b5c540e --- /dev/null +++ b/test/security/check_versions/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "check_versions" +readme = "README.md" +version = "1.0" +requires-python = ">=3.7" +dependencies = [ + "kubernetes", + "jinja2", + "xtesting", + "tabulate", + "cerberus", + "packaging", + "wget" +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project.entry-points."xtesting.testcase"] +versions = "versions.k8s_bin_versions_inspector_test_case:Inspector" diff --git a/test/security/check_versions/requirements.txt b/test/security/check_versions/requirements.txt new file mode 100644 index 000000000..8e46a3acf --- /dev/null +++ b/test/security/check_versions/requirements.txt @@ -0,0 +1,7 @@ +kubernetes +jinja2 +xtesting +tabulate +cerberus +packaging +wget diff --git a/test/security/check_versions/tests/test_main.py b/test/security/check_versions/tests/test_main.py index 0dff0b230..37ad45ee3 100644 --- a/test/security/check_versions/tests/test_main.py +++ b/test/security/check_versions/tests/test_main.py @@ -7,9 +7,7 @@ import yaml def exec_main(pod_name_trimmer, acceptable_data): - with tempfile.NamedTemporaryFile() as output_temp, tempfile.NamedTemporaryFile() as acceptable_temp: - with open(acceptable_temp.name, "w") as stream: yaml.safe_dump(acceptable_data, stream) @@ -61,7 +59,6 @@ def exec_main(pod_name_trimmer, acceptable_data): def test_main(pod_name_trimmer): - acceptable_data = { "python": ["2.7.5", "3.6.6", "3.8.4"], "java": ["11.0.5", "11.0.8"], @@ -73,7 +70,6 @@ def test_main(pod_name_trimmer): def test_main_neg(pod_name_trimmer): - acceptable_data = { "python": ["3.6.6", "3.8.4"], "java": ["11.0.5", "11.0.8"], diff --git a/test/security/check_versions/tests/test_verify_versions_acceptability.py b/test/security/check_versions/tests/test_verify_versions_acceptability.py index 5e2f0d2c8..1cb931679 100644 --- a/test/security/check_versions/tests/test_verify_versions_acceptability.py +++ b/test/security/check_versions/tests/test_verify_versions_acceptability.py @@ -7,7 +7,6 @@ import pathlib def exec_verify_versions_acceptability(containers): - config = { "python": ["1.1.1", "2.2.2"], "java": ["3.3.3"], @@ -23,7 +22,6 @@ def exec_verify_versions_acceptability(containers): def test_verify_versions_acceptability(): - containers = [ kbvi.ContainerInfo("a", "b", "c", None, kbvi.ContainerVersions([], [])), kbvi.ContainerInfo( @@ -37,7 +35,6 @@ def test_verify_versions_acceptability(): def test_verify_versions_acceptability_neg_1(): - containers = [ kbvi.ContainerInfo("a", "b", "c", None, kbvi.ContainerVersions(["3.3.3"], [])) ] @@ -48,7 +45,6 @@ def test_verify_versions_acceptability_neg_1(): def test_verify_versions_acceptability_neg_2(): - containers = [ kbvi.ContainerInfo("a", "b", "c", None, kbvi.ContainerVersions([], ["1.1.1"])) ] diff --git a/test/security/check_versions/tox.ini b/test/security/check_versions/tox.ini index 703ee280a..d2a007160 100644 --- a/test/security/check_versions/tox.ini +++ b/test/security/check_versions/tox.ini @@ -1,16 +1,18 @@ [tox] -envlist = black, pylint +envlist = black, pylint, pytest skipsdist = true [testenv] basepython = python3.8 -deps = -r{toxinidir}/env/requirements-dev.txt +deps = -r{toxinidir}/requirements.txt [testenv:black] -commands = black {toxinidir}/src tests +commands = black {toxinidir}/versions tests +deps = black [testenv:pylint] -commands = pylint -d C0330,W0511 {toxinidir}/src +commands = pylint -d C0330,W0511 {toxinidir}/versions +deps= pylint [testenv:pytest] setenv = PYTHONPATH = {toxinidir}/src diff --git a/test/security/check_versions/versions/__init__.py b/test/security/check_versions/versions/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/test/security/check_versions/versions/__init__.py diff --git a/test/security/check_versions/src/k8s_bin_versions_inspector.py b/test/security/check_versions/versions/k8s_bin_versions_inspector.py index f5ff53714..bd3041d63 100644 --- a/test/security/check_versions/src/k8s_bin_versions_inspector.py +++ b/test/security/check_versions/versions/k8s_bin_versions_inspector.py @@ -40,17 +40,26 @@ import argparse import dataclasses import itertools import json +import logging import pathlib import pprint import re import string import sys +from typing import Iterable, List, Optional, Pattern, Union import tabulate import yaml -import cerberus import kubernetes +RECOMMENDED_VERSIONS_FILE = "/tmp/recommended_versions.yaml" +WAIVER_LIST_FILE = "/tmp/versions_xfail.txt" + +# Logger +logging.basicConfig() +LOGGER = logging.getLogger("onap-versions-status-inspector") +LOGGER.setLevel("INFO") + def parse_argv(argv: Optional[List[str]] = None) -> argparse.Namespace: """Function for parsing command line arguments. @@ -118,19 +127,19 @@ def parse_argv(argv: Optional[List[str]] = None) -> argparse.Namespace: "-n", "--namespace", help="Namespace to use to list pods." - "If empty pods are going to be listed from all namespaces" + "If empty pods are going to be listed from all namespaces", ) parser.add_argument( "--check-istio-sidecar", action="store_true", - help="Add if you want to check istio sidecars also" + help="Add if you want to check istio sidecars also", ) parser.add_argument( "--istio-sidecar-name", default="istio-proxy", - help="Name of istio sidecar to filter out" + help="Name of istio sidecar to filter out", ) parser.add_argument( @@ -148,6 +157,13 @@ def parse_argv(argv: Optional[List[str]] = None) -> argparse.Namespace: ) parser.add_argument( + "-w", + "--waiver", + type=pathlib.Path, + help="Path of the waiver xfail file.", + ) + + parser.add_argument( "-V", "--version", action="version", @@ -221,7 +237,7 @@ def list_all_containers( field_selector: str, namespace: Union[None, str], check_istio_sidecars: bool, - istio_sidecar_name: str + istio_sidecar_name: str, ) -> Iterable[ContainerInfo]: """Get list of all containers names. @@ -243,6 +259,13 @@ def list_all_containers( else: pods = api.list_pod_for_all_namespaces(field_selector=field_selector).items + # Filtering to avoid testing integration or replica pods + pods = [ + pod + for pod in pods + if "replica" not in pod.metadata.name and "integration" not in pod.metadata.name + ] + containers_statuses = ( (pod.metadata.namespace, pod.metadata.name, pod.status.container_statuses) for pod in pods @@ -276,7 +299,9 @@ def list_all_containers( ) if not check_istio_sidecars: - container_items = filter(lambda container: container.container != istio_sidecar_name, container_items) + container_items = filter( + lambda container: container.container != istio_sidecar_name, container_items + ) yield from container_items @@ -303,8 +328,13 @@ def sync_post_namespaced_pod_exec( or -2 if other failure occurred. """ + stdout = "" + stderr = "" + error = {} + code = -1 + LOGGER.debug("sync_post_namespaced_pod_exec container= %s", container.pod) try: - client = kubernetes.stream.stream( + client_stream = kubernetes.stream.stream( api.connect_post_namespaced_pod_exec, namespace=container.namespace, name=container.pod, @@ -317,33 +347,13 @@ def sync_post_namespaced_pod_exec( _request_timeout=1.0, _preload_content=False, ) - except ( - kubernetes.client.rest.ApiException, - kubernetes.client.exceptions.ApiException, - ): - - if container.extra.running: - raise - - return { - "stdout": "", - "stderr": "", - "error": {}, - "code": -1, - } - - client.run_forever(timeout=5) - - stdout = client.read_stdout() - stderr = client.read_stderr() - error = yaml.safe_load( - client.read_channel(kubernetes.stream.ws_client.ERROR_CHANNEL) - ) + client_stream.run_forever(timeout=5) + stdout = client_stream.read_stdout() + stderr = client_stream.read_stderr() + error = yaml.safe_load( + client_stream.read_channel(kubernetes.stream.ws_client.ERROR_CHANNEL) + ) - # TODO: Is there really no better way, to check - # execution exit code in python k8s API client? - code = -2 - try: code = ( 0 if error["status"] == "Success" @@ -351,7 +361,13 @@ def sync_post_namespaced_pod_exec( if error["reason"] != "NonZeroExitCode" else int(error["details"]["causes"][0]["message"]) ) - except: + except ( + kubernetes.client.rest.ApiException, + kubernetes.client.exceptions.ApiException, + ): + LOGGER.debug("Discard unexpected k8s client Error..") + except TypeError: + LOGGER.debug("Type Error, no error status") pass return { @@ -498,7 +514,7 @@ def gather_containers_informations( ignore_empty: bool, namespace: Union[None, str], check_istio_sidecars: bool, - istio_sidecar_name: str + istio_sidecar_name: str, ) -> List[ContainerInfo]: """Get list of all containers names. @@ -516,14 +532,20 @@ def gather_containers_informations( List of initialized objects for containers in k8s cluster. """ - containers = list(list_all_containers(api, field_selector, namespace, - check_istio_sidecars, istio_sidecar_name)) + containers = list( + list_all_containers( + api, field_selector, namespace, check_istio_sidecars, istio_sidecar_name + ) + ) + LOGGER.info("List of containers: %s", containers) # TODO: This loop should be parallelized for container in containers: + LOGGER.info("Container -----------------> %s", container) python_versions = determine_versions_of_python(api, container) java_versions = determine_versions_of_java(api, container) container.versions = ContainerVersions(python_versions, java_versions) + LOGGER.info("Container versions: %s", container.versions) if ignore_empty: containers = [c for c in containers if c.versions.python or c.versions.java] @@ -635,14 +657,18 @@ def generate_and_handle_output( "pprint": generate_output_pprint, "json": generate_output_json, } + LOGGER.debug("output_generators: %s", output_generators) output = output_generators[output_format](containers) if output_file: - output_file.write_text(output) + try: + output_file.write_text(output) + except AttributeError: + LOGGER.error("Not possible to write_text") if not quiet: - print(output) + LOGGER.info(output) def verify_versions_acceptability( @@ -662,38 +688,32 @@ def verify_versions_acceptability( if not acceptable: return 0 + try: + acceptable.is_file() + except AttributeError: + LOGGER.error("No acceptable file found") + return -1 + if not acceptable.is_file(): raise FileNotFoundError( "File with configuration for acceptable does not exists!" ) - schema = { - "python": {"type": "list", "schema": {"type": "string"}}, - "java": {"type": "list", "schema": {"type": "string"}}, - } - - validator = cerberus.Validator(schema) - with open(acceptable) as stream: data = yaml.safe_load(stream) - if not validator.validate(data): - raise cerberus.SchemaError( - "Schema of file with configuration for acceptable is not valid." - ) - - python_acceptable = data.get("python", []) - java_acceptable = data.get("java", []) + python_acceptable = data.get("python3", []) + java_acceptable = data.get("java11", []) python_not_acceptable = [ - (container, "python", version) + (container, "python3", version) for container in containers for version in container.versions.python if version not in python_acceptable ] java_not_acceptable = [ - (container, "java", version) + (container, "java11", version) for container in containers for version in container.versions.java if version not in java_acceptable @@ -705,7 +725,7 @@ def verify_versions_acceptability( if quiet: return 1 - print("List of not acceptable versions") + LOGGER.error("List of not acceptable versions") pprint.pprint(python_not_acceptable) pprint.pprint(java_not_acceptable) @@ -728,8 +748,12 @@ def main(argv: Optional[List[str]] = None) -> str: api.api_client.configuration.debug = args.debug containers = gather_containers_informations( - api, args.field_selector, args.ignore_empty, args.namespace, - args.check_istio_sidecar, args.istio_sidecar_name + api, + args.field_selector, + args.ignore_empty, + args.namespace, + args.check_istio_sidecar, + args.istio_sidecar_name, ) generate_and_handle_output( diff --git a/test/security/check_versions/versions/k8s_bin_versions_inspector_test_case.py b/test/security/check_versions/versions/k8s_bin_versions_inspector_test_case.py new file mode 100644 index 000000000..87516cb60 --- /dev/null +++ b/test/security/check_versions/versions/k8s_bin_versions_inspector_test_case.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +# COPYRIGHT NOTICE STARTS HERE +# +# Copyright 2020 Samsung Electronics Co., Ltd. +# +# 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. +# +# COPYRIGHT NOTICE ENDS HERE + +import logging +import pathlib +import time +import os +import wget +from kubernetes import client, config +from xtesting.core import testcase # pylint: disable=import-error + +import versions.reporting as Reporting +from versions.k8s_bin_versions_inspector import ( + gather_containers_informations, + generate_and_handle_output, + verify_versions_acceptability, +) + +RECOMMENDED_VERSIONS_FILE = "/tmp/recommended_versions.yaml" +WAIVER_LIST_FILE = "/tmp/versions_xfail.txt" + +# Logger +logging.basicConfig() +LOGGER = logging.getLogger("onap-versions-status-inspector") +LOGGER.setLevel("INFO") + + +class Inspector(testcase.TestCase): + """Inspector CLass.""" + + def __init__(self, **kwargs): + """Init the testcase.""" + if "case_name" not in kwargs: + kwargs["case_name"] = "check_versions" + super().__init__(**kwargs) + + version = os.getenv("ONAP_VERSION", "master") + base_url = "https://git.onap.org/integration/seccom/plain" + + self.namespace = "onap" + # if no Recommended file found, download it + if pathlib.Path(RECOMMENDED_VERSIONS_FILE).is_file(): + self.acceptable = pathlib.Path(RECOMMENDED_VERSIONS_FILE) + else: + self.acceptable = wget.download( + base_url + "/recommended_versions.yaml?h=" + version, + out=RECOMMENDED_VERSIONS_FILE, + ) + self.output_file = "/tmp/versions.json" + # if no waiver file found, download it + if pathlib.Path(WAIVER_LIST_FILE).is_file(): + self.waiver = pathlib.Path(WAIVER_LIST_FILE) + else: + self.waiver = wget.download( + base_url + "/waivers/versions/versions_xfail.txt?h=" + version, + out=WAIVER_LIST_FILE, + ) + self.result = 0 + self.start_time = None + self.stop_time = None + + def run(self): + """Execute the version Inspector.""" + self.start_time = time.time() + config.load_kube_config() + api = client.CoreV1Api() + + field_selector = "metadata.namespace==onap" + + containers = gather_containers_informations(api, field_selector, True) + LOGGER.info("gather_containers_informations") + LOGGER.info(containers) + LOGGER.info("---------------------------------") + + generate_and_handle_output( + containers, "json", pathlib.Path(self.output_file), True + ) + LOGGER.info("generate_and_handle_output in %s", self.output_file) + LOGGER.info("---------------------------------") + + code = verify_versions_acceptability(containers, self.acceptable, True) + LOGGER.info("verify_versions_acceptability") + LOGGER.info(code) + LOGGER.info("---------------------------------") + + # Generate reporting + test = Reporting.OnapVersionsReporting(result_file=self.output_file) + LOGGER.info("Prepare reporting") + self.result = test.generate_reporting(self.output_file) + LOGGER.info("Reporting generated") + + self.stop_time = time.time() + if self.result >= 90: + return testcase.TestCase.EX_OK + return testcase.TestCase.EX_TESTCASE_FAILED + + def set_namespace(self, namespace): + """Set namespace.""" + self.namespace = namespace diff --git a/test/security/check_versions/versions/reporting.py b/test/security/check_versions/versions/reporting.py new file mode 100644 index 000000000..43ef26db8 --- /dev/null +++ b/test/security/check_versions/versions/reporting.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 + +# Copyright 2020 Orange, Ltd. +# +# 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. +# +""" +Generate result page +""" +import logging +import pathlib +import json +from dataclasses import dataclass +import os +import statistics +import wget +import yaml + +from packaging.version import Version + +from jinja2 import ( # pylint: disable=import-error + Environment, + select_autoescape, + PackageLoader, +) + +# Logger +LOG_LEVEL = "INFO" +logging.basicConfig() +LOGGER = logging.getLogger("onap-versions-status-reporting") +LOGGER.setLevel(LOG_LEVEL) + +REPORTING_FILE = "/var/lib/xtesting/results/versions_reporting.html" +# REPORTING_FILE = "/tmp/versions_reporting.html" +RESULT_FILE = "/tmp/versions.json" +RECOMMENDED_VERSIONS_FILE = "/tmp/recommended_versions.yaml" +WAIVER_LIST_FILE = "/tmp/versions_xfail.txt" + + +@dataclass +class TestResult: + """Test results retrieved from xtesting.""" + + pod_name: str + container: str + image: str + python_version: str + python_status: int + java_version: str + java_status: int + + +@dataclass +class SerieResult: + """Serie of tests.""" + + serie_id: str + success_rate: int = 0 + min: int = 0 + max: int = 0 + mean: float = 0.0 + median: float = 0.0 + nb_occurences: int = 0 + + +class OnapVersionsReporting: + """Build html summary page.""" + + def __init__(self, result_file) -> None: + """Initialization of the report.""" + version = os.getenv("ONAP_VERSION", "master") + base_url = "https://git.onap.org/integration/seccom/plain" + if pathlib.Path(WAIVER_LIST_FILE).is_file(): + self._waiver_file = pathlib.Path(WAIVER_LIST_FILE) + else: + self._waiver_file = wget.download( + base_url + "/waivers/versions/versions_xfail.txt?h=" + version, + out=WAIVER_LIST_FILE, + ) + if pathlib.Path(RECOMMENDED_VERSIONS_FILE).is_file(): + self._recommended_versions_file = pathlib.Path(RECOMMENDED_VERSIONS_FILE) + else: + self._recommended_versions_file = wget.download( + base_url + "/recommended_versions.yaml?h=" + version, + out=RECOMMENDED_VERSIONS_FILE, + ) + + def get_versions_scan_results(self, result_file, waiver_list): + """Get all the versions from the scan.""" + testresult = [] + # Get the recommended version list for java and python + min_java_version = self.get_recommended_version( + RECOMMENDED_VERSIONS_FILE, "java11" + ) + min_python_version = self.get_recommended_version( + RECOMMENDED_VERSIONS_FILE, "python3" + ) + + LOGGER.info("Min Java recommended version: %s", min_java_version) + LOGGER.info("Min Python recommended version: %s", min_python_version) + + with open(result_file) as json_file: + data = json.load(json_file) + LOGGER.info("Number of pods: %s", len(data)) + for component in data: + if component["container"] not in waiver_list: + testresult.append( + TestResult( + pod_name=component["pod"], + container=component["container"], + image=component["extra"]["image"], + python_version=component["versions"]["python"], + java_version=component["versions"]["java"], + python_status=self.get_version_status( + component["versions"]["python"], min_python_version[0] + ), + java_status=self.get_version_status( + component["versions"]["java"], min_java_version[0] + ), + ) + ) + LOGGER.info("Nb of pods (after waiver filtering) %s", len(testresult)) + return testresult + + @staticmethod + def get_version_status(versions, min_version): + """Based on the min version set the status of the component version.""" + # status_code + # 0: only recommended version found + # 1: recommended version found but not alone + # 2: recommended version not found but not far + # 3: recommended version not found but not far but not alone + # 4: recommended version not found + # we assume that versions are given accordign to usual java way + # X.Y.Z + LOGGER.debug("Version = %s", versions) + LOGGER.debug("Min Version = %s", min_version) + nb_versions_found = len(versions) + status_code = -1 + LOGGER.debug("Nb versions found :%s", nb_versions_found) + # if no version found retrieved -1 + if nb_versions_found > 0: + for version in versions: + clean_version = Version(version.replace("_", ".")) + min_version_ok = str(min_version) + + if clean_version >= Version(min_version_ok): + if nb_versions_found < 2: + status_code = 0 + else: + status_code = 2 + elif clean_version.major >= Version(min_version_ok).major: + if nb_versions_found < 2: + status_code = 1 + else: + status_code = 3 + else: + status_code = 4 + LOGGER.debug("Version status code = %s", status_code) + return status_code + + @staticmethod + def get_recommended_version(recommended_versions_file, component): + """Retrieve data from the json file.""" + with open(recommended_versions_file) as stream: + data = yaml.safe_load(stream) + try: + recommended_version = data[component]["recommended_versions"] + except KeyError: + recommended_version = None + return recommended_version + + @staticmethod + def get_waiver_list(waiver_file_path): + """Get the waiver list.""" + pods_to_be_excluded = [] + with open(waiver_file_path) as waiver_list: + for line in waiver_list: + line = line.strip("\n") + line = line.strip("\t") + if not line.startswith("#"): + pods_to_be_excluded.append(line) + return pods_to_be_excluded + + @staticmethod + def get_score(component_type, scan_res): + # Look at the java and python results + # 0 = recommended version + # 1 = acceptable version + nb_good_versions = 0 + nb_results = 0 + + for res in scan_res: + if component_type == "java": + if res.java_status >= 0: + nb_results += 1 + if res.java_status < 2: + nb_good_versions += 1 + elif component_type == "python": + if res.python_status >= 0: + nb_results += 1 + if res.python_status < 2: + nb_good_versions += 1 + try: + return round(nb_good_versions * 100 / nb_results, 1) + except ZeroDivisionError: + LOGGER.error("Impossible to calculate the success rate") + return 0 + + def generate_reporting(self, result_file): + """Generate HTML reporting page.""" + LOGGER.info("Generate versions HTML report.") + + # Get the waiver list + waiver_list = self.get_waiver_list(self._waiver_file) + LOGGER.info("Waiver list: %s", waiver_list) + + # Get the Versions results + scan_res = self.get_versions_scan_results(result_file, waiver_list) + + LOGGER.info("scan_res: %s", scan_res) + + # Evaluate result + status_res = {"java": 0, "python": 0} + for component_type in "java", "python": + status_res[component_type] = self.get_score(component_type, scan_res) + + LOGGER.info("status_res: %s", status_res) + + # Calculate the average score + numbers = [status_res[key] for key in status_res] + mean_ = statistics.mean(numbers) + + # Create reporting page + jinja_env = Environment( + autoescape=select_autoescape(["html"]), + loader=PackageLoader("onap_check_versions"), + ) + page_info = { + "title": "ONAP Integration versions reporting", + "success_rate": status_res, + "mean": mean_, + } + jinja_env.get_template("versions.html.j2").stream( + info=page_info, data=scan_res + ).dump("{}".format(REPORTING_FILE)) + + return mean_ + + +if __name__ == "__main__": + test = OnapVersionsReporting( + RESULT_FILE, WAIVER_LIST_FILE, RECOMMENDED_VERSIONS_FILE + ) + test.generate_reporting(RESULT_FILE) diff --git a/test/security/check_versions/versions/templates/base.html.j2 b/test/security/check_versions/versions/templates/base.html.j2 new file mode 100644 index 000000000..025c0ad25 --- /dev/null +++ b/test/security/check_versions/versions/templates/base.html.j2 @@ -0,0 +1,232 @@ +{% 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 %} + +<!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.9.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" width="234" height="50"> + </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"> + <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/test/security/check_versions/versions/templates/versions.html.j2 b/test/security/check_versions/versions/templates/versions.html.j2 new file mode 100644 index 000000000..4860a72da --- /dev/null +++ b/test/security/check_versions/versions/templates/versions.html.j2 @@ -0,0 +1,85 @@ +{% extends "base.html.j2" %} +{% block title %}ONAPTEST Bench{% endblock %} + +{% block content %} +<h1 class="title is-1">{{ info.title }}</h1> + +<div class="container"> + +<article class="message"> +<div class="message-header"> + <p>Results</p> +</div> +<div class="message-body"> +SECCOM recommended versions (global success rate: {{ info.mean }}): + <ul> + <li>Java: {{ info.success_rate.java }}% </li> + <li>Python: {{ info.success_rate.python }}%</li> + </ul> +</div> +</article> + +<article class="message"> + <div class="message-header"> + <p>Legend</p> + </div> + <div class="message-body"> + <div class="has-background-success">SECCOM recommended version</div> + <div class="has-background-success-light">Not the recommended version but at least the major version</div> + <div class="has-background-warning-light">Ambiguous versions but at least 1 is the SECCOM recommended version</div> + <div class="has-background-warning">Ambiguous versions but at least 1 is the major recommended version</div> + <div class="has-background-danger">Wrong Versions</div> + </div> +</article> +<br> + +<h2 class="title is-1">JAVA versions</h2> + +<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth"> + <thead> + <tr> + <th>Component</th> + <th>Versions</th> + </tr> + </thead> + <tbody> + {% for component in data %} + <tr {% if component.java_status == 4 %} class="has-background-danger" {%elif component.java_status == 0 %} class="has-background-success" {%elif component.java_status == 1 %} class="has-background-success-light" {%elif component.java_status == 2 %} class="has-background-warning-light" {%elif component.java_status == 3 %} class="has-background-warning" {% endif %}> + + {% if component.java_version is defined and component.java_version|length > 0 %} + <td>{{ component.container }}</td> + <td>{{ component.java_version}}</td> + {% endif %} + </tr> + {% endfor %} + </tbody> + </table> +</div> +<br> + +<div class="container"> +<h2 class="title is-1">Python versions</h2> + +<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth"> + <thead> + <tr> + <th>Component</th> + <th>Versions</th> + </tr> + </thead> + <tbody> + {% for component in data %} + <tr {% if component.python_status == 4 %} class="has-background-danger" {%elif component.python_status == 0 %} class="has-background-success" {%elif component.python_status == 1 %} class="has-background-success-light" {%elif component.python_status == 2 %} class="has-background-warning-light" {%elif component.python_status == 3 %} class="has-background-warning" {% endif %}> + {% if component.python_version is defined and component.python_version|length > 0 %} + <td>{{ component.container }}</td> + <td>{{ component.python_version}}</td> + {% endif %} + </tr> + {% endfor %} + </tbody> + </table> +</div> + +{% endblock %} +</div> +</section> |