diff --git a/docs/clmc-service.md b/docs/clmc-service.md index 8f8c1ef30dfdedf5b39a4fe5e84c25a3ec3a30a0..7a435b73172a409445ff172ee171b7fbd1a8dfa6 100644 --- a/docs/clmc-service.md +++ b/docs/clmc-service.md @@ -573,6 +573,11 @@ with **/clmc-service** so that the nginx reverse proxy server (listening on port } ``` +* **PUT** ***/graph/network*** + + This API methods provides the same functionality as the *POST /graph/network* API endpoint with the only difference being + that the properties (e.g. latency) of any existing links between network nodes that already exist will be updated. + * **DELETE** ***/graph/network*** This API method instructs CLMC to delete the network topology from its graph database. diff --git a/src/service/clmcservice/graphapi/tests.py b/src/service/clmcservice/graphapi/tests.py index 7830b4338cd51ebb17dcd97f1d63eb2efffa100a..4a8bd585fd71a823043f0db6fbc3b00918f92783 100644 --- a/src/service/clmcservice/graphapi/tests.py +++ b/src/service/clmcservice/graphapi/tests.py @@ -30,7 +30,8 @@ from pyramid import testing from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPInternalServerError from clmcservice.graphapi.views import GraphAPI from clmcservice.models import MonitoringProcess -from clmcservice.graphapi.conftest import links, sdn_switches, ues, clusters +from clmcservice.graphapi.conftest import links, sdn_switches, switches, ues, clusters +from clmcservice.graphapi.utilities import delete_network_graph class TestGraphAPI(object): @@ -494,22 +495,21 @@ class TestGraphAPI(object): mock_response3.status_code = 200 mock_response3.json.return_value = links # we are doing two calls to the API, hence need to repeat the responses - http_get_mock.side_effect = [mock_response1, mock_response2, mock_response3, mock_response1, mock_response2, mock_response3] + http_get_mock.side_effect = [mock_response1, mock_response2, mock_response3] * 2 # mock the behaviour of reading the clusters and ues mappping from files file_open_mock.return_value = MagicMock() # use magic mock so that special methods (dunders) are auto generated # we are doing two calls to the API, hence need to repeat the results - json_load_mock.side_effect = [clusters, ues, clusters, ues] + json_load_mock.side_effect = [clusters, ues] * 2 assert set([node["name"] for node in graph_db.nodes.match("Cluster")]) == set(), "Cluster nodes must not be created before the build request" assert set([node["name"] for node in graph_db.nodes.match("Switch")]) == set(), "Switch nodes must not be created before the build request" assert set([node["name"] for node in graph_db.nodes.match("UserEquipment")]) == set(), "UE nodes must not be created before the build request" - # sent request to build the network topology + # sent request to build the network topology through a POST request request = testing.DummyRequest() response = GraphAPI(request).build_network_topology() assert response == {"new_switches_count": 6, "new_clusters_count": 6, "new_ues_count": 3} - assert set([node["name"] for node in graph_db.nodes.match("Cluster")]) == set(["DC" + str(i) for i in range(1, 7)]), "Cluster nodes must have been created" assert set([node["name"] for node in graph_db.nodes.match("Switch")]) == set(["127.0.0." + str(i) for i in range(1, 7)]), "Switch nodes must have been created" assert set([node["name"] for node in graph_db.nodes.match("UserEquipment")]) == set(["ue" + str(i) for i in (2, 3, 6)]), "UE nodes must have been created" @@ -518,11 +518,85 @@ class TestGraphAPI(object): request = testing.DummyRequest() response = GraphAPI(request).build_network_topology() assert response == {"new_switches_count": 0, "new_clusters_count": 0, "new_ues_count": 0} - assert set([node["name"] for node in graph_db.nodes.match("Cluster")]) == set(["DC" + str(i) for i in range(1, 7)]) assert set([node["name"] for node in graph_db.nodes.match("Switch")]) == set(["127.0.0." + str(i) for i in range(1, 7)]) assert set([node["name"] for node in graph_db.nodes.match("UserEquipment")]) == set(["ue" + str(i) for i in (2, 3, 6)]) + # clean up + delete_network_graph(graph_db) + + @patch('clmcservice.graphapi.views.load') + @patch('clmcservice.graphapi.views.open') + @patch('clmcservice.graphapi.views.get') + def test_build_and_update_network(self, http_get_mock, file_open_mock, json_load_mock, db_testing_data): + """ + Tests the functionality to build and update the network graph. + + :param http_get_mock: mocks the HTTP GET function + :param file_open_mock: mocks the open file function + :param json_load_mock: mocks the JSON load function + :param db_testing_data: fixture used to get a reference to the graph DB + """ + + from_timestamp, to_timestamp, graph_db = db_testing_data # fixture, used to get reference to the graph DB + + # mock the responses from the sdn controller - 3 GET requests are executed, so we need 3 responses + mock_response1 = Mock() + mock_response1.status_code = 200 + mock_response1.json.return_value = sdn_switches + mock_response2 = Mock() + mock_response2.status_code = 200 + mock_response2.json.return_value = links + mock_response3 = Mock() + mock_response3.status_code = 200 + mock_response3.json.return_value = links + # we are doing two calls to the API, hence need to repeat the responses + http_get_mock.side_effect = [mock_response1, mock_response2, mock_response3] * 2 + + # mock the behaviour of reading the clusters and ues mappping from files + file_open_mock.return_value = MagicMock() # use magic mock so that special methods (dunders) are auto generated + # we are doing two calls to the API, hence need to repeat the results + json_load_mock.side_effect = [clusters, ues] * 2 + + # sent request to build the network topology through a PUT request + request = testing.DummyRequest() + response = GraphAPI(request).build_and_update_network_topology() + assert response == {"new_switches_count": 6, "new_clusters_count": 6, "new_ues_count": 3} + assert set([node["name"] for node in graph_db.nodes.match("Cluster")]) == set(["DC" + str(i) for i in range(1, 7)]), "Cluster nodes must have been created" + assert set([node["name"] for node in graph_db.nodes.match("Switch")]) == set(["127.0.0." + str(i) for i in range(1, 7)]), "Switch nodes must have been created" + assert set([node["name"] for node in graph_db.nodes.match("UserEquipment")]) == set(["ue" + str(i) for i in (2, 3, 6)]), "UE nodes must have been created" + + # fetch the first network link as test link + test_link = links[0] + test_link_source = switches[test_link["src-switch"]] + test_link_destination = switches[test_link["dst-switch"]] + test_link_source_node = graph_db.nodes.match("Switch", name=test_link_source).first() + test_link_destination_node = graph_db.nodes.match("Switch", name=test_link_destination).first() + # assert the nodes exist + assert test_link_source_node is not None + assert test_link_destination_node is not None + + # confirm the latency value of the edge + old_latency = test_link["latency"] / 1000 # convert to seconds + test_link_edge = graph_db.relationships.match(nodes=(test_link_source_node, test_link_destination_node), r_type="linkedTo").first() + # assert the edge exists + assert test_link_edge is not None + assert test_link_edge["latency"] == old_latency, "Edge was not created with the correct latency value" + + # update the test link in the SDN controller mock data with a new latency + new_latency = 5 * old_latency + test_link["latency"] = new_latency * 1000 # convert to milliseconds + + # send the same request again and ensure that the latency of the network edge is updated + request = testing.DummyRequest() + GraphAPI(request).build_and_update_network_topology() + + graph_db.pull(test_link_edge) + assert test_link_edge["latency"] == new_latency, "Edge was not updated with the correct latency value" + + # clean up + delete_network_graph(graph_db) + def test_delete_network(self, graph_network_topology, db_testing_data): """ Tests the delete network graph functionality. diff --git a/src/service/clmcservice/graphapi/utilities.py b/src/service/clmcservice/graphapi/utilities.py index 9d8dd6808a5fc16440d65bb128fa6da17fe39f51..df339df1bc8e164e1a45e7262293da7946d138d3 100644 --- a/src/service/clmcservice/graphapi/utilities.py +++ b/src/service/clmcservice/graphapi/utilities.py @@ -350,7 +350,7 @@ def delete_temporal_subgraph(graph, subgraph_id): return nodes_matched -def build_network_graph(graph, switches, links, clusters, ues): +def build_network_graph(graph, switches, links, clusters, ues, update_existing_links=True): """ A function used to build the network topology in the neo4j graph given the collection of switches, links and clusters. @@ -359,6 +359,7 @@ def build_network_graph(graph, switches, links, clusters, ues): :param links: a collection of all switch-to-switch links in the network topology - JSON format, list of objects, each object must have "src-switch", "dst-switch" and "latency" as keys :param clusters: a collection of all clusters and the IP address of the service router that they are connected to - mapping between an IP address of a service router and a cluster identifier :param ues: a collection of all ues and the IP address of the service router that they are connected to - mapping between an IP address of a servicer router and a ue identifier + :param update_existing_links: (defaults to True) a flag to indicate if existing network edges must be updated with the latest latencies """ new_switches_count = 0 @@ -391,7 +392,7 @@ def build_network_graph(graph, switches, links, clusters, ues): # create the link between the two nodes edge = find_or_create_edge(graph, "linkedTo", from_node, to_node, latency=latency) - if edge["latency"] != latency: + if update_existing_links and edge["latency"] != latency: log.info("Updating latency for edge {0}, old latency {1}, new latency {2}".format(edge, edge["latency"], latency)) edge["latency"] = latency # make sure that the latency is updated if the edge already existed graph.push(edge) # update the relationship in the DB diff --git a/src/service/clmcservice/graphapi/views.py b/src/service/clmcservice/graphapi/views.py index e89f32e9376f2b979116f86f8e1f8ca1f21949cd..f7f251582d1dee5c3b82581bd4c235af6fd6bf8f 100644 --- a/src/service/clmcservice/graphapi/views.py +++ b/src/service/clmcservice/graphapi/views.py @@ -255,11 +255,32 @@ class GraphAPI(object): @view_config(route_name='graph_network_topology', request_method='POST') def build_network_topology(self): """ - An API endpoint to build/update the network topology in the neo4j graph. + An API endpoint to build the network topology in the neo4j graph, however, without updating the latency of existing edges. :return: A JSON response with the number of switches, clusters and ues that were built. """ + return self._build_network_topology(update_existing_links=False) + + @view_config(route_name='graph_network_topology', request_method='PUT') + def build_and_update_network_topology(self): + """ + An API endpoint to build the network topology in the neo4j graph and also update the latency of existing edges. + + :return: A JSON response with the number of switches, clusters and ues that were built. + """ + + return self._build_network_topology(update_existing_links=True) + + def _build_network_topology(self, update_existing_links): + """ + A utility method used to build the network graph topology with the option of updating existing links. + + :param update_existing_links: a flag to be set to True if existing network links need to be updated + + :return: A dictionary with the number of switches, clusters and ues that were created. + """ + graph = self.get_graph_reference() sdn_controller_ip = self.request.registry.settings['sdn_controller_ip'] @@ -307,8 +328,8 @@ class GraphAPI(object): ues = {} # build the network graph and retrieve the number of switch nodes and cluster nodes that were created - tmp_switch_count, tmp_clusters_count, tmp_ues_count = build_network_graph(graph, switches, external_links, clusters, ues) - switch_count, clusters_count, ues_count = build_network_graph(graph, switches, local_links, clusters, ues) + tmp_switch_count, tmp_clusters_count, tmp_ues_count = build_network_graph(graph, switches, external_links, clusters, ues, update_existing_links=update_existing_links) + switch_count, clusters_count, ues_count = build_network_graph(graph, switches, local_links, clusters, ues, update_existing_links=update_existing_links) switch_count += tmp_switch_count clusters_count += tmp_clusters_count ues_count += tmp_ues_count