Skip to content
Snippets Groups Projects
tests.py 12.7 KiB
Newer Older
#!/usr/bin/python3
"""
// © University of Southampton IT Innovation Centre, 2018
//
// Copyright in this software belongs to University of Southampton
// IT Innovation Centre of Gamma House, Enterprise Road,
// Chilworth Science Park, Southampton, SO16 7NS, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
//      Created By :            Nikolay Stanchev
//      Created Date :          14-08-2018
//      Created for Project :   FLAME
"""


# Python standard libs
from os import listdir
from os.path import isfile, join
from urllib.parse import urlparse
from yaml import load
from requests import get, delete
from toscaparser.tosca_template import ToscaTemplate

# CLMC-service imports
from clmcservice.alertsapi.utilities import adjust_tosca_definitions_import
from clmcservice.alertsapi.alerts_specification_schema import validate_clmc_alerts_specification
from clmcservice.alertsapi.views import AlertsConfigurationAPI
from clmcservice import ROOT_DIR


class TestAlertsConfigurationAPI(object):
    """
    A pytest-implementation test for the Alerts Configuration API endpoints.
    """

    @pytest.fixture(autouse=True)
    def print_fixture(self):
        """
        Fixture to adjust the printing format when running pytest with the "-s" flag - by default print messages mix up with pytest's output
        """

        print()

    @pytest.fixture()
    def app_config(self):
        """
        A fixture to implement setUp/tearDown functionality for all tests by initializing configuration structure for the web service
        """

        self.registry = testing.setUp()

        yield

        testing.tearDown()

    def test_alerts_config_tosca_parsing(self):
        """
        Tests that what we consider a valid/invalid alerts specification is successfully/unsuccessfully parsed by the TOSCA-parser.
        """

        for path_suffix, valid_expected in (("valid", True), ("invalid", False)):
            test_data_path = join(ROOT_DIR, *["resources", "tosca", "test-data", "tosca-parser", path_suffix])

            for test_file_path in listdir(test_data_path):
                alert_config_abs_path = join(test_data_path, test_file_path)

                if not isfile(alert_config_abs_path):
                    continue  # skip directories

                if not test_file_path.lower().endswith('.yaml'):
                    continue  # non-yaml files are not intended for being tested

                print(alert_config_abs_path, valid_expected)

                with open(alert_config_abs_path, 'r') as fh:
                    yaml_content = load(fh)
                    adjust_tosca_definitions_import(yaml_content)

                valid_real = True
                try:
                    ToscaTemplate(yaml_dict_tpl=yaml_content)
                except Exception:
                    valid_real = False

                assert valid_expected == valid_real, "TOSCA parser test failed for file: {0}".format(alert_config_abs_path)

    def test_alerts_config_clmc_validation(self):
        """
        Tests the custom CLMC validation of the TOSCA alerts specification.
        """

        for path_suffix, valid_expected in (("valid", True), ("invalid", False)):
            test_data_path = join(ROOT_DIR, *["resources", "tosca", "test-data", "clmc-validator", path_suffix])

            for test_file_path in listdir(test_data_path):
                alert_config_abs_path = join(test_data_path, test_file_path)

                if not isfile(alert_config_abs_path):
                    continue  # skip directories

                if not test_file_path.lower().endswith('.yaml'):
                    continue  # non-yaml files are not intended for being tested

                print(alert_config_abs_path, valid_expected)

                with open(alert_config_abs_path, 'r') as fh:
                    yaml_content = load(fh)
                    adjust_tosca_definitions_import(yaml_content)

                # do not catch exceptions here since we are testing the clmc validator, the tosca parsing is tested in the previous test method
                alert_tosca_spec = ToscaTemplate(yaml_dict_tpl=yaml_content)
                valid_real, err = validate_clmc_alerts_specification(alert_tosca_spec.tpl, include_error=True)
                assert valid_expected == valid_real, "CLMC alerts specification validator test failed for file: {0}".format(alert_config_abs_path)

    def test_alerts_config_api_post(self, app_config):
        """
        Tests the POST API endpoint of the alerts configuration API responsible for receiving alerts specifications.

        Test steps are:
            * Traverse all valid TOSCA Alerts Specifications in the
                src/service/clmcservice/resources/tosca/test-data/clmc-validator/valid and src/service/clmcservice/resources/tosca/test-data/tosca-parser/valid
            * Sending a valid TOSCA Alert Specification to the view responsible for configuring Kapacitor
            * Check that Kapacitor alerts, topics and handlers are created with the correct identifier and arguments

        :param app_config: fixture for setUp/tearDown of the web service registry
        """

        for test_folder in ("clmc-validator", "tosca-parser"):
            test_data_path = join(ROOT_DIR, *["resources", "tosca", "test-data", test_folder, "valid"])

            for test_file_path in listdir(test_data_path):
                alert_spec_abs_path = join(test_data_path, test_file_path)

                if not isfile(alert_spec_abs_path):
                    continue  # skip directories

                if not test_file_path.lower().endswith('.yaml'):
                    continue  # non-yaml files are not intended for being tested

                print("Testing file {0} in folder {1}".format(test_file_path, test_folder))

                request = testing.DummyRequest()

                with open(alert_spec_abs_path) as alert_spec:
                    sfc, sfc_instance, alert_ids, topic_handlers = extract_alert_spec_data(alert_spec)
                    alert_spec.seek(0)
                    request.POST['alert-spec'] = FieldStorageMock(test_file_path, alert_spec)  # a simple mock class is used to mimic the FieldStorage class
                    clmc_service_response = AlertsConfigurationAPI(request).post_alerts_specification()

                assert (sfc, sfc_instance) == (clmc_service_response["service_function_chain_id"], clmc_service_response["service_function_chain_instance_id"]), \
                    "Incorrect extraction of metadata for file {0}". format(test_file_path)
                # traverse through all alert IDs and check that they are created within Kapacitor
                for alert_id in alert_ids:
                    kapacitor_response = get("http://localhost:9092/kapacitor/v1/tasks/{0}".format(alert_id))
                    assert kapacitor_response.status_code == 200, "Alert with ID {0} was not created - test file {1}.".format(alert_id, test_file_path)
                    kapacitor_response_json = kapacitor_response.json()
                    assert "link" in kapacitor_response_json, "Incorrect response from kapacitor for alert with ID {0} - test file {1}".format(alert_id, test_file_path)
                    assert kapacitor_response_json["status"] == "enabled", "Alert with ID {0} was created but is disabled - test file {1}".format(alert_id, test_file_path)
                # check that all topic IDs were registered within Kapacitor
                topic_ids = list(topic_handlers.keys())
                kapacitor_response = get("http://localhost:9092/kapacitor/v1/alerts/topics")
                assert kapacitor_response.status_code == 200, "Kapacitor couldn't return the list of created topics - test file {0}".format(test_file_path)
                kapacitor_response_json = kapacitor_response.json()
                kapacitor_defined_topics = [topic["id"] for topic in kapacitor_response_json["topics"]]
                assert set(topic_ids).issubset(kapacitor_defined_topics), "Not all topic IDs were created within kapacitor - test file {0}".format(test_file_path)
                # check that all handler IDs were created and each of them is subscribed to the correct topic ID
                for topic_id in topic_handlers:
                    for handler_id, handler_url in topic_handlers[topic_id]:
                        kapacitor_response = get("http://localhost:9092/kapacitor/v1/alerts/topics/{0}/handlers/{1}".format(topic_id, handler_id))
                        assert kapacitor_response.status_code == 200, "Handler with ID {0} for topic with ID {1} doesn't exist - test file {2}".format(handler_id, topic_id, test_file_path)
                        kapacitor_response_json = kapacitor_response.json()
                        assert kapacitor_response_json["id"] == handler_id, "Incorrect ID of handler {0} in the Kapacitor response - test file {1}".format(handler_id, test_file_path)
                        assert kapacitor_response_json["kind"] == "post", "Incorrect kind of handler {0} in the Kapacitor response - test file {1}".format(handler_id, test_file_path)
                        assert kapacitor_response_json["options"]["url"], "Incorrect url of handler {0} in the Kapacitor response - test file {1}".format(handler_id, test_file_path)
                clear_kapacitor_alerts(alert_ids, topic_handlers)


