Fix security versions script 32/134232/1
authorMichal Jagiello <michal.jagiello@t-mobile.pl>
Wed, 19 Apr 2023 09:53:38 +0000 (09:53 +0000)
committerMichal Jagiello <michal.jagiello@t-mobile.pl>
Wed, 19 Apr 2023 09:58:02 +0000 (09:58 +0000)
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

19 files changed:
test/security/check_versions/.gitignore
test/security/check_versions/README.md
test/security/check_versions/env/Vagrantfile [deleted file]
test/security/check_versions/env/configuration/namespaces.yaml [deleted file]
test/security/check_versions/env/configuration/terminated.yaml [deleted file]
test/security/check_versions/env/configuration/versions.yaml [deleted file]
test/security/check_versions/env/requirements-dev.txt [deleted file]
test/security/check_versions/env/requirements.txt [deleted file]
test/security/check_versions/pyproject.toml [new file with mode: 0644]
test/security/check_versions/requirements.txt [new file with mode: 0644]
test/security/check_versions/tests/test_main.py
test/security/check_versions/tests/test_verify_versions_acceptability.py
test/security/check_versions/tox.ini
test/security/check_versions/versions/__init__.py [new file with mode: 0644]
test/security/check_versions/versions/k8s_bin_versions_inspector.py [moved from test/security/check_versions/src/k8s_bin_versions_inspector.py with 87% similarity]
test/security/check_versions/versions/k8s_bin_versions_inspector_test_case.py [new file with mode: 0644]
test/security/check_versions/versions/reporting.py [new file with mode: 0644]
test/security/check_versions/versions/templates/base.html.j2 [new file with mode: 0644]
test/security/check_versions/versions/templates/versions.html.j2 [new file with mode: 0644]

index db6444b..2b574f8 100644 (file)
@@ -1,5 +1,4 @@
 .pytest_cache/
 __pycache__/
-/env/.vagrant
 /temp/
 /.tox/
index 3934ca7..399d104 100644 (file)
@@ -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 (file)
index 9753a74..0000000
+++ /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 (file)
index f300cc7..0000000
+++ /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 (file)
index dd6ce82..0000000
+++ /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 (file)
index 75b7f7b..0000000
+++ /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 (file)
index 1ced42c..0000000
+++ /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 (file)
index e81358f..0000000
+++ /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 (file)
index 0000000..a9b5c54
--- /dev/null
@@ -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 (file)
index 0000000..8e46a3a
--- /dev/null
@@ -0,0 +1,7 @@
+kubernetes
+jinja2
+xtesting
+tabulate
+cerberus
+packaging
+wget
index 0dff0b2..37ad45e 100644 (file)
@@ -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"],
index 5e2f0d2..1cb9316 100644 (file)
@@ -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"]))
     ]
index 703ee28..d2a0071 100644 (file)
@@ -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 (file)
index 0000000..e69de29
@@ -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(
@@ -147,6 +156,13 @@ def parse_argv(argv: Optional[List[str]] = None) -> argparse.Namespace:
         help="Suppress printing text on standard output.",
     )
 
+    parser.add_argument(
+        "-w",
+        "--waiver",
+        type=pathlib.Path,
+        help="Path of the waiver xfail file.",
+    )
+
     parser.add_argument(
         "-V",
         "--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 (file)
index 0000000..87516cb
--- /dev/null
@@ -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 (file)
index 0000000..43ef26d
--- /dev/null
@@ -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 (file)
index 0000000..025c0ad
--- /dev/null
@@ -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 (file)
index 0000000..4860a72
--- /dev/null
@@ -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>