diff --git a/fim/__init__.py b/fim/__init__.py index 76d7c79..63c1827 100644 --- a/fim/__init__.py +++ b/fim/__init__.py @@ -1,2 +1,2 @@ # -__VERSION__ = "1.0.4" +__VERSION__ = "1.0.5" diff --git a/fim/graph/abc_property_graph.py b/fim/graph/abc_property_graph.py index 0a90d56..01f0789 100644 --- a/fim/graph/abc_property_graph.py +++ b/fim/graph/abc_property_graph.py @@ -42,7 +42,7 @@ from fim.slivers.delegations import Delegations, DelegationType from fim.slivers.interface_info import InterfaceSliver, InterfaceInfo from fim.slivers.base_sliver import BaseSliver -from fim.slivers.network_node import NodeSliver +from fim.slivers.network_node import NodeSliver, CompositeNodeSliver from fim.slivers.network_link import NetworkLinkSliver from fim.slivers.path_info import PathInfo, ERO from fim.slivers.network_service import NetworkServiceSliver, NetworkServiceInfo, NSLayer @@ -588,6 +588,61 @@ def interface_sliver_to_graph_properties_dict(sliver: InterfaceSliver) -> Dict[s return prop_dict + @staticmethod + def sliver_to_dict(sliver) -> Dict[str, Any]: + """ + Convert sliver to a deep dictionary. . + :param sliver: + :return: + """ + assert (issubclass(type(sliver), BaseSliver)) + + # convert to properties dict first + if type(sliver) == NodeSliver or type(sliver) == CompositeNodeSliver: + d = ABCPropertyGraph.node_sliver_to_graph_properties_dict(sliver) + # now add deep sliver stuff + # components and network services + if sliver.attached_components_info is not None: + ac = list() + for c in sliver.attached_components_info.list_devices(): + ac.append(ABCPropertyGraph.sliver_to_dict(c)) + d['components'] = ac + + if sliver.network_service_info is not None: + nss = list() + for ns in sliver.network_service_info.list_network_services(): + nss.append(ABCPropertyGraph.sliver_to_dict(ns)) + d['network_services'] = nss + + elif type(sliver) == NetworkServiceSliver: + d = ABCPropertyGraph.network_service_sliver_to_graph_properties_dict(sliver) + # now add deep sliver stuff + # interfaces + if sliver.interface_info is not None: + ii = list() + for i in sliver.interface_info.list_interfaces(): + ii.append(ABCPropertyGraph.sliver_to_dict(i)) + d['interfaces'] = ii + + elif type(sliver) == ComponentSliver: + d = ABCPropertyGraph.component_sliver_to_graph_properties_dict(sliver) + # now add deep sliver stuff + # network services + if sliver.network_service_info is not None: + nss = list() + for ns in sliver.network_service_info.list_services(): + nss.append(ABCPropertyGraph.sliver_to_dict(ns)) + d['network_services'] = nss + + elif type(sliver) == NetworkLinkSliver: + d = ABCPropertyGraph.link_sliver_to_graph_properties_dict(sliver) + elif type(sliver) == InterfaceSliver: + d = ABCPropertyGraph.interface_sliver_to_graph_properties_dict(sliver) + else: + raise RuntimeError(f'JSON Conversion for type {type(sliver)} is not supported.') + + return d + @staticmethod def set_base_sliver_properties_from_graph_properties_dict(sliver: BaseSliver, d: Dict[str, str]): """ @@ -731,6 +786,35 @@ def build_deep_node_sliver(self, *, node_id: str) -> NodeSliver: return ns + @staticmethod + def build_deep_node_sliver_from_dict(*, props: Dict[str, Any]) -> NodeSliver: + """ + Build a deep NetworkNode or other similar (e.g. + network-attached storage) sliver from a deep dictionary + :param props: + :return: + """ + # create top-level sliver + ns = ABCPropertyGraph.node_sliver_from_graph_properties_dict(props) + # find and build deep slivers of network services (if any) and components (if any) + comps = props.get('components', None) + if comps is not None and len(comps) > 0: + aci = AttachedComponentsInfo() + for c in comps: + cs = ABCPropertyGraph.build_deep_component_sliver_from_dict(props=c) + aci.add_device(cs) + ns.attached_components_info = aci + + nss = props.get('network_services', None) + if nss is not None and len(nss) > 0: + nsi = NetworkServiceInfo() + for s in nss: + nssl = ABCPropertyGraph.build_deep_ns_sliver_from_dict(props=s) + nsi.add_network_service(nssl) + ns.network_service_info = nsi + + return ns + def build_deep_ns_sliver(self, *, node_id: str) -> NetworkServiceSliver: """ Build a deep sliver for a NetworkService @@ -755,7 +839,31 @@ def build_deep_ns_sliver(self, *, node_id: str) -> NetworkServiceSliver: nss.interface_info = ifi return nss + @staticmethod + def build_deep_ns_sliver_from_dict(*, props: Dict[str, Any]) -> NetworkServiceSliver: + """ + Build a deep sliver for a NetworkService from a deep dictionary + :param props: + :return: + """ + # create top-level sliver + nss = ABCPropertyGraph.network_service_sliver_from_graph_properties_dict(props) + # find interfaces and attach + ifs = props.get('interfaces', None) + if ifs is not None and len(ifs) > 0: + ifi = InterfaceInfo() + for i in ifs: + ifsl = ABCPropertyGraph.interface_sliver_from_graph_properties_dict(i) + ifi.add_interface(ifsl) + nss.interface_info = ifi + return nss + def build_deep_component_sliver(self, *, node_id: str) -> ComponentSliver: + """ + Build a deep component slliver from graph + :param node_id: + :return: + """ clazzes, props = self.get_node_properties(node_id=node_id) if ABCPropertyGraph.CLASS_Component not in clazzes: raise PropertyGraphQueryException(node_id=node_id, graph_id=self.graph_id, @@ -773,6 +881,25 @@ def build_deep_component_sliver(self, *, node_id: str) -> ComponentSliver: cs.network_service_info = nsi return cs + @staticmethod + def build_deep_component_sliver_from_dict(*, props: Dict[str, Any]) -> ComponentSliver: + """ + Build a deep component sliver from deep dictionary + :param props: + :return: + """ + # create top-level sliver + cs = ABCPropertyGraph.component_sliver_from_graph_properties_dict(props) + # find any network services, build and attach + nss = props.get('network_services', None) + if nss is not None and len(nss) > 0: + nsi = NetworkServiceInfo() + for s in nss: + nssl = ABCPropertyGraph.build_deep_ns_sliver_from_dict(props=s) + nsi.add_network_service(nssl) + cs.network_service_info = nsi + return cs + def remove_network_node_with_components_nss_cps_and_links(self, node_id: str): """ Remove a network node, all of components and their interfaces, parent interfaces diff --git a/fim/slivers/interface_info.py b/fim/slivers/interface_info.py index faed5dd..18f7ba2 100644 --- a/fim/slivers/interface_info.py +++ b/fim/slivers/interface_info.py @@ -82,3 +82,6 @@ def get_interface(self, name: str): def get_interface_names(self): return list(self.interfaces.keys()) + + def list_interfaces(self): + return list(self.interfaces.values()) diff --git a/fim/slivers/json.py b/fim/slivers/json.py new file mode 100644 index 0000000..4810549 --- /dev/null +++ b/fim/slivers/json.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# MIT License +# +# Copyright (c) 2020 FABRIC Testbed +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Author Ilya Baldin (ibaldin@renci.org) +from typing import Dict, Any + +import json + +from .network_node import NodeSliver +from .network_service import NetworkServiceSliver +from fim.graph.abc_property_graph import ABCPropertyGraph + + +class JSONSliver: + """ + This class uses ABC Property Graph dictionary serialization + to also convert slivers to and from JSON. It is made into a + separate class to avoid circular dependencies. + """ + + @staticmethod + def sliver_to_json(sliver) -> str: + """ + Take an object that is a descendant of a BaseSliver and + convert it to JSON + :param sliver: + :return: + """ + d = ABCPropertyGraph.sliver_to_dict(sliver) + if d is not None: + return json.dumps(d) + + @staticmethod + def node_sliver_from_json(s: str) -> NodeSliver: + """ + Recover a node sliver from JSON + :param s: + :return: + """ + d = json.loads(s) + if not isinstance(d, dict): + raise RuntimeError(f'Unable to decode NodeSliver from {s=}') + return ABCPropertyGraph.build_deep_node_sliver_from_dict(props=d) + + @staticmethod + def network_service_sliver_from_json(s: str) -> NetworkServiceSliver: + """ + Recover a network service sliver from JSON + :param s: + :return: + """ + d = json.loads(s) + if not isinstance(d, dict): + raise RuntimeError(f'Unable to decode NetworkServiceSliver from {s=}') + return ABCPropertyGraph.build_deep_ns_sliver_from_dict(props=d) diff --git a/fim/slivers/network_service.py b/fim/slivers/network_service.py index c7652fe..77595d6 100644 --- a/fim/slivers/network_service.py +++ b/fim/slivers/network_service.py @@ -228,3 +228,6 @@ def get_network_service(self, name: str): def get_network_service_names(self): return list(self.network_services.keys()) + def list_services(self): + return list(self.network_services.values()) + diff --git a/fim/user/interface.py b/fim/user/interface.py index 6af6987..5304ca9 100644 --- a/fim/user/interface.py +++ b/fim/user/interface.py @@ -81,7 +81,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any, raise RuntimeError(f"Interface with this id and name {name} doesn't exist") @property - def itype(self): + def type(self): return self.get_property('type') if self.__dict__.get('topo', None) is not None else None def add_child_interface(self): diff --git a/fim/user/link.py b/fim/user/link.py index ce63e4c..b8928a4 100644 --- a/fim/user/link.py +++ b/fim/user/link.py @@ -109,7 +109,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any, self._interfaces = [Interface(node_id=tup[1], topo=topo, name=tup[0]) for tup in name_id_tuples] @property - def ltype(self): + def type(self): return self.get_property('type') if self.__dict__.get('topo', None) is not None else None @property diff --git a/fim/user/network_service.py b/fim/user/network_service.py index 102f0df..1237ece 100644 --- a/fim/user/network_service.py +++ b/fim/user/network_service.py @@ -124,7 +124,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any, self._interfaces = [Interface(node_id=tup[1], topo=topo, name=tup[0]) for tup in name_id_tuples] @property - def nstype(self): + def type(self): return self.get_property('type') if self.__dict__.get('topo', None) is not None else None @property @@ -171,6 +171,13 @@ def path_info(self, value: PathInfo): if self.__dict__.get('topo', None) is not None: self.set_property('path_info', value) + def get_sliver(self) -> NetworkServiceSliver: + """ + Get a deep sliver representation of this node from graph + :return: + """ + return self.topo.graph_model.build_deep_ns_sliver(node_id=self.node_id) + def validate_service_constraints(self, nstype, interfaces) -> Set[str]: """ Validate service constraints as encoded for each services (number of interfaces, instances, sites). @@ -219,11 +226,11 @@ def __service_guardrails(sliver: NetworkServiceSliver, interface: Interface): # - L2P2P service does not work for shared ports # - L2S2S needs to warn that it may not work if the VMs with shared ports land on the same worker if sliver.get_type() == ServiceType.L2PTP and \ - interface.itype == InterfaceType.SharedPort: + interface.type == InterfaceType.SharedPort: raise RuntimeError(f"Unable to connect interface {interface.name} to service {sliver.get_name()}: " f"L2P2P service currently doesn't support shared interfaces") if sliver.get_type() == ServiceType.L2STS and \ - interface.itype == InterfaceType.SharedPort: + interface.type == InterfaceType.SharedPort: print('WARNING: Current implementation of L2STS service does not support hairpins (connections withing the ' 'same physical port), if your VMs are assigned to the same worker node, communications between them ' 'over this service will not be possible! We recommend not using L2STS with shared ports unless you ' @@ -250,7 +257,7 @@ def __connect_interface(self, *, interface: Interface): peer_if = Interface(name=self.name + '-' + interface.name, parent_node_id=self.node_id, etype=ElementType.NEW, topo=self.topo, itype=InterfaceType.ServicePort) # link type is determined by the type of interface = L2Path for shared, Patch for Dedicated - if interface.itype == InterfaceType.SharedPort: + if interface.type == InterfaceType.SharedPort: ltype = LinkType.L2Path else: ltype = LinkType.Patch diff --git a/fim/user/node.py b/fim/user/node.py index 156c7b5..228427b 100644 --- a/fim/user/node.py +++ b/fim/user/node.py @@ -115,7 +115,7 @@ def site(self, value: str): self.set_property('site', value) @property - def ntype(self): + def type(self): return self.get_property('type') if self.__dict__.get('topo', None) is not None else None @property @@ -181,6 +181,13 @@ def direct_interfaces(self): def network_services(self): return self.__list_network_services() + def get_sliver(self) -> NodeSliver: + """ + Get a deep sliver representation of this node from graph + :return: + """ + return self.topo.graph_model.build_deep_node_sliver(node_id=self.node_id) + def get_property(self, pname: str) -> Any: """ Retrieve a node property diff --git a/fim/user/topology.py b/fim/user/topology.py index 208b7a7..c87d4c0 100644 --- a/fim/user/topology.py +++ b/fim/user/topology.py @@ -252,11 +252,11 @@ def validate(self): # have ServicePorts which peer with node ports. Validation code needs # owning nodes for each interface so we search for proper interfaces for si in service_interfaces: - if si.itype == InterfaceType.ServicePort: + if si.type == InterfaceType.ServicePort: node_interfaces.append(si.get_peer()) else: node_interfaces.append(si) - s.validate_service_constraints(s.nstype, node_interfaces) + s.validate_service_constraints(s.type, node_interfaces) def __get_node_by_name(self, name: str) -> Node: """ diff --git a/test/sliver_json_test.py b/test/sliver_json_test.py new file mode 100644 index 0000000..b84e0a8 --- /dev/null +++ b/test/sliver_json_test.py @@ -0,0 +1,38 @@ +import unittest + +import fim.user as f +from fim.slivers.json import JSONSliver +from fim.graph.abc_property_graph import ABCPropertyGraph + + +class TupleTests(unittest.TestCase): + + def testNodeAndServiceSlivers(self): + t = f.ExperimentTopology() + t.add_node(name='n1', site='RENC') + t.nodes['n1'].capacities = f.Capacities(core=1) + t.nodes['n1'].add_component(name='c1', ctype=f.ComponentType.SmartNIC, model='ConnectX-6') + d = ABCPropertyGraph.sliver_to_dict(t.nodes['n1'].get_sliver()) + t.add_node(name='n2', site='RENC') + t.nodes['n2'].add_component(name='c2', ctype=f.ComponentType.SmartNIC, model='ConnectX-6') + t.nodes['n2'].add_component(name='c3', ctype=f.ComponentType.NVME, model='P4510') + t.add_network_service(name='s1', nstype=f.ServiceType.L2Bridge, interfaces=t.interface_list) + jns = JSONSliver.sliver_to_json(t.nodes['n1'].get_sliver()) + ns1 = JSONSliver.node_sliver_from_json(jns) + jns = JSONSliver.sliver_to_json(t.nodes['n2'].get_sliver()) + ns2 = JSONSliver.node_sliver_from_json(jns) + ness = JSONSliver.sliver_to_json(t.network_services['s1'].get_sliver()) + nss1 = JSONSliver.network_service_sliver_from_json(ness) + + self.assertEqual(ns1.capacities.core, 1) + self.assertEqual(len(ns1.attached_components_info.list_devices()), 1) + self.assertEqual(len(ns2.attached_components_info.list_devices()), 2) + self.assertEqual(len(nss1.interface_info.list_interfaces()), 4) + inames = [i.resource_name for i in nss1.interface_info.list_interfaces()] + self.assertTrue('s1-c2-p1' in inames) + self.assertEqual(ns1.attached_components_info.list_devices()[0]. + network_service_info.list_services()[0]. + interface_info.list_interfaces()[0].capacities.bw, 100) + + +