#!/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 : 09-07-2018 // Created for Project : FLAME """ from json import dumps import pytest from pyramid import testing from clmcservice.graphapi.views import GraphAPI from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound graph_1_id = None graph_2_id = None class TestGraphAPI(object): """ A pytest-implementation test for the Graph API endpoints. """ @pytest.fixture(autouse=True) def app_config(self): """ A fixture to implement setUp/tearDown functionality for all tests by initializing configuration structure for the web service and db connection """ self.registry = testing.setUp() self.registry.add_settings({"neo4j_host": "localhost", "neo4j_password": "admin", "influx_host": "localhost", "influx_port": 8086, "network_bandwidth": 104857600}) yield testing.tearDown() @pytest.mark.parametrize("body, from_timestamp, to_timestamp, error_msg", [ (None, None, None, "A bad request error must have been raised in case of missing request body."), ('{}', 12341412, 1234897, "A bad request error must have been raised in case of invalid request body."), ('"service_function_chain": "sfc", "service_function_chain_instance": "sfci"}', 12341412, 1234897, "A bad request error must have been raised in case of invalid request body."), ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": "{invalid_json}"}', 1528386860, 1528389860, "A bad request error must have been raised in case of invalid request body."), ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": ["nginx", "minio"]}', 1528386860, 1528389860, "A bad request error must have been raised in case of invalid request body."), ('"service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}', 1528386860, 1528389860, "A bad request error must have been raised in case of missing service function chain value in the request body"), ('"service_function_chain": "sfc", "service_function_chain_instance": "sfcinstance", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}', 1528386860, 1528389860, "A bad request error must have been raised in case of invalid sfci ID in the request body"), ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}', "not a timestamp", "not a timestamp", "A bad request error must have been raised in case of invalid URL parameters."), ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}', None, "not a timestamp", "A bad request error must have been raised in case of invalid URL parameters."), ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}', 2131212, None, "A bad request error must have been raised in case of invalid URL parameters."), ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}', 2131212, 2131212, "A bad request error must have been raised in case of a non-existing database."), ]) def test_build_error_handling(self, body, from_timestamp, to_timestamp, error_msg): """ Tests the error handling of the graph build API endpoint by passing erroneous input and confirming an HTTPBadRequest was returned. :param body: body of the request to test :param from_timestamp: the 'from' URL param :param to_timestamp: the 'to' URL param :param error_msg: the error message to pass in case of an error not being properly handled by the API endpoint (in other words, a test failure) """ request = testing.DummyRequest() if body is not None: request.body = body request.body = request.body.encode(request.charset) if from_timestamp is not None: request.params["from"] = from_timestamp if to_timestamp is not None: request.params["to"] = to_timestamp error_raised = False try: GraphAPI(request).build_temporal_graph() except HTTPBadRequest: error_raised = True assert error_raised, error_msg def test_build(self, db_testing_data): """ Tests the graph build API endpoint - it makes 2 API calls and checks that the expected graph was created (the influx data that's being used is reported to InfluxDB in the conftest file) :param db_testing_data: pair of time stamps - the from-to range of the generated influx test data, test database name and the graph db client object (this is a fixture from conftest) """ global graph_1_id, graph_2_id # these variables are used to store the ID of the graphs that were created during the execution of this test method; they are reused later when testing the delete method from_timestamp, to_timestamp, graph_db = db_testing_data ue_nodes = set([node["name"] for node in graph_db.nodes.match("UserEquipment")]) assert ue_nodes == set("ue" + str(i) for i in [1, 3, 6]), "UE nodes have not been created" dc_nodes = set([node["name"] for node in graph_db.nodes.match("Cluster")]) assert dc_nodes == set("DC" + str(i) for i in range(1, 7)), "Compute nodes must have been created by the db_testing_data fixture" # test with invalid URL parameters naming service_functions = dict(nginx={"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"}, minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)", "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"}, apache={"measurement_name": "apache", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"}) body = dumps(dict(service_function_chain="sfc", service_function_chain_instance="sfc_1", service_functions=service_functions)) request = testing.DummyRequest() request.body = body.encode(request.charset) with pytest.raises(HTTPBadRequest): GraphAPI(request).build_temporal_graph() # Create a valid build request and send it to the API endpoint service_functions = dict(nginx={"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"}, minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)", "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"}) build_json_body = dict(service_function_chain="test_sfc", service_function_chain_instance="test_sfc_premium", service_functions=service_functions) build_json_body["from"] = from_timestamp build_json_body["to"] = to_timestamp body = dumps(build_json_body) request = testing.DummyRequest() request.body = body.encode(request.charset) response = GraphAPI(request).build_temporal_graph() graph_subresponse = response.pop("graph") # remove the "from" and "to" keys, these will be returned in the graph_subresponse build_json_body.pop("from") build_json_body.pop("to") assert response == build_json_body, "Response must contain the request body" assert graph_subresponse.get("uuid") is not None, "Request UUID must be attached to the response." assert graph_subresponse["time_range"]["from"] == from_timestamp * 10**9 # timestamp returned in nanoseconds assert graph_subresponse["time_range"]["to"] == to_timestamp * 10**9 # timestamp returned in nanoseconds request_id = graph_subresponse["uuid"] graph_1_id = request_id # check that the appropriate nodes have been created sfp_names = set([node["name"] for node in graph_db.nodes.match("ServiceFunctionPackage")]) assert sfp_names == {"nginx", "minio"}, "The graph must contain 2 service function packages - nginx and minio" sf_names = set([node["name"] for node in graph_db.nodes.match("ServiceFunction")]) assert sf_names == {"nginx_1", "minio_1"}, "The graph must contain 2 service functions - nginx_1 and minio_1" endpoints = set([node["name"] for node in graph_db.nodes.match("Endpoint", uuid=request_id)]) assert endpoints == {"minio_1_ep1", "nginx_1_ep1", "nginx_1_ep2"}, "The graph must contain 3 endpoints - minio_1_ep1, nginx_1_ep1, nginx_1_ep2" sfci_names = set([node["name"] for node in graph_db.nodes.match("ServiceFunctionChainInstance")]) assert sfci_names == {"test_sfc_premium"}, "The graph must contain 1 service function chain instance - test_sfc_premium" sfc_names = set([node["name"] for node in graph_db.nodes.match("ServiceFunctionChain")]) assert sfc_names == {"test_sfc"}, "The graph must contain 1 service function chain - test_sfc" reference_node = graph_db.nodes.match("Reference", uuid=request_id, sfci="test_sfc_premium", sfc="test_sfc").first() assert reference_node is not None and reference_node["from"] == from_timestamp * 10**9 and reference_node["to"] == to_timestamp * 10**9, "Reference node must have been created" # check the appropriate edges have been created self.check_exist_relationship( ( ("minio_1_ep1", "Endpoint", "DC4", "Cluster", "hostedBy"), ("nginx_1_ep1", "Endpoint", "DC4", "Cluster", "hostedBy"), ("nginx_1_ep2", "Endpoint", "DC6", "Cluster", "hostedBy"), ("minio_1", "ServiceFunction", "minio_1_ep1", "Endpoint", "realisedBy"), ("nginx_1", "ServiceFunction", "nginx_1_ep1", "Endpoint", "realisedBy"), ("nginx_1", "ServiceFunction", "nginx_1_ep2", "Endpoint", "realisedBy"), ("minio_1", "ServiceFunction", "minio", "ServiceFunctionPackage", "instanceOf"), ("nginx_1", "ServiceFunction", "test_sfc_premium", "ServiceFunctionChainInstance", "utilizedBy"), ("minio_1", "ServiceFunction", "test_sfc_premium", "ServiceFunctionChainInstance", "utilizedBy"), ("nginx", "ServiceFunctionPackage", "test_sfc", "ServiceFunctionChain", "utilizedBy"), ("minio", "ServiceFunctionPackage", "test_sfc", "ServiceFunctionChain", "utilizedBy"), ("test_sfc_premium", "ServiceFunctionChainInstance", "test_sfc", "ServiceFunctionChain", "instanceOf"), ), graph_db, request_id ) # check endpoint nodes have the correct properties for endpoint, response_time, request_size, response_size in (("minio_1_ep1", 9, 5760, 2033), ("nginx_1_ep1", 18.2, 2260, 9660), ("nginx_1_ep2", 22.2, 35600, 6420)): endpoint_node = graph_db.nodes.match("Endpoint", name=endpoint, uuid=request_id).first() assert endpoint_node["response_time"] == response_time, "Wrong response time property of endpoint node" # approximation is used to avoid long float numbers retrieved from influx, the test case ensures the results are different enough so that approximation of +-1 is good enough for testing assert endpoint_node["request_size"] == pytest.approx(request_size, 1), "Wrong request size attribute of endpoint node" assert endpoint_node["response_size"] == pytest.approx(response_size, 1), "Wrong response size attribute of endpoint node" # send a new request for a new service function chain instance and check the new subgraph has been created service_functions = dict(minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)", "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"}, apache={"measurement_name": "apache", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"}) build_json_body = dict(service_function_chain="test_sfc", service_function_chain_instance="test_sfc_non_premium", service_functions=service_functions) build_json_body["from"] = from_timestamp build_json_body["to"] = to_timestamp body = dumps(build_json_body) request = testing.DummyRequest() request.body = body.encode(request.charset) response = GraphAPI(request).build_temporal_graph() graph_subresponse = response.pop("graph") # remove the "from" and "to" keys, these will be returned in the graph_subresponse build_json_body.pop("from") build_json_body.pop("to") assert response == build_json_body, "Response must contain the request body" assert graph_subresponse.get("uuid") is not None, "Request UUID must be attached to the response." assert graph_subresponse["time_range"]["from"] == from_timestamp * 10**9 # timestamp returned in nanoseconds assert graph_subresponse["time_range"]["to"] == to_timestamp * 10**9 # timestamp returned in nanoseconds request_id = graph_subresponse["uuid"] graph_2_id = request_id # check the new nodes have been created assert graph_db.nodes.match("ServiceFunctionPackage", name="apache").first() is not None, "Service function package apache must have been added to the graph" for sf in ("apache_1", "minio_2"): assert graph_db.nodes.match("ServiceFunction", name=sf).first() is not None, "Service function {0} must have been added to the graph".format(sf) for ep in ("minio_2_ep1", "apache_1_ep1"): assert graph_db.nodes.match("Endpoint", name=ep, uuid=request_id).first() is not None, "Endpoint {0} must have been added to the graph".format(ep) assert graph_db.nodes.match("ServiceFunctionChainInstance", name="test_sfc_non_premium").first() is not None, "Service function chain instance test_sfc_non_premium must have been added to the graph" assert graph_db.nodes.match("ServiceFunctionChain", name="test_sfc").first() is not None, "Service function chain test_sfc must have been added to the graph" reference_node = graph_db.nodes.match("Reference", uuid=request_id, sfci="test_sfc_non_premium", sfc="test_sfc").first() assert reference_node is not None and reference_node["from"] == from_timestamp * 10**9 and reference_node["to"] == to_timestamp * 10**9, "Reference node must have been created" # check the appropriate edges have been created self.check_exist_relationship( ( ("minio_2_ep1", "Endpoint", "DC5", "Cluster", "hostedBy"), ("apache_1_ep1", "Endpoint", "DC5", "Cluster", "hostedBy"), ("minio_2", "ServiceFunction", "minio_2_ep1", "Endpoint", "realisedBy"), ("apache_1", "ServiceFunction", "apache_1_ep1", "Endpoint", "realisedBy"), ("minio_2", "ServiceFunction", "minio", "ServiceFunctionPackage", "instanceOf"), ("apache_1", "ServiceFunction", "apache", "ServiceFunctionPackage", "instanceOf"), ("minio_2", "ServiceFunction", "test_sfc_non_premium", "ServiceFunctionChainInstance", "utilizedBy"), ("apache_1", "ServiceFunction", "test_sfc_non_premium", "ServiceFunctionChainInstance", "utilizedBy"), ("minio", "ServiceFunctionPackage", "test_sfc", "ServiceFunctionChain", "utilizedBy"), ("apache", "ServiceFunctionPackage", "test_sfc", "ServiceFunctionChain", "utilizedBy"), ("test_sfc_non_premium", "ServiceFunctionChainInstance", "test_sfc", "ServiceFunctionChain", "instanceOf") ), graph_db, request_id ) # check endpoint nodes have the correct properties for endpoint, response_time, request_size, response_size in (("minio_2_ep1", 7, 2998, 3610), ("apache_1_ep1", 17.6, 1480, 7860)): endpoint_node = graph_db.nodes.match("Endpoint", name=endpoint, uuid=request_id).first() assert endpoint_node["response_time"] == response_time, "Wrong response time property of endpoint node" # approximation is used to avoid long float numbers retrieved from influx, the test case ensures the results are different enough so that approximation of +-1 is good enough for testing assert endpoint_node["request_size"] == pytest.approx(request_size, 1), "Wrong request size attribute of endpoint node" assert endpoint_node["response_size"] == pytest.approx(response_size, 1), "Wrong response size attribute of endpoint node" def test_delete(self, db_testing_data): """ Tests the delete API endpoint of the Graph API - the test depends on the build test to have been passed successfully so that graph_1_id and graph_2_id have been set :param db_testing_data: pair of time stamps - the from-to range of the generated influx test data, test database name and the graph db client object (this is a fixture from conftest) """ global graph_1_id, graph_2_id from_timestamp, to_timestamp, graph_db = db_testing_data request = testing.DummyRequest() request.matchdict["graph_id"] = "invalid_graph_id" error_raised = False try: GraphAPI(request).delete_temporal_graph() except HTTPNotFound: error_raised = True assert error_raised, "HTTP Not Found error must be raised in case of unrecognized subgraph ID" # delete the graph associated with graph_1_id request = testing.DummyRequest() request.matchdict["graph_id"] = graph_1_id response = GraphAPI(request).delete_temporal_graph() assert response == {"uuid": graph_1_id, "deleted": 4}, "Incorrect response when deleting temporal graph" # delete the graph associated with graph_2_id request = testing.DummyRequest() request.matchdict["graph_id"] = graph_2_id response = GraphAPI(request).delete_temporal_graph() assert response == {"uuid": graph_2_id, "deleted": 3}, "Incorrect response when deleting temporal graph" assert len(graph_db.nodes.match("Endpoint")) == 0, "All endpoint nodes should have been deleted" assert set([node["name"] for node in graph_db.nodes.match("Cluster")]) == set(["DC" + str(i) for i in range(1, 7)]), "Compute nodes must not be deleted" assert set([node["name"] for node in graph_db.nodes.match("ServiceFunction")]) == {"nginx_1", "apache_1", "minio_1", "minio_2"}, "Service functions must not be deleted." assert set([node["name"] for node in graph_db.nodes.match("ServiceFunctionPackage")]) == {"nginx", "minio", "apache"}, "Service function packages must not be deleted" assert set([node["name"] for node in graph_db.nodes.match("ServiceFunctionChainInstance")]) == {"test_sfc_premium", "test_sfc_non_premium"}, "Service function chain instances must not be deleted" assert set([node["name"] for node in graph_db.nodes.match("ServiceFunctionChain")]) == {"test_sfc"}, "Service function chains must not be deleted" @pytest.mark.parametrize("graph_id, endpoint, startpoint, error_type, error_msg", [ ('e8cd4768-47dd-48cd-9c74-7f8926ddbad8', None, None, HTTPBadRequest, "HTTP Bad Request must be thrown in case of missing or invalid url parameters"), ('e8cd4768-47dd-48cd-9c74-7f8926ddbad8', None, "nginx", HTTPBadRequest, "HTTP Bad Request must be thrown in case of missing or invalid url parameters"), ('e8cd4768-47dd-48cd-9c74-7f8926ddbad8', "nginx_1_ep1", None, HTTPBadRequest, "HTTP Bad Request must be thrown in case of missing or invalid url parameters"), ('random-uuid', "nginx_1_ep1", "nginx", HTTPNotFound, "HTTP Not Found error must be thrown for an endpoint node with incorrect request ID"), ('random-uuid', "minio_1_ep1", "minio", HTTPNotFound, "HTTP Not Found error must be thrown for an endpoint node with incorrect request ID"), ]) def test_rtt_error_handling(self, graph_id, endpoint, startpoint, error_type, error_msg): """ Tests the error handling of the graph round trip time API endpoint - achieved by sending erroneous input in the request and verifying the appropriate error type has been returned. :param graph_id: the UUID of the subgraph :param endpoint: endpoint ID :param startpoint: the start node ID :param error_type: error type to expect as a response :param error_msg: error message in case of a test failure """ request = testing.DummyRequest() request.matchdict["graph_id"] = graph_id if endpoint is not None: request.params["endpoint"] = endpoint if startpoint is not None: request.params["startpoint"] = startpoint error_raised = False try: GraphAPI(request).run_rtt_query() except error_type: error_raised = True assert error_raised, error_msg def test_rtt(self, db_testing_data): """ Tests the rtt API endpoint of the Graph API. :param db_testing_data: pair of time stamps - the from-to range of the generated influx test data, test database name and the graph db client object (this is a fixture from conftest) """ from_timestamp, to_timestamp, graph_db = db_testing_data # create a graph to use for RTT test by using the build API endpoint service_functions = dict(nginx={"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"}, minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)", "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"}) build_json_body = dict(service_function_chain="test_sfc", service_function_chain_instance="test_sfc_premium", service_functions=service_functions) build_json_body["from"] = from_timestamp build_json_body["to"] = to_timestamp body = dumps(build_json_body) request = testing.DummyRequest() request.body = body.encode(request.charset) response = GraphAPI(request).build_temporal_graph() graph_subresponse = response.pop("graph") # remove the "from" and "to" keys, these will be returned in the graph_subresponse build_json_body.pop("from") build_json_body.pop("to") assert response == build_json_body, "Response must contain the request body" assert graph_subresponse.get("uuid") is not None, "Request UUID must be attached to the response." assert graph_subresponse["time_range"]["from"] == from_timestamp * 10**9 # timestamp returned in nanoseconds assert graph_subresponse["time_range"]["to"] == to_timestamp * 10**9 # timestamp returned in nanoseconds request_id = graph_subresponse["uuid"] # test some more error case handling of the RTT API endpoint request = testing.DummyRequest() request.matchdict["graph_id"] = request_id request.params["endpoint"] = "nginx_1_ep1" request.params["compute"] = "DC1" error_raised = False try: GraphAPI(request).run_rtt_query() except HTTPBadRequest: error_raised = True assert error_raised, "HTTP Bad Request must be thrown in case of missing or invalid url parameters" request = testing.DummyRequest() request.matchdict["graph_id"] = request_id request.params["endpoint"] = "nginx_1_ep1" request.params["startpoint"] = "DC0" error_raised = False try: GraphAPI(request).run_rtt_query() except HTTPNotFound: error_raised = True assert error_raised, "HTTP Not Found error must be thrown for non existing compute node" request = testing.DummyRequest() request.matchdict["graph_id"] = request_id request.params["endpoint"] = "apache_1_ep1" request.params["startpoint"] = "DC1" error_raised = False try: GraphAPI(request).run_rtt_query() except HTTPNotFound: error_raised = True assert error_raised, "HTTP Not Found error must be thrown for a non existing endpoint" # go through the set of input/output (expected) parameters and assert actual results match with expected ones for dc, endpoint, forward_latencies, reverse_latencies, response_time, request_size, response_size, rtt, global_tags in ( ("DC6", "nginx_1_ep2", [], [], 22.2, 35600, 6420, 22.2, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}), ("127.0.0.6", "nginx_1_ep2", [0], [0], 22.2, 35600, 6420, 22.2, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}), ("DC2", "nginx_1_ep2", [0, 7.5, 15, 4.5, 0], [0, 4.5, 15, 7.5, 0], 22.2, 35600, 6420, 78, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}), ("127.0.0.2", "nginx_1_ep2", [7.5, 15, 4.5, 0], [0, 4.5, 15, 7.5], 22.2, 35600, 6420, 78, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}), ("DC3", "nginx_1_ep1", [0, 12.5, 0], [0, 12.5, 0], 18.2, 2260, 9660, 38, {"flame_location": "DC4", "flame_sfe": "nginx_1_ep1", "flame_server": "DC4", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}), ("127.0.0.3", "nginx_1_ep1", [12.5, 0], [0, 12.5], 18.2, 2260, 9660, 38, {"flame_location": "DC4", "flame_sfe": "nginx_1_ep1", "flame_server": "DC4", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}) ): request = testing.DummyRequest() request.matchdict["graph_id"] = request_id request.params["endpoint"] = endpoint request.params["startpoint"] = dc response = GraphAPI(request).run_rtt_query() # approximation is used to avoid long float numbers retrieved from influx, the test case ensures the results are different enough so that approximation of +-1 is good enough for testing assert response.pop("round_trip_time") == pytest.approx(rtt, 1), "Incorrect RTT response" assert response == {"forward_latencies": forward_latencies, "reverse_latencies": reverse_latencies, "total_forward_latency": sum(forward_latencies), "total_reverse_latency": sum(reverse_latencies), "bandwidth": 104857600, "response_time": response_time, "global_tags": global_tags, "request_size": request_size, "response_size": response_size}, "Incorrect RTT response" # send a new request for a new service function chain to create a second subgraph to test service_functions = dict(minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)", "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"}, apache={"measurement_name": "apache", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"}) build_json_body = dict(service_function_chain="test_sfc", service_function_chain_instance="test_sfc_non_premium", service_functions=service_functions) build_json_body["from"] = from_timestamp build_json_body["to"] = to_timestamp body = dumps(build_json_body) request = testing.DummyRequest() request.body = body.encode(request.charset) response = GraphAPI(request).build_temporal_graph() graph_subresponse = response.pop("graph") # remove the "from" and "to" keys, these will be returned in the graph_subresponse build_json_body.pop("from") build_json_body.pop("to") assert response == build_json_body, "Response must contain the request body" assert graph_subresponse.get("uuid") is not None, "Request UUID must be attached to the response." assert graph_subresponse["time_range"]["from"] == from_timestamp * 10**9 # timestamp returned in nanoseconds assert graph_subresponse["time_range"]["to"] == to_timestamp * 10**9 # timestamp returned in nanoseconds request_id = graph_subresponse["uuid"] # go through the set of input/output (expected) parameters and assert actual results match with expected ones for dc, endpoint, forward_latencies, reverse_latencies, response_time, request_size, response_size, rtt, global_tags in ( ("DC5", "apache_1_ep1", [], [], 17.6, 1480, 7860, 17.6, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}), ("127.0.0.5", "apache_1_ep1", [0], [0], 17.6, 1480, 7860, 17.6, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}), ("DC5", "minio_2_ep1", [], [], 7, 2998, 3610, 7, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"}), ("127.0.0.5", "minio_2_ep1", [0], [0], 7, 2998, 3610, 7, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"}), ("DC3", "apache_1_ep1", [0, 9, 15, 0], [0, 15, 9, 0], 17.6, 1480, 7860, 64, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}), ("127.0.0.3", "apache_1_ep1", [9, 15, 0], [0, 15, 9], 17.6, 1480, 7860, 64, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}), ("DC2", "minio_2_ep1", [0, 7.5, 15, 0], [0, 15, 7.5, 0], 7, 2998, 3610, 53, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"}), ("127.0.0.2", "minio_2_ep1", [7.5, 15, 0], [0, 15, 7.5], 7, 2998, 3610, 53, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"}) ): request = testing.DummyRequest() request.matchdict["graph_id"] = request_id request.params["endpoint"] = endpoint request.params["startpoint"] = dc response = GraphAPI(request).run_rtt_query() # approximation is used to avoid long float numbers retrieved from influx, the test case ensures the results are different enough so that approximation of +-1 is good enough for testing assert response.pop("request_size") == pytest.approx(request_size, 1), "Incorrect RTT response" assert response.pop("response_size") == pytest.approx(response_size, 1), "Incorrect RTT response" assert response.pop("round_trip_time") == pytest.approx(rtt, 1), "Incorrect RTT response" assert response == {"forward_latencies": forward_latencies, "reverse_latencies": reverse_latencies, "total_forward_latency": sum(forward_latencies), "total_reverse_latency": sum(reverse_latencies), "bandwidth": 104857600, "response_time": response_time, "global_tags": global_tags}, "Incorrect RTT response" def test_delete_network_graph(self): """ Tests the delete network graph functionality. """ request = testing.DummyRequest() response = GraphAPI(request).delete_network_topology() assert response == {"deleted_switches_count": 6, "deleted_clusters_count": 6, "deleted_ues_count": 3} @staticmethod def check_exist_relationship(relationships_tuple, graph, uuid): """ Iterates through a tuple of relationships and checks that each of those exists - a utility method to be reused for testing. :param relationships_tuple: the tuple to iterate :param graph: the graph object :param uuid: the uuid of the request """ for relationship in relationships_tuple: from_node_name, from_node_type, to_node_name, to_node_type, relationship_type = relationship if from_node_type == "Endpoint": from_node = graph.nodes.match(from_node_type, name=from_node_name, uuid=uuid).first() else: from_node = graph.nodes.match(from_node_type, name=from_node_name).first() assert from_node is not None # IMPORTANT, assert the from_node exists, otherwise the py2neo RelationshipMatcher object assumes you are looking for any node (instead of raising an error) if to_node_type == "Endpoint": to_node = graph.nodes.match(to_node_type, name=to_node_name, uuid=uuid).first() else: to_node = graph.nodes.match(to_node_type, name=to_node_name).first() assert to_node is not None # IMPORTANT, assert the from_node exists, otherwise the py2neo RelationshipMatcher object assumes you are looking for any node (instead of raising an error) assert graph.relationships.match(nodes=(from_node, to_node), r_type=relationship_type).first() is not None, "Graph is missing a required relationship"