class FieldStorageMock(object):

    def __init__(self, filename, file):
        """
        Used to mock the behaviour of the cgi.FieldStorage class - two attributes needed only to forward a file to the view.

        :param filename: file name
        :param file: file object
        """

        self.filename = filename
        self.file = file


def extract_alert_spec_data(alert_spec):
    """
    A utility function to extract the expected alert, handler and topic identifiers from a given alert specification.

    :param alert_spec: the alert specification file (file object)
    :return: a tuple containing sfc_id and sfc_instance_id along with a list and a dictionary of generated IDs (alert IDs (list), topic IDs linked to handler IDs (dict))
    """

    yaml_alert_spec = load(alert_spec)
    adjust_tosca_definitions_import(yaml_alert_spec)
    tosca_tpl = ToscaTemplate(yaml_dict_tpl=yaml_alert_spec)
    sfc, sfc_instance = tosca_tpl.tpl["metadata"]["sfc"], tosca_tpl.tpl["metadata"]["sfci"]

    alert_ids = []  # saves all alert IDs in a list
    topic_handlers = {}  # saves all topics in a dictionary, each topic is linked to a list of handler pairs (a handler pair consists of handler id and handler url)

    for policy in tosca_tpl.policies:
        policy_id = policy.name
        for trigger in policy.triggers:
            trigger_id = trigger.name

            topic_id = "{0}.{1}.{2}".format(sfc, sfc_instance, trigger_id)
            topic_handlers[topic_id] = []

            alert_id = "{0}.{1}.{2}.{3}".format(sfc, sfc_instance, policy_id, trigger_id)
            alert_ids.append(alert_id)

            for handler_url in trigger.trigger_tpl["action"]["implementation"]:
                handler_host = urlparse(handler_url).hostname
                handler_id = "{0}.{1}.{2}".format(policy_id, trigger_id, handler_host)
                topic_handlers[topic_id].append((handler_id, handler_url))

    return sfc, sfc_instance, alert_ids, topic_handlers


def clear_kapacitor_alerts(alert_ids, topic_handlers):
    """
    A utility function to clean up Kapacitor from the configured alerts, topics and handlers.

    :param alert_ids: the list of alert IDs to delete
    :param topic_handlers: the dictionary of topic and handlers to delete
    """

    for alert_id in alert_ids:
        kapacitor_response = delete("http://localhost:9092/kapacitor/v1/tasks/{0}".format(alert_id))  # delete alert
        assert kapacitor_response.status_code == 204

    for topic_id in topic_handlers:
        for handler_id, handler_url in topic_handlers[topic_id]:
            kapacitor_response = delete("http://localhost:9092/kapacitor/v1/alerts/topics/{0}/handlers/{1}".format(topic_id, handler_id))  # delete handler
            assert kapacitor_response.status_code == 204

        kapacitor_response = delete("http://localhost:9092/kapacitor/v1/alerts/topics/{0}".format(topic_id))  # delete topic
        assert kapacitor_response.status_code == 204