diff --git a/docs/clmc-service.md b/docs/clmc-service.md index 69b1ac091d307d9094d6015a61df33043ad833cb..8721c7251ff42c9b1d7f4c493a31e3dfd37143ef 100644 --- a/docs/clmc-service.md +++ b/docs/clmc-service.md @@ -144,6 +144,15 @@ with **/clmc-service** so that the nginx reverse proxy server (listening on port } ``` +* **PUT** ***/alerts*** + + This API method can be used to send an alert specification document, which is then used by the CLMC service to create or update + alert tasks and subscribe alert handlers to those tasks in Kapacitor. For further information on the alert specification + document, please check the [CLMC Alert Specification Documentation](AlertsSpecification.md). + + The request/response format of this method is the same as the **POST /alerts** API endpoint with the only difference being that existing alert tasks + or handlers will be re-created rather than returning a duplication error. + * **GET** ***/alerts/{sfc_id}/{sfc_instance_id}*** This API method can be used to fetch all alerts that are registered for a specific service function chain instance identified diff --git a/src/service/clmcservice/alertsapi/tests.py b/src/service/clmcservice/alertsapi/tests.py index 59da0fbc681f5c5b8c66dc811bebaff4d064d9da..64683d24aa51ba2a99c2896797fb971d5acc8e3a 100644 --- a/src/service/clmcservice/alertsapi/tests.py +++ b/src/service/clmcservice/alertsapi/tests.py @@ -149,14 +149,15 @@ class TestAlertsConfigurationAPI(object): def test_alerts_config_api_post(self, app_config): """ - Tests the POST API endpoint of the alerts configuration API responsible for receiving alerts specifications. + Tests the POST API endpoint of the alerts configuration API responsible for creating alerts. Test steps are: * Traverse all valid TOSCA Alerts Specifications and TOSCA Resource Specifications in the src/service/clmcservice/resources/tosca/test-data/clmc-validator/valid and src/service/clmcservice/resources/tosca/test-data/tosca-parser/valid folders - * Send a valid TOSCA Alert Specification to the view responsible for configuring Kapacitor and creating alerts + * Send a valid TOSCA Alert Specification to the view responsible for configuring Kapacitor and creating alerts (POST request) * Check that Kapacitor alerts, topics and handlers are created with the correct identifier and arguments - * Check that the API returns the duplication errors if the same alerts specification is sent + * Check that the API returns the duplication errors if the same alerts specification is sent through a POST request + * Check that the API returns no duplication errors if the same alerts specification is sent through a PUT request * Clean up the registered alerts :param app_config: fixture for setUp/tearDown of the web service registry @@ -222,8 +223,8 @@ class TestAlertsConfigurationAPI(object): # send the same request but as a PUT method instead of POST with open(alert_spec_abs_path) as alert_spec: with open(valid_resource_spec_abs_path) as valid_resource_spec: - request.POST['alert-spec'] = FieldStorageMock(alerts_test_file, alert_spec) # a simple mock class is used to mimic the FieldStorage class - request.POST['resource-spec'] = FieldStorageMock(valid_resources_test_file, valid_resource_spec) + request.params['alert-spec'] = FieldStorageMock(alerts_test_file, alert_spec) # a simple mock class is used to mimic the FieldStorage class + request.params['resource-spec'] = FieldStorageMock(valid_resources_test_file, valid_resource_spec) clmc_service_response = AlertsConfigurationAPI(request).put_alerts_specification() # no errors are expected now, since the PUT request must update existing alerts @@ -238,6 +239,52 @@ class TestAlertsConfigurationAPI(object): # clean-up in the end of the test clean_kapacitor_alerts(alerts, kapacitor_host, kapacitor_port) + def test_alerts_config_api_put(self, app_config): + """ + Tests the PUT API endpoint of the alerts configuration API responsible for creating or updating alerts. + + Test steps are: + * Traverse all valid TOSCA Alerts Specifications and TOSCA Resource Specifications in the + src/service/clmcservice/resources/tosca/test-data/clmc-validator/valid and src/service/clmcservice/resources/tosca/test-data/tosca-parser/valid folders + * Send a valid TOSCA Alert Specification to the view responsible for configuring Kapacitor and creating/updating alerts (PUT request) + * Check that Kapacitor alerts, topics and handlers are created with the correct identifier and arguments + * Clean up the registered alerts + + :param app_config: fixture for setUp/tearDown of the web service registry + """ + + kapacitor_host = self.config.registry.settings["kapacitor_host"] + kapacitor_port = self.config.registry.settings["kapacitor_port"] + + # test all of the test files provided by the path_generator_testfiles function, ignoring the last result, which is invalid resource spec. files + for alert_spec_file_paths, valid_resource_spec_file_paths, _ in path_generator_testfiles(): + alert_spec_abs_path, alerts_test_file = alert_spec_file_paths # absolute path and name of the alerts spec. file + resource_spec_abs_path, resources_test_file = valid_resource_spec_file_paths # absolute path and name of a valid resource spec. file + + with open(alert_spec_abs_path) as alert_spec: + # extract alert configuration data + sfc, sfc_instance, alerts = extract_alert_configuration_data(alert_spec, self.config.registry.settings["sfemc_fqdn"], self.config.registry.settings["sfemc_port"]) + alert_spec.seek(0) # reset the read pointer to the beginning again (the extraction in the previous step had to read the full file) + + # send valid alert and resource spec to create the alerts with a PUT request (can be used for updating or creating) + with open(resource_spec_abs_path) as resource_spec: + request = testing.DummyRequest() + request.params['alert-spec'] = FieldStorageMock(alerts_test_file, alert_spec) # a simple mock class is used to mimic the FieldStorage class + request.params['resource-spec'] = FieldStorageMock(resources_test_file, resource_spec) + clmc_service_response = AlertsConfigurationAPI(request).put_alerts_specification() + + # no errors are expected now, since the PUT request must update existing alerts + 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(alerts_test_file) + assert "triggers_specification_errors" not in clmc_service_response, "Unexpected error was returned for triggers specification" + assert "triggers_action_errors" not in clmc_service_response, "Unexpected error was returned for handlers specification" + + # traverse through all alerts and check that they were created by the PUT request + check_kapacitor_alerts(alerts, kapacitor_host, kapacitor_port, alerts_test_file) + + # clean-up in the end of the test + clean_kapacitor_alerts(alerts, kapacitor_host, kapacitor_port) + def test_alerts_config_api_get(self, app_config): """ Tests the GET API endpoint of the alerts configuration API responsible for fetching registered alerts for a specific SFC instance. @@ -409,6 +456,7 @@ class TestAlertsConfigurationAPI(object): assert response == {"deleted_alerts": [], "deleted_handlers": []}, "Incorrect response after a second delete" +# The implementation below is for utility methods and classes used in the tests' implementation. class FieldStorageMock(object): def __init__(self, filename, file): diff --git a/src/service/clmcservice/alertsapi/views.py b/src/service/clmcservice/alertsapi/views.py index ddb1f71a47cf8177cd561639fd01fb589beac193..c984dc818cbc37b62b33d3abaae8bea229bd8aa2 100644 --- a/src/service/clmcservice/alertsapi/views.py +++ b/src/service/clmcservice/alertsapi/views.py @@ -63,6 +63,7 @@ class AlertsConfigurationAPI(object): @view_config(route_name='alerts_configuration', request_method='GET') def get_alerts_hash(self): """ + (DEPRECATED - there is a GET /alerts/<sfc>/<sfc_instance> API method) Retrieves hash value for alerts task, topic and handlers based on sfc, sfci, policy and trigger IDs """ @@ -238,8 +239,12 @@ class AlertsConfigurationAPI(object): kapacitor_host, kapacitor_port = self.request.registry.settings['kapacitor_host'], self.request.registry.settings['kapacitor_port'] - alert_spec_reference = self.request.POST.get('alert-spec') - resource_spec_reference = self.request.POST.get('resource-spec') + if patch_duplicates: # implying a PUT request + alert_spec_reference = self.request.params.get('alert-spec') + resource_spec_reference = self.request.params.get('resource-spec') + else: # implying a POST request + alert_spec_reference = self.request.POST.get('alert-spec') + resource_spec_reference = self.request.POST.get('resource-spec') # parse the resource specification file and extract the required information resource_spec_sfc, resource_spec_sfc_instance, resource_spec_policy_triggers = self._parse_resource_spec(resource_spec_reference) @@ -469,6 +474,8 @@ class AlertsConfigurationAPI(object): if patch_duplicates and get(kapacitor_api_handlers_instance_url).status_code == 200: # this will only happen if the patch_duplicates flag is set to True delete(kapacitor_api_handlers_instance_url) + # an alternative is to use the PATCH API endpoint instead of deleting and creating it again, however, + # using the PATCH method requires the task to be disabled and re-enabled for changes to take effect (more HTTP requests) response = post(kapacitor_api_handlers_url, json=kapacitor_http_request_body) response_content = response.json() capture_error = response_content.get("error", "") != "" diff --git a/src/test/clmctest/alerts/test_alerts.py b/src/test/clmctest/alerts/test_alerts.py index 9faf8b79e08e6f09980aa0d21b9cca2a08813b2e..1adaad618b61341eca90f6d5206521ec2b0aa0a2 100644 --- a/src/test/clmctest/alerts/test_alerts.py +++ b/src/test/clmctest/alerts/test_alerts.py @@ -21,9 +21,9 @@ ## Created Date : 22-08-2018 ## Created for Project : FLAME """ - +import datetime from time import sleep, strptime -from requests import post, get, delete +from requests import post, get, delete, put from os import listdir from os.path import join, dirname from json import load @@ -208,3 +208,86 @@ class TestAlerts(object): {"policy": "scale_nginx_policy", "trigger": "increase_in_running_processes", "handler": "http://172.40.231.200:9999/"}, {"policy": "deadman_policy", "trigger": "no_measurements", "handler": "http://172.40.231.200:9999/"}], \ "Incorrect list of deleted handlers" + + def test_alerts_update_request(self, rspec_config): + """ + Test is implemented using the following steps: + * Send to clmc service a POST request with TOSCA alert spec. and resource spec. files + * Send to clmc service a PUT request with TOSCA alert spec. and resource spec. files + * Check that the alerts have a "created" timestamp that is later than the timestamp of the alerts during the POST request, + implying that the alerts were re-created during the PUT request + + :param rspec_config: fixture from conftest.py + """ + + clmc_service_host = None + for host in rspec_config: + if host["name"] == "clmc-service": + clmc_service_host = host["ip_address"] + break + + # create the alerts with a POST request + print("Sending alerts specification to clmc service...") + alerts_spec = join(dirname(__file__), "alerts_test_config.yaml") + resources_spec = join(dirname(__file__), "resources_test_config.yaml") + + with open(alerts_spec, 'rb') as alerts: + with open(resources_spec, 'rb') as resources: + files = {'alert-spec': alerts, 'resource-spec': resources} + response = post("http://{0}/clmc-service/alerts".format(clmc_service_host), files=files) + assert response.status_code == 200 + clmc_service_response = response.json() + assert "triggers_specification_errors" not in clmc_service_response, "Unexpected error was returned for triggers specification" + assert "triggers_action_errors" not in clmc_service_response, "Unexpected error was returned for handlers specification" + sfc, sfc_instance = "MS_Template_1", "MS_Template_1_1" + assert (sfc, sfc_instance) == (clmc_service_response["service_function_chain_id"], clmc_service_response["service_function_chain_instance_id"]) + print("Alert spec sent successfully") + + # find the latest timestamp of the registered alerts + max_post_timestamp = 0 + tasks = get("http://{0}/kapacitor/v1/tasks".format(clmc_service_host)).json()["tasks"] + for task in tasks: + # get the configured variables of this alert + task_config = task["vars"] + # if configured for this SFC instance + if task_config["sfc"]["value"] == sfc and task_config["sfci"]["value"] == sfc_instance: + created_datestr = task["created"][:26] # ignore the timezone and only take the first 6 digits of the microseconds + task_created_timestamp = datetime.datetime.strptime(created_datestr, "%Y-%m-%dT%H:%M:%S.%f") + max_post_timestamp = max(max_post_timestamp, task_created_timestamp.timestamp()) + + print("Sleeping 2 seconds to ensure a difference between the timestamps when creating the alerts and when updating them...") + sleep(2) + + # update the alerts with a PUT request and check that the "created" metadata is updated implying that the alerts were recreated + print("Sending alerts specification to clmc service for updating...") + with open(alerts_spec, 'rb') as alerts: + with open(resources_spec, 'rb') as resources: + files = {'alert-spec': alerts, 'resource-spec': resources} + response = put("http://{0}/clmc-service/alerts".format(clmc_service_host), files=files) + assert response.status_code == 200 + clmc_service_response = response.json() + assert "triggers_specification_errors" not in clmc_service_response, "Unexpected error was returned for triggers specification" + assert "triggers_action_errors" not in clmc_service_response, "Unexpected error was returned for handlers specification" + sfc, sfc_instance = "MS_Template_1", "MS_Template_1_1" + assert (sfc, sfc_instance) == (clmc_service_response["service_function_chain_id"], clmc_service_response["service_function_chain_instance_id"]) + print("Alert spec updated successfully") + + # find the earliest timestamp of the updated alerts + min_put_timestamp = float("inf") + tasks = get("http://{0}/kapacitor/v1/tasks".format(clmc_service_host)).json()["tasks"] + for task in tasks: + # get the configured variables of this alert + task_config = task["vars"] + # if configured for this SFC instance + if task_config["sfc"]["value"] == sfc and task_config["sfci"]["value"] == sfc_instance: + created_datestr = task["created"][:26] # ignore the timezone and only take the first 6 digits of the microseconds + task_created_timestamp = datetime.datetime.strptime(created_datestr, "%Y-%m-%dT%H:%M:%S.%f") + min_put_timestamp = min(min_put_timestamp, task_created_timestamp.timestamp()) + + print("Latest timestamp during the POST request", max_post_timestamp, "Earliest timestamp during the PUT request", min_put_timestamp) + assert max_post_timestamp < min_put_timestamp, "There is an alert that wasn't updated properly with a PUT request" + + # delete the alerts with a DELETE request + with open(alerts_spec, 'rb') as alerts: + files = {'alert-spec': alerts} + delete("http://{0}/clmc-service/alerts".format(clmc_service_host), files=files)