Skip to content

Commit

Permalink
Merge pull request #85 from fabric-testbed/issue-83
Browse files Browse the repository at this point in the history
Issue 83 - add ability to serialize/deserialize Node and Network Service slivers from JSON
  • Loading branch information
ibaldin authored Sep 22, 2021
2 parents 5004f74 + 9a96850 commit 9768727
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 11 deletions.
2 changes: 1 addition & 1 deletion fim/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#
__VERSION__ = "1.0.4"
__VERSION__ = "1.0.5"
129 changes: 128 additions & 1 deletion fim/graph/abc_property_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]):
"""
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions fim/slivers/interface_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
75 changes: 75 additions & 0 deletions fim/slivers/json.py
Original file line number Diff line number Diff line change
@@ -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 ([email protected])
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)
3 changes: 3 additions & 0 deletions fim/slivers/network_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

2 changes: 1 addition & 1 deletion fim/user/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion fim/user/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions fim/user/network_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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 '
Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion fim/user/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions fim/user/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
38 changes: 38 additions & 0 deletions test/sliver_json_test.py
Original file line number Diff line number Diff line change
@@ -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)



0 comments on commit 9768727

Please sign in to comment.