diff --git a/scripts/clmc-service/install-clmc-service.sh b/scripts/clmc-service/install-clmc-service.sh index a3755cd4ce86d7147381b0d5cfae188b41f03324..a815007293e409e745f9538b394b2f52b6c6d276 100755 --- a/scripts/clmc-service/install-clmc-service.sh +++ b/scripts/clmc-service/install-clmc-service.sh @@ -126,6 +126,7 @@ start_script_file="/opt/flame/clmc/start.sh" echo "#!/bin/bash" > $start_script_file echo "export WORKON_HOME=${HOME}/.virtualenvs" >> $start_script_file echo "export SFEMC_FQDN=${SFEMC_FQDN}" >> $start_script_file +echo "export SDN_CONTROLLER_IP=${SDN_CONTROLLER_IP}" >> $start_script_file echo "source /usr/local/bin/virtualenvwrapper.sh" >> $start_script_file echo "workon CLMC" >> $start_script_file echo "pserve ${REPO_ROOT}/src/service/production.ini &" >> $start_script_file diff --git a/scripts/test/fixture.sh b/scripts/test/fixture.sh index 2cfbae5fdf77a33fde8360058158bee569d14140..dc20be92e0b945cde9fed1c3cec247ebc2645979 100755 --- a/scripts/test/fixture.sh +++ b/scripts/test/fixture.sh @@ -77,7 +77,7 @@ create() { if [ ${service_name} == "clmc-service" ]; then cmd="${target_root}/scripts/clmc-service/install.sh" echo "Provisioning command ${cmd}" - lxc exec ${service_name} --env REPO_ROOT=${target_root} --env SFEMC_FQDN="sfemc.localhost" --env NETWORK_DEPENDENCY="network.target"-- ${cmd} + lxc exec ${service_name} --env REPO_ROOT=${target_root} --env SFEMC_FQDN="sfemc.localhost" --env SDN_CONTROLLER_IP="127.0.0.1" --env NETWORK_DEPENDENCY="network.target"-- ${cmd} exit_code=$? if [ $exit_code != 0 ]; then echo "clmc-service installation failed with exit code ${exit_code}" diff --git a/src/service/clmcservice/__init__.py b/src/service/clmcservice/__init__.py index 3d56991b194b5c23f742f7205d35fa003fa60489..c35e2ec20a48d8440e215eef52b0375582b45b17 100644 --- a/src/service/clmcservice/__init__.py +++ b/src/service/clmcservice/__init__.py @@ -49,6 +49,7 @@ def main(global_config, **settings): Base.metadata.bind = engine # bind the engine to the Base class metadata settings['sfemc_fqdn'] = os.environ['SFEMC_FQDN'] # read the SFEMC FQDN from the OS environment + settings['sdn_controller_ip'] = os.environ['SDN_CONTROLLER_IP'] # read the SDN controller IP address from the OS environment settings['influx_port'] = int(settings['influx_port']) # the influx port setting must be converted to integer instead of a string settings['kapacitor_port'] = int(settings['kapacitor_port']) # the kapacitor port setting must be converted to integer instead of a string @@ -70,6 +71,7 @@ def main(global_config, **settings): config.add_route('graph_build', '/graph/temporal') config.add_route('graph_manage', '/graph/temporal/{graph_id}') config.add_route('graph_algorithms_rtt', '/graph/temporal/{graph_id}/round-trip-time') + config.add_route('graph_network_topology', '/graph/network') # add routes of the Alerts Configuration API config.add_route('alerts_configuration', '/alerts') diff --git a/src/service/clmcservice/graphapi/conftest.py b/src/service/clmcservice/graphapi/conftest.py index 63e241deb7ef366d9b40b9d2e80129ecb878d46b..d2b11aa8b0067cfd14686a9d24633fa39d3130f9 100644 --- a/src/service/clmcservice/graphapi/conftest.py +++ b/src/service/clmcservice/graphapi/conftest.py @@ -105,7 +105,8 @@ def db_testing_data(): # create the physical infrastructure subgraph dbs = influx.get_list_database() - build_network_graph(graph, switches, links, clusters) + switch_count, cluster_count = build_network_graph(graph, switches, links, clusters) + assert switch_count == 6 and cluster_count == 6, "Network graph build failure" # check if exists ( if so, clear ) or create the test DB in influx if test_db_name in dbs: diff --git a/src/service/clmcservice/graphapi/utilities.py b/src/service/clmcservice/graphapi/utilities.py index 1459e6905c622da41bf23220cf6c6088bc429a4c..4f4dfe3e2e15cb0184ecbd24d2af3e1e0612bda5 100644 --- a/src/service/clmcservice/graphapi/utilities.py +++ b/src/service/clmcservice/graphapi/utilities.py @@ -148,12 +148,13 @@ def validate_graph_rtt_params(params): return url_params -def find_or_create_node(graph, node_type, **properties): +def find_or_create_node(graph, node_type, return_created=False, **properties): """ This function checks if a node of the given type with the given properties exists, and if not - creates it. :param graph: the graph object :param node_type: the type of the node to find or create + :param return_created: if True the result will contain both the node and a boolean flag if the node was created now :param properties: the properties of the node to find or create :return: the found or newly created node object """ @@ -163,12 +164,17 @@ def find_or_create_node(graph, node_type, **properties): else: node = graph.nodes.match(node_type, name=properties['name']).first() + created = False if node is None: log.info("Creating node of type {0} with properties {1}".format(node_type, properties)) node = Node(node_type, **properties) graph.create(node) + created = True - return node + if return_created: + return node, created + else: + return node def find_or_create_edge(graph, edge_type, from_node, to_node, **properties): @@ -314,6 +320,8 @@ def build_network_graph(graph, switches, links, clusters): :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 """ + new_switches_count = 0 + new_clusters_count = 0 for link in links: # get the DPID of the source switch source = link["src-switch"] @@ -328,18 +336,33 @@ def build_network_graph(graph, switches, links, clusters): # retrieve the latency for this link latency = link["latency"] - from_node = find_or_create_node(graph, "Switch", name=source) - to_node = find_or_create_node(graph, "Switch", name=destination) + # create or retrieve the from node + from_node, created = find_or_create_node(graph, "Switch", return_created=True, name=source) + if created: + new_switches_count += 1 + + # create or retrieve the to node + to_node, created = find_or_create_node(graph, "Switch", return_created=True, name=destination) + if created: + new_switches_count += 1 # create the link between the two nodes find_or_create_edge(graph, "linkedTo", from_node, to_node, latency=latency) + # check whether the source service router connects a particular cluster if source in clusters: cluster_name = clusters[source] - cluster_node = find_or_create_node(graph, "Cluster", name=cluster_name) + cluster_node, created = find_or_create_node(graph, "Cluster", return_created=True, name=cluster_name) + if created: + new_clusters_count += 1 find_or_create_edge(graph, "linkedTo", cluster_node, from_node, latency=0) + # check whether the destination service router connects a particular cluster if destination in clusters: cluster_name = clusters[destination] - cluster_node = find_or_create_node(graph, "Cluster", name=cluster_name) + cluster_node, created = find_or_create_node(graph, "Cluster", return_created=True, name=cluster_name) + if created: + new_clusters_count += 1 find_or_create_edge(graph, "linkedTo", cluster_node, to_node, latency=0) + + return new_switches_count, new_clusters_count diff --git a/src/service/clmcservice/graphapi/views.py b/src/service/clmcservice/graphapi/views.py index a208e7ae710b7a67fbe2652cfadf5f9c3763ef05..dfcb38af124786eca9bffe880c34642f35704f86 100644 --- a/src/service/clmcservice/graphapi/views.py +++ b/src/service/clmcservice/graphapi/views.py @@ -23,12 +23,14 @@ """ -from clmcservice.graphapi.utilities import validate_json_queries_body, validate_graph_url_params, build_temporal_subgraph, delete_temporal_subgraph, validate_graph_rtt_params, RTT_CYPHER_QUERY_TEMPLATE +from clmcservice.graphapi.utilities import validate_json_queries_body, validate_graph_url_params, \ + build_network_graph, build_temporal_subgraph, delete_temporal_subgraph, validate_graph_rtt_params, RTT_CYPHER_QUERY_TEMPLATE from uuid import uuid4 from influxdb import InfluxDBClient from py2neo import Graph -from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound +from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPServiceUnavailable, HTTPNotImplemented from pyramid.view import view_defaults, view_config +from requests import exceptions, get import logging @@ -206,3 +208,71 @@ class GraphAPI(object): forward_data_delay, reverse_data_delay = 0, 0 return forward_latency + forward_data_delay + service_delay + reverse_latency + reverse_data_delay + + @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. + + :return: A JSON response with the number of switches and clusters that were built. + """ + + graph = Graph(host=self.request.registry.settings['neo4j_host'], password=self.request.registry.settings['neo4j_password']) # connect to the neo4j graph db + + sdn_controller_ip = self.request.registry.settings['sdn_controller_ip'] + + # retrieve all switches - if SDN controller is unavailable on the given IP address return 503 Service Unavailable + try: + url = "http://{0}:8080{1}".format(sdn_controller_ip, "/wm/core/controller/switches/json") + response = get(url) + except exceptions.ConnectionError: + msg = "The SDN controller is not available on IP {0} and port 8080.".format(sdn_controller_ip) + log.error("Unexpected error: {0}".format(msg)) + raise HTTPServiceUnavailable("The SDN controller couldn't be reached when trying to build the network topology.") + + # check if the SDN controller returned the expected response + if response.status_code != 200: + msg = "The SDN controller returned a response with status code different than 200." + log.error("Unexpected error: {0}".format(msg)) + raise HTTPNotImplemented("The SDN controller failed to return a successful response when querying for the list of switches.") + + try: + content = response.json() + except ValueError: # response not in JSON + msg = "The SDN controller returned a response which couldn't be converted to JSON." + log.error("Unexpected error: {0}".format(msg)) + raise HTTPNotImplemented("The SDN controller failed to return a valid JSON response when querying for the list of switches.") + + # map the DPID of each switch to its IP address + switches = {} + for switch in content: + # map the dpid to the switch IP address, the IP address is in the format '/172.168.23.54:1234' + switches[switch["switchDPID"]] = switch["inetAddress"][1:].split(":")[0] + + # retrieve all links - if SDN controller is unavailable on the given IP address return 503 Service Unavailable + try: + url = "http://{0}:8080{1}".format(sdn_controller_ip, "/wm/topology/external-links/json") + response = get(url) + except exceptions.ConnectionError: + msg = "The SDN controller is not available on IP {0} and port 8080.".format(sdn_controller_ip) + log.error("Unexpected error: {0}".format(msg)) + raise HTTPServiceUnavailable("The SDN controller couldn't be reached when trying to build the network topology.") + + # check if the SDN controller returned the expected response + if response.status_code != 200: + msg = "The SDN controller returned a response with status code different than 200." + log.error("Unexpected error: {0}".format(msg)) + raise HTTPNotImplemented("The SDN controller failed to return a successful response when querying for the network topology.") + + try: + links = response.json() + except ValueError: # response not in JSON + msg = "The SDN controller returned a response which couldn't be converted to JSON." + log.error("Unexpected error: {0}".format(msg)) + raise HTTPNotImplemented("The SDN controller failed to return a valid JSON response when querying for the network topology.") + + clusters = {} # TODO this mapping should be retrieved somehow + # build the network graph and retrieve the number of switch nodes and cluster nodes that were created + switch_count, clusters_count = build_network_graph(graph, switches, links, clusters) + + return {"new_switches_count": switch_count, "new_clusters_count": clusters_count}