Feature 11021: Automatic generation of E2E Robot tests from templates and a reduced... 53/14753/4
authorgarciadeblas <gerardo.garciadeblas@telefonica.com>
Thu, 21 Nov 2024 00:12:53 +0000 (01:12 +0100)
committergarciadeblas <gerardo.garciadeblas@telefonica.com>
Thu, 28 Nov 2024 14:24:09 +0000 (15:24 +0100)
Change-Id: I161076669f3b1fb14179c82fc2207b9297ad0c85
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
README.md
robot-systest/autogeneration/generate_osm_test.py [new file with mode: 0755]
robot-systest/autogeneration/test_config.yaml [new file with mode: 0644]
robot-systest/autogeneration/test_template.j2 [new file with mode: 0644]

index 5ef6d1a..1c32dcb 100644 (file)
--- a/README.md
+++ b/README.md
@@ -253,6 +253,62 @@ rebot [-d <output_folder>] --merge output1.xml output2.xml ... outputN.xml
 
 More information about post-processing Robot output files [here](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#post-processing-outputs)
 
+## Autogeneration of tests
+
+There is a tool `robot-systest/autogeneration/generate_osm_test.py` that allows generating a Robot test from a YAML configuration:
+
+```bash
+$ ./generate_osm_test.py -h
+usage: generate_osm_test.py [-h] [-v] --config CONFIG [--template TEMPLATE] [--output OUTPUT]
+
+Generate OSM tests from YAML configuration file.
+
+options:
+  -h, --help           show this help message and exit
+  -v, --verbose        increase output verbosity
+  --config CONFIG      yaml configuration file to create the test
+  --template TEMPLATE  template file for rendering the test (default: test_template.j2)
+  --output OUTPUT      output file (default: standard output)
+```
+
+A YAML configuration file provides the input parameters to generate the test, such as NF and NS packages, NS instances to be created and some operations to be executed once the NS instances are created. An example can be found in `robot-systest/autogeneration/test_config.yaml`:
+
+```yaml
+documentation: "[BASIC-40] Auto-generated test"
+name: basic_40
+tags:
+  - basic_40
+  - cluster_main
+  - daily
+nfpkg:
+  - package: hackfest_basic_vnf
+    name: hackfest_basic-vnf
+  - package: hackfest_basic2_vnf
+    name: hackfest_basic2-vnf
+nspkg:
+  - package: hackfest_basic_ns
+    name: hackfest_basic-ns
+  - package: hackfest_basic2_ns
+    name: hackfest_basic2-ns
+ns:
+  - name: basic_40
+    nspkg: hackfest_basic-ns
+    config: "{vld: [ {name: mgmtnet, vim-network-name: %{VIM_MGMT_NET}} ] }"
+vnf:
+  - ns: basic_40
+    vnf_member_index: vnf
+    tests:
+      - type: ping
+      - type: ssh
+```
+
+To generate the Robot, you can run:
+
+```bash
+cd robot-systest/autogeneration/
+./generate_osm_test.py --config test_config.yaml > ../testsuite/mynewtest.robot
+```
+
 ## Built With
 
 * [Python](www.python.org/) - The language used
diff --git a/robot-systest/autogeneration/generate_osm_test.py b/robot-systest/autogeneration/generate_osm_test.py
new file mode 100755 (executable)
index 0000000..2fbbaa9
--- /dev/null
@@ -0,0 +1,150 @@
+#!/usr/bin/python3
+# Copyright ETSI Contributors and Others.
+# All Rights Reserved.
+#
+#    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_osm_test.py --config test_config.yaml
+
+import argparse
+import logging
+import os
+import yaml
+
+# from jinja2 import Environment, FileSystemLoader, select_autoescape
+from jinja2 import Template
+
+
+####################################
+# Global functions
+####################################
+def set_logger(verbose):
+    global logger
+    log_format_simple = "%(levelname)s %(message)s"
+    log_format_complete = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s"
+    log_formatter_simple = logging.Formatter(
+        log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S"
+    )
+    handler = logging.StreamHandler()
+    handler.setFormatter(log_formatter_simple)
+    logger = logging.getLogger("generate_osm_test")
+    logger.setLevel(level=logging.WARNING)
+    logger.addHandler(handler)
+    if verbose == 1:
+        logger.setLevel(level=logging.INFO)
+    elif verbose > 1:
+        log_formatter = logging.Formatter(
+            log_format_complete, datefmt="%Y-%m-%dT%H:%M:%S"
+        )
+        handler.setFormatter(log_formatter)
+        logger.setLevel(level=logging.DEBUG)
+
+
+def exit_with_error(message, code=1):
+    logger.error(message)
+    exit(code)
+
+
+def render_template(template_file, test_data, output_file=None):
+    logger.info(f"Rendering {template_file} with test data")
+
+    # Load Jinja template
+    with open(template_file, "r") as template_stream:
+        template = Template(template_stream.read())
+
+    # Render template with test_data and store it in output_file
+    output = template.render(test_data)
+    if output_file:
+        if os.path.exists(output_file):
+            exit_with_error(
+                f"Output file '{output_file}' already exists. Use a different filename or remove it."
+            )
+        with open(output_file, "w") as output_stream:
+            output_stream.write(output)
+    else:
+        print(output)
+
+
+def validate_data(test_data):
+    required_keys = {"nspkg", "ns"}
+    missing_keys = required_keys - test_data.keys()
+    if missing_keys:
+        logger.error(f"Missing required keys in YAML: {', '.join(missing_keys)}")
+        raise ValueError(f"Invalid test data structure: missing keys {missing_keys}")
+
+
+def complete_data(test_data):
+    nspkg_indexes = {nspkg["name"]: i + 1 for i, nspkg in enumerate(test_data["nspkg"])}
+    ns_indexes = {ns["name"]: i + 1 for i, ns in enumerate(test_data["ns"])}
+    for ns in test_data["ns"]:
+        if ns["nspkg"] not in nspkg_indexes:
+            logger.error(
+                f"nspkg '{ns['nspkg']}' referenced in 'ns' not found in 'nspkg'"
+            )
+            raise ValueError(f"Invalid nspkg name: {ns['nspkg']}")
+        ns["nspkg_index"] = nspkg_indexes[ns["nspkg"]]
+    for vnf in test_data["vnf"]:
+        if vnf["ns"] not in ns_indexes:
+            logger.error(f"ns '{vnf['ns']}' referenced in 'vnf' not found in 'ns'")
+            raise ValueError(f"Invalid ns name: {vnf['ns']}")
+        vnf["ns_index"] = ns_indexes[vnf["ns"]]
+
+
+####################################
+# Main
+####################################
+if __name__ == "__main__":
+    # Argument parse
+    parser = argparse.ArgumentParser(
+        description="Generate OSM tests from YAML configuration file."
+    )
+    parser.add_argument(
+        "-v", "--verbose", action="count", default=0, help="increase output verbosity"
+    )
+    # parser.add_argument("--basedir", default=".", help="basedir for the test")
+    parser.add_argument(
+        "--config", required=True, help="yaml configuration file to create the test"
+    )
+    parser.add_argument(
+        "--template",
+        default="test_template.j2",
+        help="template file for rendering the test (default: test_template.j2)",
+    )
+    parser.add_argument(
+        "--output", default=None, help="output file (default: standard output)"
+    )
+    args = parser.parse_args()
+
+    # Calculate paths relative to the script directory
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    config_path = os.path.abspath(args.config)
+    template_path = os.path.join(script_dir, args.template)
+
+    # Initialize logger
+    set_logger(args.verbose)
+
+    # Load test_data
+    try:
+        with open(config_path, "r") as yaml_stream:
+            test_data = yaml.safe_load(yaml_stream)
+    except FileNotFoundError:
+        exit_with_error(f"Configuration file '{args.config}' not found.")
+    except yaml.YAMLError as e:
+        exit_with_error(f"Error parsing YAML file '{args.config}': {e}")
+    validate_data(test_data)
+    complete_data(test_data)
+    logger.debug(f"Test data:\n{yaml.safe_dump(test_data)}")
+    if not os.path.exists(template_path):
+        exit_with_error(f"Template file '{template_path}' not found.")
+    render_template(template_path, test_data, args.output)
diff --git a/robot-systest/autogeneration/test_config.yaml b/robot-systest/autogeneration/test_config.yaml
new file mode 100644 (file)
index 0000000..3f21136
--- /dev/null
@@ -0,0 +1,37 @@
+#   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.
+documentation: "[BASIC-40] Auto-generated test"
+name: basic_40
+tags:
+  - basic_40
+  - cluster_main
+  - daily
+nfpkg:
+  - package: hackfest_basic_vnf
+    name: hackfest_basic-vnf
+  - package: hackfest_basic2_vnf
+    name: hackfest_basic2-vnf
+nspkg:
+  - package: hackfest_basic_ns
+    name: hackfest_basic-ns
+  - package: hackfest_basic2_ns
+    name: hackfest_basic2-ns
+ns:
+  - name: basic_40
+    nspkg: hackfest_basic-ns
+    config: "{vld: [ {name: mgmtnet, vim-network-name: %{VIM_MGMT_NET}} ] }"
+vnf:
+  - ns: basic_40
+    vnf_member_index: vnf
+    tests:
+      - type: ping
+      - type: ssh
diff --git a/robot-systest/autogeneration/test_template.j2 b/robot-systest/autogeneration/test_template.j2
new file mode 100644 (file)
index 0000000..8ebc7bc
--- /dev/null
@@ -0,0 +1,139 @@
+*** Comments ***
+#   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.
+
+
+*** Settings ***
+Documentation   {{ documentation }}
+
+Library   OperatingSystem
+Library   String
+Library   Collections
+Library   Process
+Library   SSHLibrary
+
+Resource   ../lib/vnfd_lib.resource
+Resource   ../lib/nsd_lib.resource
+Resource   ../lib/ns_lib.resource
+Resource   ../lib/connectivity_lib.resource
+Resource   ../lib/ssh_lib.resource
+
+Test Tags   {% for tag in tags %}{{ tag }}{% if not loop.last %}   {% endif %}{% endfor %}
+
+Suite Teardown   Run Keyword And Ignore Error   Suite Cleanup
+
+
+*** Variables ***
+# NF package folders and names
+{% for item in nfpkg -%}
+${NFPKG{{ loop.index }}_FOLDER}   {{ item.package }}
+${NFPKG{{ loop.index }}_NAME}   {{ item.name }}
+{% endfor %}
+# NS package folders and names
+{% for item in nspkg -%}
+${NSPKG{{ loop.index }}_FOLDER}   {{ item.package }}
+${NSPKG{{ loop.index }}_NAME}   {{ item.name }}
+{% endfor %}
+# NS instance name and configuration
+{% for item in ns -%}
+${NS{{ loop.index }}_NAME}   {{ item.name }}
+${NS{{ loop.index }}_CONFIG}   {{ item.config }}
+{% endfor %}
+# SSH keys and username to be used
+${PUBLICKEY}   %{HOME}/.ssh/id_rsa.pub
+${PRIVATEKEY}   %{HOME}/.ssh/id_rsa
+${USERNAME}   ubuntu
+${PASSWORD}   ${EMPTY}
+
+{% for item in ns -%}
+${NS{{ loop.index }}_ID}   ${EMPTY}
+{%- endfor %}
+{% for item in vnf -%}
+${VNF{{ loop.index }}_IP_ADDR}   ${EMPTY}
+${VNF{{ loop.index }}_MEMBER_INDEX}   {{ item.vnf_member_index }}
+${VNF{{ loop.index }}_NS_NAME}   {{ item.ns }}
+{%- endfor %}
+
+
+*** Test Cases ***
+{%- for item in nfpkg %}
+Add NF Package {{ item.name }}
+    [Documentation]   Upload NF package {{ item.name }} for the testsuite.
+    Create VNFD   '%{PACKAGES_FOLDER}/${NFPKG{{ loop.index }}_FOLDER}'
+{% endfor %}
+{%- for item in nspkg %}
+Add NS Package {{ item.name }}
+    [Documentation]   Upload NS package {{ item.name }} for the testsuite.
+    Create NSD   '%{PACKAGES_FOLDER}/${NSPKG{{ loop.index }}_FOLDER}'
+{% endfor %}
+{%- for item in ns %}
+Network Service Instance {{ item.name }}
+    [Documentation]   Instantiate NS {{ item.name }} for the testsuite.
+    ${id}=   Create Network Service   ${NSPKG{{ item.nspkg_index }}_NAME}   %{VIM_TARGET}   ${NS{{ loop.index }}_NAME}   ${NS{{ loop.index }}_CONFIG}   ${PUBLICKEY}
+    Set Suite Variable   ${NS{{ loop.index }}_ID}   ${id}
+{% endfor %}
+{%- for item in vnf %}
+Get Vnf Ip Address
+    [Documentation]   Get the mgmt IP address of the VNF {{ item.vnf_member_index }} of the NS {{ item.ns }}.
+    ${ip_addr}=   Get Vnf Management Ip Address   ${NS{{ item.ns_index }}_ID}   ${VNF{{ loop.index }}_MEMBER_INDEX}
+    Log   ${ip_addr}
+    Set Suite Variable   ${VNF{{ loop.index }}_IP_ADDR}   ${ip_addr}
+{% for item2 in item.tests %}
+{#- Check if "ping" test exists for this VNF -#}
+{% if item2.type == 'ping' %}
+Test Ping
+    [Documentation]   Test that the mgmt IP address of the VNF {{ item.vnf_member_index }} of the NS {{ item.ns }} is reachable with ping.
+    Test Connectivity   ${VNF{{ loop.index }}_IP_ADDR}
+{% endif %}
+{#- Check if "ssh" test exists for this VNF -#}
+{% if item2.type == 'ssh' %}
+Test SSH Access
+    [Documentation]   Check that the VNF {{ item.vnf_member_index }} of the NS {{ item.ns }} is accessible via SSH in its mgmt IP address.
+    Sleep   30s   Waiting ssh daemon to be up
+    Test SSH Connection   ${VNF{{ loop.index }}_IP_ADDR}   ${USERNAME}   ${PASSWORD}   ${PRIVATEKEY}
+{%- endif %}
+{%- endfor %}
+{%- endfor %}
+
+{% for item in ns -%}
+Delete NS Instance {{ item.name }}
+    [Documentation]   Delete NS instance {{ item.name }}.
+    [Tags]   cleanup
+    Delete NS   ${NS{{ loop.index }}_NAME}
+{% endfor %}
+
+{%- for item in nspkg %}
+Delete NS Package {{ item.name }}
+    [Documentation]   Delete NS package {{ item.name }} from OSM.
+    [Tags]   cleanup
+    Delete NSD   ${NSPKG{{ loop.index }}_NAME}
+{% endfor %}
+
+{%- for item in nfpkg %}
+Delete NF Package {{ item.name }}
+    [Documentation]   Delete NF package {{ item.name }} from OSM.
+    [Tags]   cleanup
+    Delete VNFD   ${NFPKG{{ loop.index }}_NAME}
+{% endfor %}
+
+*** Keywords ***
+Suite Cleanup
+    [Documentation]   Test Suit Cleanup: Deleting packages and NS instances
+    {%- for item in ns %}
+    Run Keyword If Any Tests Failed   Delete NS   ${NS{{ loop.index }}_NAME}
+    {%- endfor -%}
+    {%- for item in nspkg %}
+    Run Keyword If Any Tests Failed   Delete NSD   ${NSPKG{{ loop.index }}_NAME}
+    {%- endfor -%}
+    {%- for item in nfpkg %}
+    Run Keyword If Any Tests Failed   Delete VNFD   ${NFPKG{{ loop.index }}_NAME}
+    {%- endfor %}