Repository: fabric-testbed/InformationModel (language: python)

Issue:
Add support for new network service types
Update sliver models as well as base graph models 
Add support for new network service types
Update sliver models as well as base graph models 


Discussion / hints:

Fix patch:
diff --git a/fim/graph/abc_property_graph.py b/fim/graph/abc_property_graph.py
index 6dd688e..0b97292 100644
--- a/fim/graph/abc_property_graph.py
+++ b/fim/graph/abc_property_graph.py
@@ -44,7 +44,8 @@
 from fim.slivers.base_sliver import BaseSliver
 from fim.slivers.network_node import NodeSliver
 from fim.slivers.network_link import NetworkLinkSliver
-from fim.slivers.switch_fabric import SwitchFabricSliver, SwitchFabricInfo
+from fim.slivers.path_info import PathInfo, ERO
+from fim.slivers.network_service import NetworkServiceSliver, NetworkServiceInfo
 from fim.graph.abc_property_graph_constants import ABCPropertyGraphConstants
 
 
@@ -86,7 +87,10 @@ class ABCPropertyGraph(ABCPropertyGraphConstants):
         "technology": ABCPropertyGraphConstants.PROP_TECHNOLOGY,
         "model": ABCPropertyGraphConstants.PROP_MODEL,
         "node_map": ABCPropertyGraphConstants.PROP_NODE_MAP,
-        "structural_info": ABCPropertyGraphConstants.PROP_STRUCTURAL_INFO
+        "structural_info": ABCPropertyGraphConstants.PROP_STRUCTURAL_INFO,
+        "ero": ABCPropertyGraphConstants.PROP_ERO,
+        "path_info": ABCPropertyGraphConstants.PROP_PATH_INFO,
+        "controller_url": ABCPropertyGraphConstants.PROP_CONTROLLER_URL
     }
 
     @abstractmethod
@@ -231,6 +235,17 @@ def list_all_node_ids(self) -> List[str]:
         :return:
         """
 
+    @abstractmethod
+    def check_node_unique(self, *, label: str, name: str) -> bool:
+        """
+        Check no other node of this class/label and name exists. Doesn't
+        apply universally as e.g. Component names are only unique
+        within node, interface names are only unique within SF.
+        :param label:
+        :param name:
+        :return:
+        """
+
     @abstractmethod
     def get_all_nodes_by_class(self, *, label: str) -> List[str]:
         """
@@ -239,6 +254,27 @@ def get_all_nodes_by_class(self, *, label: str) -> List[str]:
         :return:
         """
 
+    @abstractmethod
+    def get_all_nodes_by_class_and_type(self, *, label: str, ntype: str) -> List[str]:
+        """
+        Get a list of nodes of the class and type
+        :param label:
+        :param ntype:
+        :return:
+        """
+
+    def get_all_network_nodes(self) -> List[str]:
+
+        return self.get_all_nodes_by_class(label=ABCPropertyGraphConstants.CLASS_NetworkNode)
+
+    def get_all_network_links(self) -> List[str]:
+
+        return self.get_all_nodes_by_class(label=ABCPropertyGraphConstants.CLASS_Link)
+
+    def get_all_network_service_nodes(self) -> List[str]:
+
+        return self.get_all_nodes_by_class(label=ABCPropertyGraphConstants.CLASS_NetworkService )
+
     @abstractmethod
     def serialize_graph(self, format: GraphFormat = GraphFormat.GRAPHML) -> str:
         """
@@ -512,15 +548,25 @@ def component_sliver_to_graph_properties_dict(sliver: ComponentSliver) -> Dict[s
         return prop_dict
 
     @staticmethod
-    def switch_fabric_sliver_to_graph_properties_dict(sliver: SwitchFabricSliver) -> Dict[str, str]:
+    def network_service_sliver_to_graph_properties_dict(sliver: NetworkServiceSliver) -> Dict[str, str]:
         """
-        This method knows how to map switch fabric sliver fields to graph properties
+        This method knows how to map network service sliver fields to graph properties
         :param sliver:
         :return:
         """
         prop_dict = ABCPropertyGraph.base_sliver_to_graph_properties_dict(sliver)
         if sliver.layer is not None:
             prop_dict[ABCPropertyGraph.PROP_LAYER] = str(sliver.layer)
+        if sliver.technology is not None:
+            prop_dict[ABCPropertyGraph.PROP_TECHNOLOGY] = str(sliver.technology)
+        if sliver.allocation_constraints is not None:
+            prop_dict[ABCPropertyGraph.PROP_ALLOCATION_CONSTRAINTS] = sliver.allocation_constraints
+        if sliver.ero is not None:
+            prop_dict[ABCPropertyGraph.PROP_ERO] = sliver.ero.to_json()
+        if sliver.path_info is not None:
+            prop_dict[ABCPropertyGraph.PROP_PATH_INFO] = sliver.path_info.to_json()
+        if sliver.controller_url is not None:
+            prop_dict[ABCPropertyGraph.PROP_CONTROLLER_URL] = sliver.controller_url
 
         return prop_dict
 
@@ -535,6 +581,41 @@ def interface_sliver_to_graph_properties_dict(sliver: InterfaceSliver) -> Dict[s
 
         return prop_dict
 
+    @staticmethod
+    def set_base_sliver_properties_from_graph_properties_dict(sliver: BaseSliver, d: Dict[str, str]):
+        """
+        Sets base sliver properties on existing sliver
+        :param sliver:
+        :param d:
+        :return:
+        """
+        sliver.set_properties(name=d.get(ABCPropertyGraph.PROP_NAME, None),
+                              type=sliver.type_from_str(d.get(ABCPropertyGraph.PROP_TYPE, None)),
+                              model=d.get(ABCPropertyGraphConstants.PROP_MODEL, None),
+                              capacities=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITIES, None)),
+                              labels=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABELS, None)),
+                              capacity_delegations=Delegations.from_json(atype=DelegationType.CAPACITY,
+                                                                         json_str=d.get(
+                                                                             ABCPropertyGraph.PROP_CAPACITY_DELEGATIONS,
+                                                                             None)),
+                              label_delegations=Delegations.from_json(atype=DelegationType.LABEL,
+                                                                      json_str=d.get(
+                                                                          ABCPropertyGraph.PROP_LABEL_DELEGATIONS,
+                                                                          None)),
+                              capacity_allocations=Capacities.from_json(
+                                  d.get(ABCPropertyGraph.PROP_CAPACITY_ALLOCATIONS,
+                                        None)),
+                              label_allocations=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABEL_ALLOCATIONS,
+                                                                       None)),
+                              structural_info=StructuralInfo.from_json(
+                                  d.get(ABCPropertyGraph.PROP_STRUCTURAL_INFO, None)),
+                              reservation_info=ReservationInfo.from_json(d.get(ABCPropertyGraph.PROP_RESERVATION_INFO,
+                                                                               None)),
+                              details=d.get(ABCPropertyGraph.PROP_DETAILS, None),
+                              node_map=json.loads(d[ABCPropertyGraph.PROP_NODE_MAP]) if
+                              d.get(ABCPropertyGraph.PROP_NODE_MAP, None) is not None else None,
+                              stitch_node=json.loads(d[ABCPropertyGraph.PROP_STITCH_NODE]))
+
     @staticmethod
     def node_sliver_from_graph_properties_dict(d: Dict[str, str]) -> NodeSliver:
         n = NodeSliver()
@@ -543,61 +624,22 @@ def node_sliver_from_graph_properties_dict(d: Dict[str, str]) -> NodeSliver:
             image_type = None
         else:
             image_ref, image_type = d[ABCPropertyGraph.PROP_IMAGE_REF].split(',')
-        n.set_properties(name=d.get(ABCPropertyGraph.PROP_NAME, None),
-                         type=n.type_from_str(d.get(ABCPropertyGraph.PROP_TYPE, None)),
-                         model=d.get(ABCPropertyGraphConstants.PROP_MODEL, None),
-                         capacities=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITIES, None)),
-                         labels=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABELS, None)),
-                         capacity_allocations=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITY_ALLOCATIONS,
-                                                                           None)),
-                         label_allocations=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABEL_ALLOCATIONS,
-                                                                    None)),
-                         reservation_info=ReservationInfo.from_json(d.get(ABCPropertyGraph.PROP_RESERVATION_INFO,
-                                                                            None)),
-                         structural_info=StructuralInfo.from_json(d.get(ABCPropertyGraph.PROP_STRUCTURAL_INFO, None)),
-                         capacity_delegations=Delegations.from_json(atype=DelegationType.CAPACITY,
-                             json_str=d.get(ABCPropertyGraph.PROP_CAPACITY_DELEGATIONS, None)),
-                         label_delegations=Delegations.from_json(atype=DelegationType.LABEL,
-                             json_str=d.get(ABCPropertyGraph.PROP_LABEL_DELEGATIONS, None)),
-                         site=d.get(ABCPropertyGraph.PROP_SITE, None),
+        ABCPropertyGraph.set_base_sliver_properties_from_graph_properties_dict(n, d)
+        n.set_properties(site=d.get(ABCPropertyGraphConstants.PROP_SITE, None),
                          image_ref=image_ref,
                          image_type=image_type,
                          management_ip=d.get(ABCPropertyGraph.PROP_MGMT_IP, None),
                          allocation_constraints=d.get(ABCPropertyGraph.PROP_ALLOCATION_CONSTRAINTS, None),
                          service_endpoint=d.get(ABCPropertyGraph.PROP_SERVICE_ENDPOINT, None),
-                         details=d.get(ABCPropertyGraph.PROP_DETAILS, None),
-                         node_map=json.loads(d[ABCPropertyGraph.PROP_NODE_MAP]) if
-                         d.get(ABCPropertyGraph.PROP_NODE_MAP, None) is not None else None,
-                         stitch_node=json.loads(d[ABCPropertyGraph.PROP_STITCH_NODE])
                          )
         return n
 
     @staticmethod
     def link_sliver_from_graph_properties_dict(d: Dict[str, str]) -> NetworkLinkSliver:
         n = NetworkLinkSliver()
-        n.set_properties(name=d.get(ABCPropertyGraph.PROP_NAME, None),
-                         type=n.type_from_str(d.get(ABCPropertyGraph.PROP_TYPE, None)),
-                         model=d.get(ABCPropertyGraphConstants.PROP_MODEL, None),
-                         capacities=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITIES, None)),
-                         labels=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABELS, None)),
-                         capacity_allocations=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITY_ALLOCATIONS,
-                                                                           None)),
-                         label_allocations=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABEL_ALLOCATIONS,
-                                                                    None)),
-                         reservation_info=ReservationInfo.from_json(d.get(ABCPropertyGraph.PROP_RESERVATION_INFO,
-                                                                            None)),
-                         structural_info=StructuralInfo.from_json(d.get(ABCPropertyGraph.PROP_STRUCTURAL_INFO, None)),
-                         capacity_delegations=Delegations.from_json(atype=DelegationType.CAPACITY,
-                             json_str=d.get(ABCPropertyGraph.PROP_CAPACITY_DELEGATIONS, None)),
-                         label_delegations=Delegations.from_json(atype=DelegationType.LABEL,
-                             json_str=d.get(ABCPropertyGraph.PROP_LABEL_DELEGATIONS, None)),
-                         allocation_constraints=d.get(ABCPropertyGraph.PROP_ALLOCATION_CONSTRAINTS, None),
-                         details=d.get(ABCPropertyGraph.PROP_DETAILS, None),
-                         layer=d.get(ABCPropertyGraph.PROP_LAYER),
+        ABCPropertyGraph.set_base_sliver_properties_from_graph_properties_dict(n, d)
+        n.set_properties(layer=d.get(ABCPropertyGraph.PROP_LAYER),
                          technology=d.get(ABCPropertyGraph.PROP_TECHNOLOGY),
-                         node_map=json.loads(d[ABCPropertyGraph.PROP_NODE_MAP]) if
-                         d.get(ABCPropertyGraph.PROP_NODE_MAP, None) is not None else None,
-                         stitch_node=json.loads(d[ABCPropertyGraph.PROP_STITCH_NODE])
                          )
         return n
 
@@ -609,54 +651,25 @@ def component_sliver_from_graph_properties_dict(d: Dict[str, str]) -> ComponentS
         :return:
         """
         cs = ComponentSliver()
-        cs.set_properties(name=d.get(ABCPropertyGraph.PROP_NAME, None),
-                          type=cs.type_from_str(d.get(ABCPropertyGraph.PROP_TYPE, None)),
-                          model=d.get(ABCPropertyGraphConstants.PROP_MODEL, None),
-                          capacities=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITIES, None)),
-                          labels=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABELS, None)),
-                          capacity_delegations=Delegations.from_json(atype=DelegationType.CAPACITY,
-                              json_str=d.get(ABCPropertyGraph.PROP_CAPACITY_DELEGATIONS, None)),
-                          label_delegations=Delegations.from_json(atype=DelegationType.LABEL,
-                              json_str=d.get(ABCPropertyGraph.PROP_LABEL_DELEGATIONS, None)),
-                          capacity_allocations=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITY_ALLOCATIONS,
-                                                                            None)),
-                          label_allocations=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABEL_ALLOCATIONS,
-                                                                     None)),
-                          details=d.get(ABCPropertyGraph.PROP_DETAILS, None),
-                          node_map=json.loads(d[ABCPropertyGraph.PROP_NODE_MAP]) if
-                          d.get(ABCPropertyGraph.PROP_NODE_MAP, None) is not None else None,
-                          stitch_node=json.loads(d[ABCPropertyGraph.PROP_STITCH_NODE])
-                          )
+        ABCPropertyGraph.set_base_sliver_properties_from_graph_properties_dict(cs, d)
         return cs
 
     @staticmethod
-    def switch_fabric_sliver_from_graph_properties_dict(d: Dict[str, str]) -> SwitchFabricSliver:
+    def network_service_sliver_from_graph_properties_dict(d: Dict[str, str]) -> NetworkServiceSliver:
         """
-        SwitchFabric sliver from node graph properties
+        NetworkService sliver from node graph properties
         :param d:
         :return:
         """
-        sf = SwitchFabricSliver()
-        sf.set_properties(name=d.get(ABCPropertyGraph.PROP_NAME, None),
-                          type=sf.type_from_str(d.get(ABCPropertyGraph.PROP_TYPE, None)),
-                          model=d.get(ABCPropertyGraphConstants.PROP_MODEL, None),
-                          capacities=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITIES, None)),
-                          labels=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABELS, None)),
-                          capacity_delegations=Delegations.from_json(atype=DelegationType.CAPACITY,
-                              json_str=d.get(ABCPropertyGraph.PROP_CAPACITY_DELEGATIONS, None)),
-                          label_delegations=Delegations.from_json(atype=DelegationType.LABEL,
-                              json_str=d.get(ABCPropertyGraph.PROP_LABEL_DELEGATIONS, None)),
-                          capacity_allocations=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITY_ALLOCATIONS,
-                                                                            None)),
-                          label_allocations=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABEL_ALLOCATIONS,
-                                                                     None)),
-                          layer=sf.layer_from_str(d.get(ABCPropertyGraph.PROP_LAYER, None)),
-                          details=d.get(ABCPropertyGraph.PROP_DETAILS, None),
-                          node_map=json.loads(d[ABCPropertyGraph.PROP_NODE_MAP]) if
-                          d.get(ABCPropertyGraph.PROP_NODE_MAP, None) is not None else None,
-                          stitch_node=json.loads(d[ABCPropertyGraph.PROP_STITCH_NODE])
-                          )
-        return sf
+        ns = NetworkServiceSliver()
+        ABCPropertyGraph.set_base_sliver_properties_from_graph_properties_dict(ns, d)
+        ns.set_properties(layer=d.get(ABCPropertyGraph.PROP_LAYER),
+                          technology=d.get(ABCPropertyGraph.PROP_TECHNOLOGY),
+                          allocation_constraints=d.get(ABCPropertyGraph.PROP_ALLOCATION_CONSTRAINTS, None),
+                          ero=ERO.from_json(d.get(ABCPropertyGraph.PROP_ERO, None)),
+                          path_info=PathInfo.from_json(d.get(ABCPropertyGraph.PROP_PATH_INFO, None)),
+                          controller_url=d.get(ABCPropertyGraph.PROP_CONTROLLER_URL, None))
+        return ns
 
     @staticmethod
     def interface_sliver_from_graph_properties_dict(d: Dict[str, str]) -> InterfaceSliver:
@@ -666,24 +679,7 @@ def interface_sliver_from_graph_properties_dict(d: Dict[str, str]) -> InterfaceS
         :return:
         """
         isl = InterfaceSliver()
-        isl.set_properties(name=d.get(ABCPropertyGraph.PROP_NAME, None),
-                           type=isl.type_from_str(d.get(ABCPropertyGraph.PROP_TYPE, None)),
-                           model=d.get(ABCPropertyGraphConstants.PROP_MODEL, None),
-                           capacities=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITIES, None)),
-                           labels=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABELS, None)),
-                           capacity_delegations=Delegations.from_json(atype=DelegationType.CAPACITY,
-                               json_str=d.get(ABCPropertyGraph.PROP_CAPACITY_DELEGATIONS, None)),
-                           label_delegations=Delegations.from_json(atype=DelegationType.LABEL,
-                               json_str=d.get(ABCPropertyGraph.PROP_LABEL_DELEGATIONS, None)),
-                           capacity_allocations=Capacities.from_json(d.get(ABCPropertyGraph.PROP_CAPACITY_ALLOCATIONS,
-                                                                             None)),
-                           label_allocations=Labels.from_json(d.get(ABCPropertyGraph.PROP_LABEL_ALLOCATIONS,
-                                                                      None)),
-                           details=d.get(ABCPropertyGraph.PROP_DETAILS, None),
-                           node_map=json.loads(d[ABCPropertyGraph.PROP_NODE_MAP]) if
-                           d.get(ABCPropertyGraph.PROP_NODE_MAP, None) is not None else None,
-                           stitch_node=json.loads(d[ABCPropertyGraph.PROP_STITCH_NODE])
-                           )
+        ABCPropertyGraph.set_base_sliver_properties_from_graph_properties_dict(isl, d)
         return isl
 
     def build_deep_node_sliver(self, *, node_id: str) -> NodeSliver:
@@ -700,7 +696,7 @@ def build_deep_node_sliver(self, *, node_id: str) -> NodeSliver:
         # create top-level sliver
         ns = self.node_sliver_from_graph_properties_dict(props)
         ns.node_id = node_id
-        # find and build deep slivers of switch fabrics (if any) and components (if any)
+        # find and build deep slivers of network services (if any) and components (if any)
         comps = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
                                         node_label=ABCPropertyGraph.CLASS_Component)
         if comps is not None and len(comps) > 0:
@@ -710,30 +706,30 @@ def build_deep_node_sliver(self, *, node_id: str) -> NodeSliver:
                 aci.add_device(cs)
             ns.attached_components_info = aci
 
-        sfs = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
-                                      node_label=ABCPropertyGraph.CLASS_SwitchFabric)
-        if sfs is not None and len(sfs) > 0:
-            sfi = SwitchFabricInfo()
-            for s in sfs:
-                sfsl = self.build_deep_sf_sliver(node_id=s)
-                sfi.add_switch_fabric(sfsl)
-            ns.switch_fabric_info = sfi
+        nss = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
+                                      node_label=ABCPropertyGraph.CLASS_NetworkService)
+        if nss is not None and len(nss) > 0:
+            nsi = NetworkServiceInfo()
+            for s in nss:
+                nssl = self.build_deep_ns_sliver(node_id=s)
+                nsi.add_network_service(nssl)
+            ns.network_service_info = nsi
 
         return ns
 
-    def build_deep_link_sliver(self, *, node_id: str) -> NetworkLinkSliver:
+    def build_deep_ns_sliver(self, *, node_id: str) -> NetworkServiceSliver:
         """
-        Build a deep NetworkLinkSliver from a graph node
+        Build a deep sliver for a NetworkService
         :param node_id:
         :return:
         """
         clazzes, props = self.get_node_properties(node_id=node_id)
-        if ABCPropertyGraph.CLASS_Link not in clazzes:
+        if ABCPropertyGraph.CLASS_NetworkService not in clazzes:
             raise PropertyGraphQueryException(node_id=node_id, graph_id=self.graph_id,
-                                              msg="Node is not of class Link")
+                                              msg="Node is not of class NetworkService")
         # create top-level sliver
-        ns = self.link_sliver_from_graph_properties_dict(props)
-        ns.node_id = node_id
+        nss = self.network_service_sliver_from_graph_properties_dict(props)
+        nss.node_id = node_id
         # find interfaces and attach
         ifs = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_CONNECTS,
                                       node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
@@ -744,30 +740,8 @@ def build_deep_link_sliver(self, *, node_id: str) -> NetworkLinkSliver:
                 ifsl = self.interface_sliver_from_graph_properties_dict(iprops)
                 ifsl.node_id = node_id
                 ifi.add_interface(ifsl)
-            ns.interface_info = ifi
-
-        return ns
-
-    def build_deep_sf_sliver(self, *, node_id: str) -> SwitchFabricSliver:
-        clazzes, props = self.get_node_properties(node_id=node_id)
-        if ABCPropertyGraph.CLASS_SwitchFabric not in clazzes:
-            raise PropertyGraphQueryException(node_id=node_id, graph_id=self.graph_id,
-                                              msg="Node is not of class SwitchFabric")
-        # create top-level sliver
-        sfs = self.switch_fabric_sliver_from_graph_properties_dict(props)
-        sfs.node_id = node_id
-        # find interfaces and attach
-        ifs = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_CONNECTS,
-                                      node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
-        if ifs is not None and len(ifs) > 0:
-            ifi = InterfaceInfo()
-            for i in ifs:
-                _, iprops = self.get_node_properties(node_id=i)
-                ifsl = self.interface_sliver_from_graph_properties_dict(iprops)
-                ifsl.node_id = node_id
-                ifi.add_interface(ifsl)
-            sfs.interface_info = ifi
-        return sfs
+            nss.interface_info = ifi
+        return nss
 
     def build_deep_component_sliver(self, *, node_id: str) -> ComponentSliver:
         clazzes, props = self.get_node_properties(node_id=node_id)
@@ -777,32 +751,266 @@ def build_deep_component_sliver(self, *, node_id: str) -> ComponentSliver:
         # create top-level sliver
         cs = self.component_sliver_from_graph_properties_dict(props)
         cs.node_id = node_id
-        # find any switch fabrics, build and attach
-        sfs = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
-                                      node_label=ABCPropertyGraph.CLASS_SwitchFabric)
-        if sfs is not None and len(sfs) > 0:
-            sfi = SwitchFabricInfo()
-            for s in sfs:
-                sfsl = self.build_deep_sf_sliver(node_id=s)
-                sfi.add_switch_fabric(sfsl)
-            cs.switch_fabric_info = sfi
+        # find any network services, build and attach
+        nss = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
+                                      node_label=ABCPropertyGraph.CLASS_NetworkService)
+        if nss is not None and len(nss) > 0:
+            nsi = NetworkServiceInfo()
+            for s in nss:
+                nssl = self.build_deep_ns_sliver(node_id=s)
+                nsi.add_network_service(nssl)
+            cs.network_service_info = nsi
         return cs
 
-    def get_all_link_interfaces(self, link_id: str) -> List[str]:
+    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
+        and connected links (as appropriate)
+        :param node_id:
+        :return:
+        """
+        # the network node itself
+        # its components and network services
+        # component network services
+        # network service interfaces (if present)
+        # their parent interfaces (if present)
+        # the 'Link' objects connected to interfaces if only one or no other interface connects to them
+
+        # check we are a network node
+        labels, node_props = self.get_node_properties(node_id=node_id)
+        if ABCPropertyGraph.CLASS_NetworkNode not in labels:
+            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=node_id,
+                                              msg="This node type is not NetworkNode")
+        # get a list of components
+        components = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
+                                             node_label=ABCPropertyGraph.CLASS_Component)
+        for cid in components:
+            self.remove_component_with_nss_cps_and_links(node_id=cid)
+
+        # get a list of network services, delete the node
+        network_services = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
+                                                   node_label=ABCPropertyGraph.CLASS_NetworkService)
+        self.delete_node(node_id=node_id)
+        for cid in network_services:
+            self.remove_ns_with_cps_and_links(node_id=cid)
+
+    def remove_component_with_nss_cps_and_links(self, node_id: str):
+        """
+        Remove a component of a network node with all attached elements (network services
+        and interfaces). Parent interfaces and links are removed if they have no other
+        children or connected interfaces.
+        :param node_id: component node id
+        :return:
+        """
+        # check we are a component
+        labels, node_props = self.get_node_properties(node_id=node_id)
+        if ABCPropertyGraph.CLASS_Component not in labels:
+            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=node_id,
+                                              msg="This node type is not Component")
+        # get a list of NetworkServices, delete the node
+        network_services = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
+                                                   node_label=ABCPropertyGraph.CLASS_NetworkService)
+        self.delete_node(node_id=node_id)
+        for ns in network_services:
+            self.remove_ns_with_cps_and_links(node_id=ns)
+
+    def remove_network_link(self, node_id: str):
+
+        # check we are a link
+        labels, node_props = self.get_node_properties(node_id=node_id)
+        if ABCPropertyGraph.CLASS_Link not in labels:
+            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=node_id,
+                                              msg="This node type is not Link")
+        # edges to interfaces/connection points will disappear
+        self.delete_node(node_id=node_id)
+
+    def remove_ns_with_cps_and_links(self, node_id: str):
+        """
+        Remove network service with subtending interfaces and links
+        :param node_id:
+        :return:
+        """
+        # check we are a network service
+        labels, node_props = self.get_node_properties(node_id=node_id)
+        if ABCPropertyGraph.CLASS_NetworkService not in labels:
+            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=node_id,
+                                              msg="This node type is not NetworkService")
+        # get a list of interfaces, delete the network service
+        interfaces = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_CONNECTS,
+                                             node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
+        self.delete_node(node_id=node_id)
+        for iif in interfaces:
+            self.remove_cp_and_links(node_id=iif)
+
+    def remove_cp_and_links(self, node_id: str):
+        """
+        Remove ConnectionPoint and links. Parent ConnectionPoints and links are removed
+        if they have no other children or other connected connection points.
+        :param node_id: interface node id
+        :return:
+        """
+        # some interfaces may have parent interfaces, which can be deleted if they only have the
+        # one child
+        interfaces_to_delete = {node_id}
+        parents = self.get_first_neighbor(node_id=node_id,
+                                          rel=ABCPropertyGraph.REL_CONNECTS,
+                                          node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
+        for parent in parents:  # really should only be one parent interface
+            children = self.get_first_neighbor(node_id=parent,
+                                               rel=ABCPropertyGraph.REL_CONNECTS,
+                                               node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
+            if len(children) == 1:  # if only child, can delete parent
+                interfaces_to_delete.add(parent)
+        # interfaces themselves and parent interfaces
+        # may be connected to links which can be deleted if nothing
+        # else connects to them
+        links_to_delete = set()
+        for interface in interfaces_to_delete:
+            links = self.get_first_neighbor(node_id=interface,
+                                            rel=ABCPropertyGraph.REL_CONNECTS,
+                                            node_label=ABCPropertyGraph.CLASS_Link)
+            for link in links:  # should only be one
+                connected_interfaces = self.get_first_neighbor(node_id=link,
+                                                               rel=ABCPropertyGraph.REL_CONNECTS,
+                                                               node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
+                if len(connected_interfaces) == 2:  # connected to us and another thing, can delete
+                    links_to_delete.add(link)
+        # delete nodes in these sets
+        for deleted_id in interfaces_to_delete.union(links_to_delete):
+            self.delete_node(node_id=deleted_id)
+
+    def add_network_node_sliver(self, *, sliver: NodeSliver):
+
+        assert sliver is not None
+        assert sliver.node_id is not None
+        assert self.check_node_unique(label=ABCPropertyGraph.CLASS_NetworkNode, name=sliver.resource_name)
+
+        props = self.node_sliver_to_graph_properties_dict(sliver)
+        self.add_node(node_id=sliver.node_id, label=ABCPropertyGraph.CLASS_NetworkNode, props=props)
+        # if components aren't empty, add components, their network services and interfaces
+        aci = sliver.attached_components_info
+        if aci is not None:
+            for csliver in aci.devices.values():
+                self.add_component_sliver(parent_node_id=sliver.node_id,
+                                          component=csliver)
+        # if network services arent empty add them with their interfaces
+        nsi = sliver.network_service_info
+        if nsi is not None:
+            for ns in nsi.network_services.values():
+                self.add_network_service_sliver(parent_node_id=sliver.node_id,
+                                                network_service=ns)
+
+    def add_network_link_sliver(self, *, lsliver: NetworkLinkSliver, interfaces: List[str]):
+
+        assert lsliver is not None
+        assert lsliver.node_id is not None
+        assert self.check_node_unique(label=ABCPropertyGraph.CLASS_Link, name=lsliver.resource_name)
+        assert interfaces is not None
+
+        props = self.link_sliver_to_graph_properties_dict(lsliver)
+        self.add_node(node_id=lsliver.node_id, label=ABCPropertyGraph.CLASS_Link, props=props)
+        # add edge links to specified interfaces
+        for i in interfaces:
+            self.add_link(node_a=lsliver.node_id, rel=ABCPropertyGraph.REL_CONNECTS, node_b=i)
+
+    def add_component_sliver(self, *, parent_node_id: str, component: ComponentSliver):
+        """
+        Add network node component as a sliver, linking back to parent
+        :param parent_node_id:
+        :param component:
+        :return:
+        """
+        assert component.node_id is not None
+        assert parent_node_id is not None
+
+        props = self.component_sliver_to_graph_properties_dict(component)
+        self.add_node(node_id=component.node_id, label=ABCPropertyGraph.CLASS_Component, props=props)
+        self.add_link(node_a=parent_node_id, rel=ABCPropertyGraph.REL_HAS, node_b=component.node_id)
+        nsi = component.network_service_info
+        if nsi is not None:
+            for ns in nsi.network_services.values():
+                self.add_network_service_sliver(parent_node_id=component.node_id,
+                                                network_service=ns)
+
+    def add_network_service_sliver(self, *, parent_node_id: str, network_service: NetworkServiceSliver):
+        """
+        Add network service as a sliver to either node or component, linking back to parent (if there is one).
+        :param parent_node_id: can be None
+        :param network_service:
+        :return:
+        """
+        assert network_service.node_id is not None
+        if parent_node_id is None:
+            # slice-wide network services must have unique names
+            assert self.check_node_unique(label=ABCPropertyGraph.CLASS_NetworkService,
+                                          name=network_service.resource_name)
+
+        props = self.network_service_sliver_to_graph_properties_dict(network_service)
+        self.add_node(node_id=network_service.node_id, label=ABCPropertyGraph.CLASS_NetworkService, props=props)
+        if parent_node_id is not None:
+            self.add_link(node_a=parent_node_id, rel=ABCPropertyGraph.REL_HAS, node_b=network_service.node_id)
+        ii = network_service.interface_info
+        if ii is not None:
+            for i in ii.interfaces.values():
+                self.add_interface_sliver(parent_node_id=network_service.node_id,
+                                          interface=i)
+
+    def add_interface_sliver(self, *, parent_node_id: str, interface: InterfaceSliver):
+        """
+        Add interface to a network service, linking back to parent.
+        :param parent_node_id: network service id
+        :param interface: interface sliver description
+        :return:
+        """
+        assert interface.node_id is not None
+        assert parent_node_id is not None
+
+        props = self.interface_sliver_to_graph_properties_dict(interface)
+        self.add_node(node_id=interface.node_id, label=ABCPropertyGraph.CLASS_ConnectionPoint, props=props)
+        self.add_link(node_a=parent_node_id, rel=ABCPropertyGraph.REL_CONNECTS, node_b=interface.node_id)
+
+    def get_all_ns_or_link_connection_points(self, link_id: str) -> List[str]:
+        """
+        Get interfaces attached to a link or network service
+        :param link_id:
+        :return:
+        """
         assert link_id is not None
         # check this is a link
         labels, parent_props = self.get_node_properties(node_id=link_id)
-        if ABCPropertyGraph.CLASS_Link not in labels:
+        if ABCPropertyGraph.CLASS_Link not in labels and ABCPropertyGraph.CLASS_NetworkService not in labels:
             raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=link_id,
-                                              msg="Node type is not Link")
+                                              msg="Node type is not Link or a NetworkService")
         return self.get_first_neighbor(node_id=link_id, rel=ABCPropertyGraph.REL_CONNECTS,
                                        node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
 
+    def get_all_node_or_component_connection_points(self, parent_node_id: str) -> List[str]:
+        """
+        Get a list of interfaces attached via network services
+        :param parent_node_id:
+        :return:
+        """
+        assert parent_node_id is not None
+        labels, parent_props = self.get_node_properties(node_id=parent_node_id)
+        if ABCPropertyGraph.CLASS_NetworkNode not in labels and \
+                ABCPropertyGraph.CLASS_Component not in labels:
+            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=parent_node_id,
+                                              msg="Parent node type is not NetworkNode or Component")
+        nss_ifs = self.get_first_and_second_neighbor(node_id=parent_node_id, rel1=ABCPropertyGraph.REL_HAS,
+                                                     node1_label=ABCPropertyGraph.CLASS_NetworkService,
+                                                     rel2=ABCPropertyGraph.REL_CONNECTS,
+                                                     node2_label=ABCPropertyGraph.CLASS_ConnectionPoint)
+        ret = list()
+        # return only interface IDs, not interested in NetworkServices
+        for tup in nss_ifs:
+            ret.append(tup[1])
+        return ret
+
     def get_parent(self, node_id: str, rel: str, parent: str) -> Tuple[str, str]:
         """
         Return name and node_id properties of a parent node (connected via 'has' relationship).
-        NetworkNodes and Links have no parents, Components have NetworkNodes, SwitchFabrics have
-        Components or NetworkNodes, ConnectionPoints have SwitchFabrics
+        NetworkNodes and Links have no parents, Components have NetworkNodes, NetworkServices have
+        Components or NetworkNodes or None, ConnectionPoints have NetworkServices
         :param node_id:
         :param rel: relationship with parent
         :param parent: parent class
diff --git a/fim/graph/abc_property_graph_constants.py b/fim/graph/abc_property_graph_constants.py
index ea7d6a8..76d8bb4 100644
--- a/fim/graph/abc_property_graph_constants.py
+++ b/fim/graph/abc_property_graph_constants.py
@@ -61,16 +61,20 @@ class ABCPropertyGraphConstants(ABC):
     PROP_NODE_MAP = "NodeMap"
     PROP_STRUCTURAL_INFO = "StructuralInfo"
     PROP_STITCH_NODE = "StitchNode"
+    PROP_ERO = "ERO"
+    PROP_PATH_INFO = "PathInfo"
+    PROP_CONTROLLER_URL = "ControllerURL"
     # these properties get validated to be valid JSON objects whenever someone validates the graph
     JSON_PROPERTY_NAMES = [PROP_LABELS, PROP_CAPACITIES, PROP_LABEL_DELEGATIONS,
                            PROP_CAPACITY_DELEGATIONS, PROP_LABEL_ALLOCATIONS,
-                           PROP_CAPACITY_ALLOCATIONS, PROP_RESERVATION_INFO, PROP_STRUCTURAL_INFO]
+                           PROP_CAPACITY_ALLOCATIONS, PROP_RESERVATION_INFO, PROP_STRUCTURAL_INFO,
+                           PROP_ERO, PROP_PATH_INFO]
     # these properties cannot be unset
     NO_UNSET_PROPERTIES = [GRAPH_ID, NODE_ID, PROP_TYPE, PROP_CLASS]
 
     CLASS_NetworkNode = 'NetworkNode'
+    CLASS_NetworkService = 'NetworkService'
     CLASS_Component = 'Component'
-    CLASS_SwitchFabric = 'SwitchFabric'
     CLASS_ConnectionPoint = 'ConnectionPoint'
     CLASS_Link = 'Link'
     CLASS_CompositeLink = 'CompositeLink'
diff --git a/fim/graph/data/graph_validation_rules.json b/fim/graph/data/graph_validation_rules.json
index 8557431..a80e59d 100644
--- a/fim/graph/data/graph_validation_rules.json
+++ b/fim/graph/data/graph_validation_rules.json
@@ -8,8 +8,8 @@
     "msg": "All node NodeIDs must be distinct."
   },
   {
-    "rule": "MATCH (n:GraphNode {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Class IN [\"ConnectionPoint\", \"NetworkNode\", \"SwitchFabric\", \"Component\", \"Link\"])",
-    "msg": "All nodes must be of class NetworkNode, Component, ConnectionPoint, SwitchFabric or Link"
+    "rule": "MATCH (n:GraphNode {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Class IN [\"ConnectionPoint\", \"NetworkNode\", \"CompositeNode\", \"NetworkService\", \"Component\", \"Link\"])",
+    "msg": "All nodes must be of class NetworkNode, CompositeNode, Component, ConnectionPoint, NetworkService or Link"
   },
   {
     "rule": "MATCH (n:NetworkNode {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"Server\", \"Switch\", \"VM\", \"Container\", \"NAS\"])",
@@ -17,34 +17,38 @@
   },
   {
     "rule": "MATCH (n:Component {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"SmartNIC\", \"GPU\", \"FPGA\", \"NVME\", \"SharedNIC\"])",
-    "msg": "All Components must be of type NIC, SmartNIC, GPU, FPGA or NVMe"
+    "msg": "All Components must be of type SharedNIC, SmartNIC, GPU, FPGA or NVME"
   },
   {
-    "rule": "MATCH (n:ConnectionPoint {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"Port\", \"AccessPort\", \"TrunkPort\", \"AccessVint\", \"TrunkVint\"])",
-    "msg": "All ConnectionPoints must be of type Port, AccessPort, TrunkPort, AccessVint or TrunkVint"
+    "rule": "MATCH (n:ConnectionPoint {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"AccessPort\", \"TrunkPort\", \"ServicePort\", \"DedicatedPort\", \"SharedPort\", \"vInt\"])",
+    "msg": "All ConnectionPoints must be of type AccessPort, TrunkPort, ServicePort, DedicatedPort, SharedPort or vInt"
   },
   {
-    "rule": "MATCH (n:SwitchFabric {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"SwitchFabric\"])",
-    "msg": "All SwitchFabrics must be of type SwitchFabric"
+    "rule": "MATCH (n:NetworkService {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [ \"P4\", \"OVS\", \"MPLS\", \"L2Path\", \"L2Bridge\", \"L2PTP\", \"L2STS\", \"FABNetv4\", \"FABNetv6\", \"L3VPN\", \"PortMirror\"])",
+    "msg": "All NetworkServices must be of type P4, OVS, MPLS, L2Path, L2Bridge, L2PTP, L2STS, FABNetv4, FABNetv6, L3VPN or PortMirror"
   },
   {
-    "rule": "MATCH (n:Link {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"DAC\", \"Wave\", \"Patch\", \"L2Path\"])",
+    "rule": "MATCH (n:Link {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"Wave\", \"Patch\", \"L2Path\"])",
     "msg": "All Links must be of type DAC, Patch or Wave"
   },
   {
-    "rule": "MATCH(n:ConnectionPoint {GraphID: $graphId}) -[:connects]- (m:GraphNode {GraphID: $graphId}) where m.Class=\"SwitchFabric\" with count(n) as vcps match(c:ConnectionPoint {GraphID: $graphId}) with vcps, count(c) as cps return cps = vcps",
-    "msg": "All ConnectionPoints must be connected to a SwitchFabric"
+    "rule": "MATCH(n:Component {GraphID: $graphId}) -[:has]- (m:GraphNode {GraphID: $graphId}) where m.Class=\"NetworkNode\" or m.Class=\"CompositeNode\" with count(n) as vcs match(c:Component {GraphID: $graphId}) with vcs, count(c) as cs return cs = vcs",
+    "msg": "All Components must be owned by a NetworkNode or a CompositeNode"
   },
   {
-    "rule": "MATCH(n:SwitchFabric {GraphID: $graphId}) -[:has]- (m:GraphNode {GraphID: $graphId}) where m.Class=\"NetworkNode\" or m.Class=\"Component\" with count(n) as vcps match(c:SwitchFabric {GraphID: $graphId}) with vcps, count(c) as cps return cps = vcps",
-    "msg": "All SwitchFabrics must be owned by a Component or a NetworkNode"
+    "rule": "MATCH (o:GraphNode {GraphID: $graphId}) -[:connects]- (n:Link {GraphID: $graphId}) where o.Class <> \"ConnectionPoint\" return count(o)=0",
+    "msg": "Links can only connect to ConnectionPoints"
   },
   {
-    "rule": "MATCH(n:Component {GraphID: $graphId}) -[:has]- (m:GraphNode {GraphID: $graphId}) where m.Class=\"NetworkNode\" with count(n) as vcs match(c:Component {GraphID: $graphId}) with vcs, count(c) as cs return cs = vcs",
-    "msg": "All Components must be owned by a NetworkNode"
+    "rule": "MATCH (n:Link {GraphID: $graphId}) WHERE n.Type = \"Patch\" or n.Type=\"L2Path\" or n.Type=\"L1Path\" or n.Type=\"L2PTP\" or n.Type=\"PortMirror\" WITH collect(size((n) -[:connects]- ())) as sizes RETURN any(s in sizes where s=2)",
+    "msg": "Patch, L[12]Path, L2PTP or PortMirror link type can only connect two ConnectionPoints"
   },
   {
-    "rule": "MATCH (o:GraphNode {GraphID: $graphId}) -[:connects]- (n:Link {GraphID: $graphId}) where o.Class <> \"ConnectionPoint\" return count(o)=0",
-    "msg": "Links can only connect to ConnectionPoints"
+    "rule": "MATCH (n:Link) WHERE n.Type=\"FABNetv4\" RETURN count(n) <= 1",
+    "msg": "Only one instance of FABnetv4 allowed to be present"
+  },
+  {
+    "rule": "MATCH (n:Link) WHERE n.Type=\"FABNetv6\" RETURN count(n) <= 1",
+    "msg": "Only one instance of FABnetv6 allowed to be present"
   }
 ]
\ No newline at end of file
diff --git a/fim/graph/neo4j_property_graph.py b/fim/graph/neo4j_property_graph.py
index 49c82d3..ca4838c 100644
--- a/fim/graph/neo4j_property_graph.py
+++ b/fim/graph/neo4j_property_graph.py
@@ -107,6 +107,17 @@ def get_all_nodes_by_class(self, *, label: str) -> List[str]:
                                                   node_id=None, msg="Unable to find network links")
             return val.data()['nodeids']
 
+    def get_all_nodes_by_class_and_type(self, *, label: str, ntype: str) -> List[str]:
+        assert label is not None
+        assert ntype is not None
+        query = f"MATCH(n:{label} {{GraphID: $graphId, Type: $ntype}}) RETURN collect(n.NodeID) as nodeids"
+        with self.driver.session() as session:
+            val = session.run(query, graphId=self.graph_id, ntype=ntype).single()
+            if val is None:
+                raise PropertyGraphQueryException(graph_id=self.graph_id,
+                                                  node_id=None, msg="Unable to find network links")
+            return val.data()['nodeids']
+
     def list_all_node_ids(self) -> List[str]:
         """
         List all NodeID properties of nodes in a graph
@@ -441,7 +452,7 @@ def get_first_and_second_neighbor(self, *, node_id: str, rel1: str, node1_label:
 
         query = f"match (a:GraphNode {{GraphID: $graphId, NodeID: $nodeA}}) -[:{rel1}]- "\
                 f"(b:{node1_label} {{GraphID: $graphId}}) -[:{rel2}]- "\
-                f"(c:{node2_label} {{GraphID: $graphId}}) return b.NodeID, c.NodeID"
+                f"(c:{node2_label} {{GraphID: $graphId}}) WHERE $nodeA <> c.NodeID return b.NodeID, c.NodeID"
         with self.driver.session() as session:
             val = session.run(query, graphId=self.graph_id, nodeA=node_id).values()
             if val is None:
@@ -580,6 +591,17 @@ def get_stitch_nodes(self) -> List[str]:
                                                   node_id=None, msg="Unable to find stitch nodes")
             return val.data()['nodeids']
 
+    def check_node_unique(self, *, label: str, name: str) -> bool:
+        assert label is not None
+        assert name is not None
+
+        query = f"MATCH (n:{label} {{GraphID: $graphId, Name: $name}}) RETURN collect(n.NodeID) as nodeids"
+        with self.driver.session() as session:
+            val = session.run(query, graphId=self.graph_id, name=name).single()
+            if val is None or len(val.data()) == 0 or len(val.data()['nodeids']) == 0:
+                return True
+            return False
+
 
 class Neo4jGraphImporter(ABCGraphImporter):
 
diff --git a/fim/graph/networkx_property_graph.py b/fim/graph/networkx_property_graph.py
index 2faf28e..aff7876 100644
--- a/fim/graph/networkx_property_graph.py
+++ b/fim/graph/networkx_property_graph.py
@@ -321,6 +321,22 @@ def get_all_nodes_by_class(self, *, label: str) -> List[str]:
             ret.append(my_graph.nodes[n][ABCPropertyGraph.NODE_ID])
         return ret
 
+    def get_all_nodes_by_class_and_type(self, *, label: str, ntype: str) -> List[str]:
+        assert label is not None
+        assert ntype is not None
+        my_graph = self.storage.get_graph(self.graph_id)
+        graph_nodes = list(nxq.search_nodes(my_graph,
+                                            {'and': [
+                                                {'eq': [ABCPropertyGraph.GRAPH_ID, self.graph_id]},
+                                                {'eq': [ABCPropertyGraph.PROP_CLASS, label]},
+                                                {'eq': [ABCPropertyGraph.PROP_TYPE, ntype]}
+                                            ]
+                                            }))
+        ret = list()
+        for n in graph_nodes:
+            ret.append(my_graph.nodes[n][ABCPropertyGraph.NODE_ID])
+        return ret
+
     def list_all_node_ids(self) -> List[str]:
         """
         List all NodeID properties of nodes in a graph
@@ -472,6 +488,9 @@ def get_first_and_second_neighbor(self, *, node_id: str, rel1: str, node1_label:
             # filter second neighbors by label
             second_neighbors = self._filter_nodes_by_label(graph, second_neighbors, node2_label)
             if len(second_neighbors) > 0:
+                # remove self in case they are there
+                if real_node in second_neighbors:
+                    second_neighbors.remove(real_node)
                 second_neighbors_dict[n] = second_neighbors
 
         # convert to a list of two-member lists, converting internal IDs to guids
@@ -630,6 +649,21 @@ def get_stitch_nodes(self) -> List[str]:
             ret.append(my_graph.nodes[n][ABCPropertyGraph.NODE_ID])
         return ret
 
+    def check_node_unique(self, *, label: str, name: str):
+        """
+        Check no other node of this class/label and name exists
+        :param label:
+        :param name:
+        :return:
+        """
+        graph_nodes = list(nxq.search_nodes(self.storage.get_graph(self.graph_id),
+                                            {'and': [
+                                                {'eq': [ABCPropertyGraph.GRAPH_ID, self.graph_id]},
+                                                {'eq': [ABCPropertyGraph.PROP_NAME, name]},
+                                                {'eq': [ABCPropertyGraph.PROP_CLASS, label]}
+                                            ]}))
+        return len(graph_nodes) == 0
+
 
 class NetworkXGraphStorage:
     """
diff --git a/fim/graph/resources/abc_arm.py b/fim/graph/resources/abc_arm.py
index 5251759..49f61a4 100644
--- a/fim/graph/resources/abc_arm.py
+++ b/fim/graph/resources/abc_arm.py
@@ -152,8 +152,8 @@ def generate_adms(self) -> Dict:
 
             # in addition to definite keep nodes, we should also keep nodes that
             # a) lie on shortest paths between two connection points we are keeping (to include the links)
-            # b) switch fabrics that connect to connection points
-            # c) network nodes that have switch fabrics
+            # b) network services that connect to connection points
+            # c) network nodes that have network services
             # Determine nodes to delete by doing a difference between all nodes and keep nodes
             # Remove nodes
             # Annotate keep nodes with proper delegation information
@@ -185,7 +185,7 @@ def generate_adms(self) -> Dict:
             for cp in keep_cps:
                 cp_neighbors = self.get_first_and_second_neighbor(node_id=cp,
                                                                   rel1=ABCPropertyGraph.REL_CONNECTS,
-                                                                  node1_label=ABCPropertyGraph.CLASS_SwitchFabric,
+                                                                  node1_label=ABCPropertyGraph.CLASS_NetworkService,
                                                                   rel2=ABCPropertyGraph.REL_HAS,
                                                                   node2_label=ABCPropertyGraph.CLASS_NetworkNode)
                 for pair in cp_neighbors:
@@ -193,7 +193,7 @@ def generate_adms(self) -> Dict:
 
                 cp_neighbors = self.get_first_and_second_neighbor(node_id=cp,
                                                                   rel1=ABCPropertyGraph.REL_CONNECTS,
-                                                                  node1_label=ABCPropertyGraph.CLASS_SwitchFabric,
+                                                                  node1_label=ABCPropertyGraph.CLASS_NetworkService,
                                                                   rel2=ABCPropertyGraph.REL_HAS,
                                                                   node2_label=ABCPropertyGraph.CLASS_Component)
                 for pair in cp_neighbors:
diff --git a/fim/graph/resources/neo4j_cbm.py b/fim/graph/resources/neo4j_cbm.py
index 4e4bc47..c95a6a3 100644
--- a/fim/graph/resources/neo4j_cbm.py
+++ b/fim/graph/resources/neo4j_cbm.py
@@ -325,9 +325,9 @@ def get_matching_nodes_with_components(self, *, label: str, props: Dict,
 
     def get_intersite_links(self) -> List[Tuple[str, str, str]]:
         # does a lexicographic comparison of Site properties to ensure only unique links are returned
-        query = 'match p= (n:NetworkNode {Type:"Switch", GraphID: $graphId}) -[:has]- (:SwitchFabric) ' \
+        query = 'match p= (n:NetworkNode {Type:"Switch", GraphID: $graphId}) -[:has]- (:NetworkService) ' \
                 '-[:connects]- (cp1:ConnectionPoint) -[:connects]- (l:Link) ' \
-                '-[:connects]- (cp2:ConnectionPoint) -[:connects]- (:SwitchFabric) ' \
+                '-[:connects]- (cp2:ConnectionPoint) -[:connects]- (:NetworkService) ' \
                 '-[:has]- (m:NetworkNode {Type:"Switch", GraphID: $graphId}) where n.Site > m.Site ' \
                 'return n.NodeID as source, l.NodeID as link, m.NodeID as sink, n.Site as source_site, ' \
                 'm.Site as sink_site, cp1.NodeID as source_cp, cp2.NodeID as sink_cp'
diff --git a/fim/graph/resources/networkx_abqm.py b/fim/graph/resources/networkx_abqm.py
index 6b1e010..3455497 100644
--- a/fim/graph/resources/networkx_abqm.py
+++ b/fim/graph/resources/networkx_abqm.py
@@ -96,7 +96,7 @@ def check_node_name(self, *, node_id: str, label: str, name: str) -> bool:
 
     def get_all_node_or_component_connection_points(self, parent_node_id: str) -> List[str]:
         """
-        Get a list of interfaces attached via switch fabrics to a composite node
+        Get a list of interfaces attached via network services to a composite node
         :param parent_node_id:
         :return:
         """
@@ -106,11 +106,11 @@ def get_all_node_or_component_connection_points(self, parent_node_id: str) -> Li
             raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=parent_node_id,
                                               msg="Parent node type is not CompositeNode")
         sfs_ifs = self.get_first_and_second_neighbor(node_id=parent_node_id, rel1=ABCPropertyGraphConstants.REL_HAS,
-                                                     node1_label=ABCPropertyGraphConstants.CLASS_SwitchFabric,
+                                                     node1_label=ABCPropertyGraphConstants.CLASS_NetworkService,
                                                      rel2=ABCPropertyGraphConstants.REL_CONNECTS,
                                                      node2_label=ABCPropertyGraphConstants.CLASS_ConnectionPoint)
         ret = list()
-        # return only interface IDs, not interested in SwitchFabrics
+        # return only interface IDs, not interested in NetworkServices
         for tup in sfs_ifs:
             ret.append(tup[1])
         return ret
diff --git a/fim/graph/slices/abc_asm.py b/fim/graph/slices/abc_asm.py
index 051ba37..726ab13 100644
--- a/fim/graph/slices/abc_asm.py
+++ b/fim/graph/slices/abc_asm.py
@@ -32,11 +32,6 @@
 
 import json
 
-from fim.slivers.network_node import NodeSliver
-from fim.slivers.attached_components import ComponentSliver
-from fim.slivers.switch_fabric import SwitchFabricSliver
-from fim.slivers.interface_info import InterfaceSliver
-from fim.slivers.network_link import NetworkLinkSliver
 from fim.graph.abc_property_graph import ABCPropertyGraph, PropertyGraphQueryException
 
 
@@ -54,17 +49,6 @@ def __subclasshook__(cls, subclass):
         return (hasattr(subclass, 'get_all_network_nodes') and
                 callable(subclass.get_all_network_nodes) or NotImplemented)
 
-    @abstractmethod
-    def check_node_unique(self, *, label: str, name: str) -> bool:
-        """
-        Check no other node of this class/label and name exists. Doesn't
-        apply universally as e.g. Component names are only unique
-        within node, interface names are only unique within SF.
-        :param label:
-        :param name:
-        :return:
-        """
-
     @abstractmethod
     def check_node_name(self, *, node_id: str, label: str, name: str) -> bool:
         """
@@ -85,14 +69,6 @@ def find_node_by_name(self, *, node_name: str, label: str) -> str:
         :return:
         """
 
-    def get_all_network_nodes(self) -> List[str]:
-
-        return self.get_all_nodes_by_class(label=ABCASMPropertyGraph.CLASS_NetworkNode)
-
-    def get_all_network_links(self) -> List[str]:
-
-        return self.get_all_nodes_by_class(label=ABCASMPropertyGraph.CLASS_Link)
-
     def set_mapping(self, *, node_id: str, to_graph_id: str, to_node_id: str) -> None:
         """
         Create a mapping from a node to another graph, node
@@ -167,7 +143,7 @@ def get_all_network_node_components(self, parent_node_id: str) -> List[str]:
         return self.get_first_neighbor(node_id=parent_node_id, rel=ABCPropertyGraph.REL_HAS,
                                        node_label=ABCPropertyGraph.CLASS_Component)
 
-    def get_all_network_node_or_component_sfs(self, parent_node_id: str) -> List[str]:
+    def get_all_network_node_or_component_nss(self, parent_node_id: str) -> List[str]:
         assert parent_node_id is not None
         # check that parent is a NetworkNode or Component
         labels, parent_props = self.get_node_properties(node_id=parent_node_id)
@@ -176,42 +152,11 @@ def get_all_network_node_or_component_sfs(self, parent_node_id: str) -> List[str
             raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=parent_node_id,
                                               msg="Parent node type is not NetworkNode or Component")
         return self.get_first_neighbor(node_id=parent_node_id, rel=ABCPropertyGraph.REL_HAS,
-                                       node_label=ABCPropertyGraph.CLASS_SwitchFabric)
-
-    def get_all_node_or_component_connection_points(self, parent_node_id: str) -> List[str]:
-        """
-        Get a list of interfaces attached via switch fabrics
-        :param parent_node_id:
-        :return:
-        """
-        assert parent_node_id is not None
-        labels, parent_props = self.get_node_properties(node_id=parent_node_id)
-        if ABCPropertyGraph.CLASS_NetworkNode not in labels and \
-                ABCPropertyGraph.CLASS_Component not in labels:
-            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=parent_node_id,
-                                              msg="Parent node type is not NetworkNode or Component")
-        sfs_ifs = self.get_first_and_second_neighbor(node_id=parent_node_id, rel1=ABCPropertyGraph.REL_HAS,
-                                                     node1_label=ABCPropertyGraph.CLASS_SwitchFabric,
-                                                     rel2=ABCPropertyGraph.REL_CONNECTS,
-                                                     node2_label=ABCPropertyGraph.CLASS_ConnectionPoint)
-        ret = list()
-        # return only interface IDs, not interested in SwitchFabrics
-        for tup in sfs_ifs:
-            ret.append(tup[1])
-        return ret
-
-    def get_all_sf_connection_points(self, parent_node_id: str) -> List[str]:
-        assert parent_node_id is not None
-        # check that parent is a SwitchFabric
-        labels, parent_props = self.get_node_properties(node_id=parent_node_id)
-        if ABCPropertyGraph.CLASS_SwitchFabric not in labels:
-            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=parent_node_id,
-                                              msg="Parent node type is not SwitchFabric")
-        return self.get_first_neighbor(node_id=parent_node_id, rel=ABCPropertyGraph.REL_CONNECTS,
-                                       node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
+                                       node_label=ABCPropertyGraph.CLASS_NetworkService)
 
     def find_component_by_name(self, *, parent_node_id: str, component_name: str) -> str:
 
+        assert component_name is not None
         component_id_list = self.get_all_network_node_components(parent_node_id=parent_node_id)
         for cid in component_id_list:
             _, cprops = self.get_node_properties(node_id=cid)
@@ -220,19 +165,23 @@ def find_component_by_name(self, *, parent_node_id: str, component_name: str) ->
         raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=None,
                                           msg=f"Unable to find component with name {component_name}")
 
-    def find_sf_by_name(self, *, parent_node_id: str, sfname: str) -> str:
+    def find_ns_by_name(self, *, parent_node_id: str, nsname: str) -> str:
+
+        assert nsname is not None
 
-        sf_id_list = self.get_all_network_node_or_component_sfs(parent_node_id=parent_node_id)
-        for cid in sf_id_list:
+        ns_id_list = self.get_all_network_node_or_component_nss(parent_node_id=parent_node_id)
+        for cid in ns_id_list:
             _, cprops = self.get_node_properties(node_id=cid)
-            if cprops[ABCPropertyGraph.PROP_NAME] == sfname:
+            if cprops[ABCPropertyGraph.PROP_NAME] == nsname:
                 return cid
         raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=None,
-                                          msg=f"Unable to find SwitchFabric with name {sfname}")
+                                          msg=f"Unable to find NetworkService with name {nsname}")
 
     def find_connection_point_by_name(self, *, parent_node_id: str, iname: str) -> str:
 
-        if_id_list = self.get_all_sf_connection_points(parent_node_id=parent_node_id)
+        assert iname is not None
+
+        if_id_list = self.get_all_ns_or_link_connection_points(link_id=parent_node_id)
         for cid in if_id_list:
             _, cprops = self.get_node_properties(node_id=cid)
             if cprops[ABCPropertyGraph.PROP_NAME] == iname:
@@ -240,205 +189,24 @@ def find_connection_point_by_name(self, *, parent_node_id: str, iname: str) -> s
         raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=None,
                                           msg=f"Unable to find ConnectionPoint with name {iname}")
 
-    def remove_network_node_with_components_sfs_cps_and_links(self, node_id: str):
+    def find_peer_connection_point(self, *, node_id: str) -> str or None:
         """
-        Remove a network node, all of components and their interfaces, parent interfaces
-        and connected links (as appropriate)
-        :param node_id:
+        Find the id of the peer connection point to this one (connected over a Link)
+        if it exists
+        :param node_id: id of the interface/connection point
         :return:
         """
-        # the network node itself
-        # its components and switch fabrics
-        # component switch fabrics
-        # switch fabric interfaces (if present)
-        # their parent interfaces (if present)
-        # the 'Link' objects connected to interfaces if only one or no other interface connects to them
-
-        # check we are a network node
-        labels, node_props = self.get_node_properties(node_id=node_id)
-        if ABCPropertyGraph.CLASS_NetworkNode not in labels:
-            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=node_id,
-                                              msg="This node type is not NetworkNode")
-        # get a list of components
-        components = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
-                                             node_label=ABCPropertyGraph.CLASS_Component)
-        for cid in components:
-            self.remove_component_with_sfs_cps_and_links(node_id=cid)
-
-        # get a list of switch fabrics, delete the node
-        switch_fabrics = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
-                                                 node_label=ABCPropertyGraph.CLASS_SwitchFabric)
-        self.delete_node(node_id=node_id)
-        for cid in switch_fabrics:
-            self.remove_sf_with_cps_and_links(node_id=cid)
-
-    def remove_component_with_sfs_cps_and_links(self, node_id: str):
-        """
-        Remove a component of a network node with all attached elements (switch fabrics
-        and interfaces). Parent interfaces and links are removed if they have no other
-        children or connected interfaces.
-        :param node_id: component node id
-        :return:
-        """
-        # check we are a component
-        labels, node_props = self.get_node_properties(node_id=node_id)
-        if ABCPropertyGraph.CLASS_Component not in labels:
-            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=node_id,
-                                              msg="This node type is not Component")
-        # get a list of SwitchFabrics, delete the node
-        switch_fabrics = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_HAS,
-                                                 node_label=ABCPropertyGraph.CLASS_SwitchFabric)
-        self.delete_node(node_id=node_id)
-        for sf in switch_fabrics:
-            self.remove_sf_with_cps_and_links(node_id=sf)
-
-    def remove_network_link(self, node_id: str):
-
-        # check we are a link
-        labels, node_props = self.get_node_properties(node_id=node_id)
-        if ABCPropertyGraph.CLASS_Link not in labels:
-            raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=node_id,
-                                              msg="This node type is not Link")
-        # edges to interfaces/connection points will disappear
-        self.delete_node(node_id=node_id)
+        assert node_id is not None
 
-    def remove_sf_with_cps_and_links(self, node_id: str):
-        """
-        Remove switch fabric with subtending interfaces and links
-        :param node_id:
-        :return:
-        """
-        # check we are a switch fabric
-        labels, node_props = self.get_node_properties(node_id=node_id)
-        if ABCPropertyGraph.CLASS_SwitchFabric not in labels:
+        # find id of connection point over a Link
+        candidates = self.get_first_and_second_neighbor(node_id=node_id, rel1=ABCPropertyGraph.REL_CONNECTS,
+                                                        node1_label=ABCPropertyGraph.CLASS_Link,
+                                                        rel2=ABCPropertyGraph.REL_CONNECTS,
+                                                        node2_label=ABCPropertyGraph.CLASS_ConnectionPoint)
+        if len(candidates) == 0:
+            return None
+        if len(candidates) != 1:
             raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=node_id,
-                                              msg="This node type is not SwitchFabric")
-        # get a list of interfaces, delete the switch fabric
-        interfaces = self.get_first_neighbor(node_id=node_id, rel=ABCPropertyGraph.REL_CONNECTS,
-                                             node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
-        self.delete_node(node_id=node_id)
-        for iif in interfaces:
-            self.remove_cp_and_links(node_id=iif)
-
-    def remove_cp_and_links(self, node_id: str):
-        """
-        Remove ConnectionPoint and links. Parent ConnectionPoints and links are removed
-        if they have no other children or other connected connection points.
-        :param node_id: interface node id
-        :return:
-        """
-        # some interfaces may have parent interfaces, which can be deleted if they only have the
-        # one child
-        interfaces_to_delete = {node_id}
-        parents = self.get_first_neighbor(node_id=node_id,
-                                          rel=ABCPropertyGraph.REL_CONNECTS,
-                                          node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
-        for parent in parents:  # really should only be one parent interface
-            children = self.get_first_neighbor(node_id=parent,
-                                               rel=ABCPropertyGraph.REL_CONNECTS,
-                                               node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
-            if len(children) == 1:  # if only child, can delete parent
-                interfaces_to_delete.add(parent)
-        # interfaces themselves and parent interfaces
-        # may be connected to links which can be deleted if nothing
-        # else connects to them
-        links_to_delete = set()
-        for interface in interfaces_to_delete:
-            links = self.get_first_neighbor(node_id=interface,
-                                            rel=ABCPropertyGraph.REL_CONNECTS,
-                                            node_label=ABCPropertyGraph.CLASS_Link)
-            for link in links:  # should only be one
-                connected_interfaces = self.get_first_neighbor(node_id=link,
-                                                               rel=ABCPropertyGraph.REL_CONNECTS,
-                                                               node_label=ABCPropertyGraph.CLASS_ConnectionPoint)
-                if len(connected_interfaces) == 2:  # connected to us and another thing, can delete
-                    links_to_delete.add(link)
-        # delete nodes in these sets
-        for deleted_id in interfaces_to_delete.union(links_to_delete):
-            self.delete_node(node_id=deleted_id)
-
-    def add_network_node_sliver(self, *, sliver: NodeSliver):
-
-        assert sliver is not None
-        assert sliver.node_id is not None
-        assert self.check_node_unique(label=ABCPropertyGraph.CLASS_NetworkNode, name=sliver.resource_name)
-
-        props = self.node_sliver_to_graph_properties_dict(sliver)
-        self.add_node(node_id=sliver.node_id, label=ABCPropertyGraph.CLASS_NetworkNode, props=props)
-        # if components aren't empty, add components, their switch fabrics and interfaces
-        aci = sliver.attached_components_info
-        if aci is not None:
-            for csliver in aci.devices.values():
-                self.add_component_sliver(parent_node_id=sliver.node_id,
-                                          component=csliver)
-        # if switch fabrics arent empty add them with their interfaces
-        sfi = sliver.switch_fabric_info
-        if sfi is not None:
-            for sf in sfi.switch_fabrics.values():
-                self.add_switch_fabric_sliver(parent_node_id=sliver.node_id,
-                                              switch_fabric=sf)
-
-    def add_network_link_sliver(self, *, lsliver: NetworkLinkSliver, interfaces: List[str]):
-
-        assert lsliver is not None
-        assert lsliver.node_id is not None
-        assert self.check_node_unique(label=ABCPropertyGraph.CLASS_Link, name=lsliver.resource_name)
-        assert interfaces is not None
-
-        props = self.link_sliver_to_graph_properties_dict(lsliver)
-        self.add_node(node_id=lsliver.node_id, label=ABCPropertyGraph.CLASS_Link, props=props)
-        # add edge links to specified interfaces
-        for i in interfaces:
-            self.add_link(node_a=lsliver.node_id, rel=ABCPropertyGraph.REL_CONNECTS, node_b=i)
-
-    def add_component_sliver(self, *, parent_node_id: str, component: ComponentSliver):
-        """
-        Add network node component as a sliver, linking back to parent
-        :param parent_node_id:
-        :param component:
-        :return:
-        """
-        assert component.node_id is not None
-        assert parent_node_id is not None
-
-        props = self.component_sliver_to_graph_properties_dict(component)
-        self.add_node(node_id=component.node_id, label=ABCPropertyGraph.CLASS_Component, props=props)
-        self.add_link(node_a=parent_node_id, rel=ABCPropertyGraph.REL_HAS, node_b=component.node_id)
-        sfi = component.switch_fabric_info
-        if sfi is not None:
-            for sf in sfi.switch_fabrics.values():
-                self.add_switch_fabric_sliver(parent_node_id=component.node_id,
-                                              switch_fabric=sf)
-
-    def add_switch_fabric_sliver(self, *, parent_node_id: str, switch_fabric: SwitchFabricSliver):
-        """
-        Add switch fabric as a sliver to either node or component, linking back to parent.
-        :param parent_node_id:
-        :param switch_fabric:
-        :return:
-        """
-        assert parent_node_id is not None
-        assert switch_fabric.node_id is not None
-
-        props = self.switch_fabric_sliver_to_graph_properties_dict(switch_fabric)
-        self.add_node(node_id=switch_fabric.node_id, label=ABCPropertyGraph.CLASS_SwitchFabric, props=props)
-        self.add_link(node_a=parent_node_id, rel=ABCPropertyGraph.REL_HAS, node_b=switch_fabric.node_id)
-        ii = switch_fabric.interface_info
-        if ii is not None:
-            for i in ii.interfaces.values():
-                self.add_interface_sliver(parent_node_id=switch_fabric.node_id,
-                                          interface=i)
-
-    def add_interface_sliver(self, *, parent_node_id: str, interface: InterfaceSliver):
-        """
-        Add interface to a switch fabric, linking back to parent.
-        :param parent_node_id: switch fabric id
-        :param interface: interface sliver description
-        :return:
-        """
-        assert interface.node_id is not None
-        assert parent_node_id is not None
-
-        props = self.interface_sliver_to_graph_properties_dict(interface)
-        self.add_node(node_id=interface.node_id, label=ABCPropertyGraph.CLASS_ConnectionPoint, props=props)
-        self.add_link(node_a=parent_node_id, rel=ABCPropertyGraph.REL_CONNECTS, node_b=interface.node_id)
+                                              msg=f"Connection point is only expected to connect to one other"
+                                                  f"connection point, instead connects to {len(candidates)}")
+        return candidates[0][1]
diff --git a/fim/graph/slices/neo4j_asm.py b/fim/graph/slices/neo4j_asm.py
index 04bb0f6..36ff88d 100644
--- a/fim/graph/slices/neo4j_asm.py
+++ b/fim/graph/slices/neo4j_asm.py
@@ -35,17 +35,6 @@ class Neo4jASM(ABCASMPropertyGraph, Neo4jPropertyGraph):
     def __init__(self, *, graph_id=str, importer, logger=None):
         super().__init__(graph_id=graph_id, importer=importer, logger=logger)
 
-    def check_node_unique(self, *, label: str, name: str) -> bool:
-        assert label is not None
-        assert name is not None
-
-        query = f"MATCH (n:{label} {{GraphID: $graphId, Name: $name}}) RETURN collect(n.NodeID) as nodeids"
-        with self.driver.session() as session:
-            val = session.run(query, graphId=self.graph_id, name=name).single()
-            if val is None or len(val.data()) == 0 or len(val.data()['nodeids']) == 0:
-                return True
-            return False
-
     def check_node_name(self, *, node_id: str, label: str, name: str) -> bool:
         assert node_id is not None
         assert label is not None
diff --git a/fim/graph/slices/networkx_asm.py b/fim/graph/slices/networkx_asm.py
index acf0955..9a9285c 100644
--- a/fim/graph/slices/networkx_asm.py
+++ b/fim/graph/slices/networkx_asm.py
@@ -61,21 +61,6 @@ def find_node_by_name(self, node_name: str, label: str) -> str:
 
         return my_graph.nodes[graph_nodes[0]][ABCPropertyGraph.NODE_ID]
 
-    def check_node_unique(self, *, label: str, name: str):
-        """
-        Check no other node of this class/label and name exists
-        :param label:
-        :param name:
-        :return:
-        """
-        graph_nodes = list(nxq.search_nodes(self.storage.get_graph(self.graph_id),
-                                            {'and': [
-                                                {'eq': [ABCPropertyGraph.GRAPH_ID, self.graph_id]},
-                                                {'eq': [ABCPropertyGraph.PROP_NAME, name]},
-                                                {'eq': [ABCPropertyGraph.PROP_CLASS, label]}
-                                            ]}))
-        return len(graph_nodes) == 0
-
     def check_node_name(self, *, node_id: str, label: str, name: str) -> bool:
         assert node_id is not None
         assert name is not None
diff --git a/fim/slivers/README.md b/fim/slivers/README.md
index f3cd73c..5cd19c6 100644
--- a/fim/slivers/README.md
+++ b/fim/slivers/README.md
@@ -1,3 +1,11 @@
 # Overview
 
-Basic sliver data structure and supporting classes (like Capacities, Labels and Delegations). 
\ No newline at end of file
+Basic sliver data structures (things that map onto reservations):
+- NetworkNode
+- NetworkService
+  
+and supporting classes:
+- Components
+- Interfaces/ConnectionPoints
+- Links 
+
diff --git a/fim/slivers/attached_components.py b/fim/slivers/attached_components.py
index 2998fc6..e044615 100644
--- a/fim/slivers/attached_components.py
+++ b/fim/slivers/attached_components.py
@@ -27,7 +27,7 @@
 import enum
 
 from .base_sliver import BaseSliver
-from .switch_fabric import SwitchFabricInfo
+from .network_service import NetworkServiceInfo
 
 
 @enum.unique
@@ -49,10 +49,10 @@ class ComponentSliver(BaseSliver):
 
     def __init__(self):
         super().__init__()
-        self.switch_fabric_info = None
+        self.network_service_info = None
 
-    def set_switch_fabric_info(self, sf_info: SwitchFabricInfo):
-        self.switch_fabric_info = sf_info
+    def set_network_service_info(self, ns_info: NetworkServiceInfo):
+        self.network_service_info = ns_info
 
     @staticmethod
     def type_from_str(ctype: str) -> ComponentType or None:
diff --git a/fim/slivers/capacities_labels.py b/fim/slivers/capacities_labels.py
index 28bc5ab..d9c81cd 100644
--- a/fim/slivers/capacities_labels.py
+++ b/fim/slivers/capacities_labels.py
@@ -101,7 +101,7 @@ class Capacities(JSONField):
     Implements basic capacity field handling - encoding and decoding
     from JSON dictionaries of properties
     """
-    UNITS = {'cpu': '', 'unit': '', 'core': '', 'ram': 'G', 'disk': 'G', 'bw': 'Gbps'}
+    UNITS = {'cpu': '', 'unit': '', 'core': '', 'ram': 'G', 'disk': 'G', 'bw': 'Gbps', 'burst_size': 'Mbits'}
 
     def __init__(self):
         self.cpu = 0
@@ -109,6 +109,7 @@ def __init__(self):
         self.ram = 0
         self.disk = 0
         self.bw = 0
+        self.burst_size = 0
         self.unit = 0
 
     def set_fields(self, **kwargs):
@@ -227,9 +228,11 @@ def __init__(self):
         self.asn = None
         self.vlan = None
         self.vlan_range = None
+        self.outer_vlan = None
         self.instance = None
         self.instance_parent = None
         self.local_name = None
+        self.local_type = None
 
     def set_fields(self, **kwargs):
         """
diff --git a/fim/slivers/component_catalog.py b/fim/slivers/component_catalog.py
index ddbddcc..684e4f5 100644
--- a/fim/slivers/component_catalog.py
+++ b/fim/slivers/component_catalog.py
@@ -31,7 +31,7 @@
 
 from .attached_components import ComponentSliver, ComponentType
 from .interface_info import InterfaceInfo, InterfaceSliver, InterfaceType
-from .switch_fabric import SwitchFabricSliver, SwitchFabricInfo, SFLayer, SFType
+from .network_service import NetworkServiceSliver, NetworkServiceInfo, NSLayer, ServiceType
 from .capacities_labels import Capacities, Labels
 
 
@@ -53,7 +53,7 @@ def __read_catalog() -> List[Any]:
         return catalog
 
     def generate_component(self, *, name: str, ctype: ComponentType, model: str,
-                           switch_fabric_node_id: str = None,
+                           ns_node_id: str = None,
                            interface_node_ids: List[str] = None,
                            interface_labels: List[Labels] = None) -> ComponentSliver:
         """
@@ -65,7 +65,7 @@ def generate_component(self, *, name: str, ctype: ComponentType, model: str,
         :param name: name to give to the component
         :param ctype: type of the component
         :param model:
-        :param switch_fabric_node_id: if specified and if component has a switch fabric, put that there
+        :param ns_node_id: if specified and if component has a network service, put that there
         :param interface_node_ids: list of node ids for expected interfaces if component has any
         :param interface_labels: list of labels for expected interfaces if component has any
         :return:
@@ -102,7 +102,10 @@ def generate_component(self, *, name: str, ctype: ComponentType, model: str,
             for interface_name in interfaces_dict.keys():
                 isliver = InterfaceSliver()
                 isliver.set_name(name + '-' + interface_name)
-                isliver.set_type(InterfaceType.TrunkPort)
+                if cs.get_type() == ComponentType.SmartNIC:
+                    isliver.set_type(InterfaceType.DedicatedPort)
+                elif cs.get_type() == ComponentType.SharedNIC:
+                    isliver.set_type(InterfaceType.SharedPort)
                 if interface_node_ids is not None:
                     isliver.node_id = interface_node_ids[id_index]
                 else:
@@ -117,18 +120,18 @@ def generate_component(self, *, name: str, ctype: ComponentType, model: str,
                 cap = Capacities().set_fields(unit=units, bw=int(interfaces_dict[interface_name]))
                 isliver.set_capacities(cap=cap)
                 iinfo.add_interface(isliver)
-            sf = SwitchFabricSliver()
-            if switch_fabric_node_id is None:
-                switch_fabric_node_id = str(uuid.uuid4())
-            sf.node_id = switch_fabric_node_id
-            sf.set_name(name + '-l2sf')
-            sf.set_type(SFType.SwitchFabric)
+            ns = NetworkServiceSliver()
+            if ns_node_id is None:
+                ns_node_id = str(uuid.uuid4())
+            ns.node_id = ns_node_id
+            ns.set_name(name + '-l2ovs')
+            ns.set_type(ServiceType.OVS)
             # default to L2 for now
-            sf.set_layer(SFLayer.L2)
-            sf.interface_info = iinfo
-            sfinfo = SwitchFabricInfo()
-            sfinfo.add_switch_fabric(sf)
-            cs.set_switch_fabric_info(sfinfo)
+            ns.set_layer(NSLayer.L2)
+            ns.interface_info = iinfo
+            nsinfo = NetworkServiceInfo()
+            nsinfo.add_network_service(ns)
+            cs.set_network_service_info(nsinfo)
         return cs
 
     def component_details(self, *, model: str) -> str:
diff --git a/fim/slivers/interface_info.py b/fim/slivers/interface_info.py
index 5e6811a..2709d1a 100644
--- a/fim/slivers/interface_info.py
+++ b/fim/slivers/interface_info.py
@@ -31,10 +31,19 @@
 
 
 class InterfaceType(enum.Enum):
+    """
+    Possible Interface/Port types in FABRIC.
+    """
     AccessPort = enum.auto()
     TrunkPort = enum.auto()
+    ServicePort = enum.auto()
+    DedicatedPort = enum.auto()
+    SharedPort = enum.auto()
     vInt = enum.auto()
 
+    def help(self) -> str:
+        return 'An ' + self.name
+
     def __repr__(self):
         return self.name
 
diff --git a/fim/slivers/network_link.py b/fim/slivers/network_link.py
index 45e0916..2596674 100644
--- a/fim/slivers/network_link.py
+++ b/fim/slivers/network_link.py
@@ -24,17 +24,23 @@
 #
 # Author: Komal Thareja (kthare10@renci.org)
 import enum
+from recordclass import recordclass
 
 from .base_sliver import BaseSliver
-from .interface_info import InterfaceInfo
-from .switch_fabric import SFLayer
+from .network_service import NSLayer
 
 
 class LinkType(enum.Enum):
-    DAC = enum.auto()
-    Wave = enum.auto()
-    Patch = enum.auto()
-    L2Path = enum.auto()
+    """
+    Possible Link types in FABRIC. Links are passive, cannot be instantiated
+    on demand. Links can only connect two interfaces.
+    """
+    Patch = enum.auto() # DAC cable or patch fiber
+    L1Path = enum.auto() # Wave
+    L2Path = enum.auto() # provider L2 path
+
+    def help(self) -> str:
+        return self.name
 
     def __repr__(self):
         return self.name
@@ -43,22 +49,40 @@ def __str__(self):
         return self.name
 
 
+LinkConstraintRecord = recordclass('LinkConstraintRecord',
+                                   ['layer', 'desc',
+                                    'num_interfaces'])
+
+
 class NetworkLinkSliver(BaseSliver):
 
+    """
+    Services can be limited by the number of interfaces/connection points they can connect
+    The number of sites they may connect and the number of instances they may have in a slice.
+    """
+    # whenever there is no limit, num is set to 0
+    NO_LIMIT = 0
+
+    LinkConstraints = {
+        LinkType.Patch: LinkConstraintRecord(layer=NSLayer.L2, num_interfaces=2,
+                                             desc='A fiber patch or a cable.'),
+        LinkType.L1Path: LinkConstraintRecord(layer=NSLayer.L1, num_interfaces=2,
+                                             desc='A wavelength.'),
+        LinkType.L2Path: LinkConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT,
+                                              desc='A provider L2 path or service.')}
+
     def __init__(self):
         super().__init__()
         self.layer = None
         self.technology = None
-        self.interface_info = None
-        self.allocation_constraints = None
 
     #
     # Setters are only needed for things we want users to be able to set
     #
-    def get_layer(self) -> SFLayer:
+    def get_layer(self) -> NSLayer:
         return self.layer
 
-    def set_layer(self, layer: SFLayer):
+    def set_layer(self, layer: NSLayer):
         self.layer = layer
 
     def get_technology(self) -> str:
@@ -67,12 +91,6 @@ def get_technology(self) -> str:
     def set_technology(self, technology: str):
         self.technology = technology
 
-    def set_allocation_constraints(self, allocation_constraints: str):
-        self.allocation_constraints = allocation_constraints
-
-    def get_allocation_constraints(self) -> str:
-        return self.allocation_constraints
-
     @staticmethod
     def type_from_str(ltype: str) -> LinkType or None:
         if ltype is None:
@@ -82,9 +100,10 @@ def type_from_str(ltype: str) -> LinkType or None:
                 return t
 
     @staticmethod
-    def layer_from_str(layer: str) -> SFLayer or None:
+    def layer_from_str(layer: str) -> NSLayer or None:
         if layer is None:
             return None
-        for t in SFLayer:
+        for t in NSLayer:
             if layer == str(t):
                 return t
+
diff --git a/fim/slivers/network_node.py b/fim/slivers/network_node.py
index e11ce7a..e9e5e62 100644
--- a/fim/slivers/network_node.py
+++ b/fim/slivers/network_node.py
@@ -27,18 +27,21 @@
 import ipaddress
 
 from .base_sliver import BaseSliver
-from .attached_components import AttachedComponentsInfo
-from .interface_info import InterfaceInfo
-from .switch_fabric import SwitchFabricInfo
 
 
 class NodeType(enum.Enum):
+    """
+    Possible NetworkNode types in FABRIC.
+    """
     Server = enum.auto()
     VM = enum.auto()
     Container = enum.auto()
     Switch = enum.auto()
     NAS = enum.auto()
 
+    def help(self) -> str:
+        return 'A ' + self.name
+
     def __repr__(self):
         return self.name
 
@@ -56,7 +59,7 @@ def __init__(self):
         self.image_type = None
         self.image_ref = None
         self.service_endpoint = None
-        self.switch_fabric_info = None
+        self.network_service_info = None
         self.site = None
 
     #
diff --git a/fim/slivers/network_service.py b/fim/slivers/network_service.py
new file mode 100644
index 0000000..bf9f691
--- /dev/null
+++ b/fim/slivers/network_service.py
@@ -0,0 +1,213 @@
+#!/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 Tuple, Any
+import enum
+from recordclass import recordclass
+
+from .base_sliver import BaseSliver
+from .path_info import PathRepresentationType, ERO, PathInfo
+
+
+class NSLayer(enum.Enum):
+    L0 = enum.auto()
+    L1 = enum.auto()
+    L2 = enum.auto()
+    L3 = enum.auto()
+
+    def __repr__(self):
+        return self.name
+
+    def __str__(self):
+        return self.name
+
+
+class ServiceType(enum.Enum):
+    """
+    Possible Service types in FABRIC. Unlike links,
+    Services can be instantiated on demand.
+    """
+    P4 = enum.auto()
+    MPLS = enum.auto()
+    OVS = enum.auto() # OVS service typically inside a NIC
+    L2Path = enum.auto() # provider L2 path (overlap with link types -
+    # it may be possible to select provider path as a service)
+    L2STS = enum.auto() # FABRIC Site-to-Site service
+    L2PTP = enum.auto() # FABRIC Port-to-Port service
+    L2Multisite = enum.auto() # FABRIC multi-site service
+    L2Bridge = enum.auto() # FABRIC L2 bridge within a site
+    FABNetv4 = enum.auto() # FABRIC IPv4 routed network
+    FABNetv6 = enum.auto() # FABRIC IPv6 routed network
+    PortMirror = enum.auto() # FABRIC port mirroring service
+    L3VPN = enum.auto() # FABRIC L3 VPN service
+
+    def help(self) -> str:
+        return NetworkServiceSliver.ServiceConstraints[self].desc
+
+    def __repr__(self):
+        return self.name
+
+    def __str__(self):
+        return self.name
+
+
+ServiceConstraintRecord = recordclass('ServiceConstraintRecord',
+                                      ['layer', 'desc',
+                                       'num_interfaces',
+                                       'num_sites',
+                                       'num_instances'])
+
+
+class NetworkServiceSliver(BaseSliver):
+
+    # whenever there is no limit, num is set to 0
+    NO_LIMIT = 0
+    """
+    Services can be limited by the number of interfaces/connection points they can connect
+    The number of sites they may connect and the number of instances they may have in a slice.
+    """
+    ServiceConstraints = {
+        ServiceType.P4: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT, num_sites=1,
+                                                num_instances=NO_LIMIT,
+                                                desc='A P4 service.'),
+        ServiceType.OVS: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT, num_sites=1,
+                                                 num_instances=NO_LIMIT,
+                                                 desc='An OVS generic service.'),
+        ServiceType.MPLS: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT, num_sites=1,
+                                                  num_instances=NO_LIMIT, desc='An MPLS generic service'),
+        ServiceType.L2Path: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=2, num_sites=2,
+                                                    num_instances=NO_LIMIT,
+                                                    desc='A provider L2 Path e.g. from ESnet or Internet2.'),
+        ServiceType.L2STS: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT, num_sites=2,
+                                                   num_instances=NO_LIMIT,
+                                                   desc='A Site-to-Site service in FABRIC.'),
+        ServiceType.L2PTP: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=2, num_sites=2,
+                                                   num_instances=NO_LIMIT,
+                                                   desc='A Port-to-Port service in FABRIC.'),
+        ServiceType.L2Multisite: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT, num_sites=NO_LIMIT,
+                                                         num_instances=NO_LIMIT,
+                                                         desc='A Multi-Site L2 service in FABRIC.'),
+        ServiceType.L2Bridge: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT, num_sites=1,
+                                                      num_instances=NO_LIMIT,
+                                                      desc='An L2 bridge service within a single FABRIC site.'),
+        ServiceType.FABNetv4: ServiceConstraintRecord(layer=NSLayer.L3, num_interfaces=NO_LIMIT, num_sites=NO_LIMIT,
+                                                      num_instances=1,
+                                                      desc='A routed IPv4 (RFC1918 addressed) FABRIC network.'),
+        ServiceType.FABNetv6: ServiceConstraintRecord(layer=NSLayer.L3, num_interfaces=NO_LIMIT, num_sites=NO_LIMIT,
+                                                      num_instances=1,
+                                                      desc='A routed IPv6 (publicly addressed) FABRIC network.'),
+        ServiceType.PortMirror: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=2, num_sites=1,
+                                                        num_instances=1,
+                                                        desc='A port mirroring service in a site.'),
+        ServiceType.L3VPN: ServiceConstraintRecord(layer=NSLayer.L3, num_interfaces=NO_LIMIT, num_sites=NO_LIMIT,
+                                                   num_instances=NO_LIMIT,
+                                                   desc='A L3 VPN service connecting to FABRIC.')
+    }
+
+    def __init__(self):
+        super().__init__()
+        self.layer = None
+        self.technology = None
+        self.interface_info = None
+        self.allocation_constraints = None
+        self.ero = None
+        self.path_info = None
+        self.controller_url = None
+
+    #
+    # Setters are only needed for things we want users to be able to set
+    #
+    def get_layer(self) -> NSLayer:
+        return self.layer
+
+    def set_layer(self, layer: NSLayer):
+        self.layer = layer
+
+    def get_technology(self) -> str:
+        return self.technology
+
+    def set_technology(self, technology: str):
+        self.technology = technology
+
+    def set_allocation_constraints(self, allocation_constraints: str):
+        self.allocation_constraints = allocation_constraints
+
+    def get_allocation_constraints(self) -> str:
+        return self.allocation_constraints
+
+    def set_ero(self, ero: ERO) -> None:
+        self.ero = ero
+
+    def get_ero(self) -> ERO:
+        return self.ero
+
+    def set_path_info(self, path_info: PathInfo) -> None:
+        self.path_info = path_info
+
+    def get_path_info(self) -> PathInfo:
+        return self.path_info
+
+    def set_controller_url(self, curl: str) -> None:
+        self.controller_url = curl
+
+    def get_controller_url(self) -> None:
+        return self.controller_url
+
+    @staticmethod
+    def type_from_str(ltype: str) -> ServiceType or None:
+        if ltype is None:
+            return None
+        for t in ServiceType:
+            if ltype == str(t):
+                return t
+
+    @staticmethod
+    def layer_from_str(layer: str) -> NSLayer or None:
+        if layer is None:
+            return None
+        for t in NSLayer:
+            if layer == str(t):
+                return t
+
+
+class NetworkServiceInfo:
+
+    def __init__(self):
+        self.network_services = {}
+
+    def add_network_service(self, ns_sliver: NetworkServiceSliver):
+        self.network_services[ns_sliver.resource_name] = ns_sliver
+
+    def remove_network_service(self, name: str):
+        assert name is not None
+
+        if name in self.network_services.keys():
+            self.network_services.pop(name)
+
+    def get_network_service(self, name: str):
+        assert name is not None
+
+        return self.network_services.get(name, None)
+
diff --git a/fim/slivers/path_info.py b/fim/slivers/path_info.py
new file mode 100644
index 0000000..9635b97
--- /dev/null
+++ b/fim/slivers/path_info.py
@@ -0,0 +1,217 @@
+#!/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 List, Tuple, Any, Dict
+import enum
+
+import json
+
+
+class PathRepresentationType(enum.Enum):
+    """
+    Path Representation type - can be represented as a list or as a graph
+    """
+    Path = enum.auto()   # PathInfo is a pair of uni-directional lists
+    Graph = enum.auto()  # PathInfo is a graph reference (graph ID)
+
+    def __repr__(self):
+        return self.name
+
+    def __str__(self):
+        return self.name
+
+
+class Path:
+    """
+    Path object expressed as two lists of entities on the path
+    """
+    def __init__(self):
+        self.a2z = None
+        self.z2a = None
+
+    def set(self, *, a2z: List = None, z2a: List = None):
+        self.a2z = a2z
+        self.z2a = z2a
+
+    def set_symmetric(self, a2z: List):
+        assert a2z is not None
+        assert isinstance(a2z, list)
+        self.a2z = a2z
+        self.z2a = a2z.copy()
+        self.z2a.reverse()
+
+    def get(self) -> Tuple[List, List]:
+        """
+        Return a tuple a2z, z2a
+        :return:
+        """
+        return self.a2z, self.z2a
+
+    def to_dict(self) -> Dict:
+        return {'a2z': self.a2z, 'z2a': self.z2a}
+
+    @classmethod
+    def from_dict(cls, d: Dict):
+        assert isinstance(d, dict)
+        assert 'a2z' in d.keys() and 'z2a' in d.keys()
+        pi = cls()
+        pi.set(a2z=d['a2z'], z2a=d['z2a'])
+        return pi
+
+    def __str__(self):
+        return self.__repr__()
+
+    def __repr__(self):
+        return json.dumps(self.to_dict())
+
+
+class PathInfo:
+    """
+    PathInfo encodes information about paths. Most of the time lists (a2z, z2a) are used,
+    however we can also do it as an external graph (by reference).
+    """
+    def __init__(self, ptype: PathRepresentationType = PathRepresentationType.Path):
+        self.payload = None
+        self.type = ptype
+
+    def set(self, payload) -> None:
+        """
+        Path payload can be a Path or id of a graph built externally (string name is payload)
+        :param payload:
+        :return:
+        """
+        if self.type == PathRepresentationType.Path:
+            assert(isinstance(payload, Path))
+        else:
+            assert(isinstance(payload, str))
+        self.payload = payload
+
+    def get(self) -> Tuple[PathRepresentationType, Any]:
+        """
+        Return a tuple of type, payload of Path (tuple of lists or a graph id)
+        :return:
+        """
+        return self.type, self.payload
+
+    @staticmethod
+    def type_from_str(etype: str) -> PathRepresentationType or None:
+        if etype is None:
+            return None
+        for t in PathRepresentationType:
+            if etype == str(t):
+                return t
+
+    def to_json(self) -> str:
+        """
+        Convert to JSON dictionary:
+        { "type": "list"|"graph", "payload": <payload> }
+        payload can be a graph identifier string or a dict with 'a2z' and 'z2a' lists
+        :return:
+        """
+        json_dict = dict()
+        json_dict['type'] = str(self.type)
+        json_dict['payload'] = self.payload if self.type == PathRepresentationType.Graph else self.payload.to_dict()
+        return json.dumps(json_dict)
+
+    @classmethod
+    def from_json(cls, json_string: str):
+        """
+        Set fields from json string and returns a new object
+        :param json_string:
+        :return: object
+        """
+        if json_string is None or len(json_string) == 0:
+            return None
+        d = json.loads(json_string)
+        ptype_str = d.get('type', None)
+        if ptype_str is None:
+            return None
+
+        ptype = PathInfo.type_from_str(ptype_str)
+        ret = cls(ptype)
+        ret.payload = d['payload'] if ptype == PathRepresentationType.Graph else Path.from_dict(d['payload'])
+        return ret
+
+    def __str__(self):
+        return self.__repr__()
+
+    def __repr__(self):
+        return self.to_json()
+
+
+class ERO(PathInfo):
+    """
+    Explicit Route Object class which adds 'strict' flag compared to PathInfo
+    """
+    def __init__(self, etype: PathRepresentationType = PathRepresentationType.Path, strict: bool = False):
+        # default is 'loose' list
+        super().__init__(etype)
+        self.strict = strict
+
+    def get_strict(self) -> bool:
+        """
+        Get the ERO strict flag
+        :return:
+        """
+        return self.strict
+
+    def to_json(self) -> str:
+        """
+        Convert to JSON dictionary:
+        { "type": "Path"|"Graph", "payload": <payload> }
+        payload can be a graph identifier string or a dict with 'a2z' and 'z2a' lists
+        :return:
+        """
+        json_dict = dict()
+        json_dict['type'] = str(self.type)
+        json_dict['strict'] = str(self.strict)
+        json_dict['payload'] = self.payload if self.type == PathRepresentationType.Graph else self.payload.to_dict()
+        return json.dumps(json_dict)
+
+    @classmethod
+    def from_json(cls, json_string: str):
+        """
+        Set fields from json string and returns a new object
+        :param json_string:
+        :return: object
+        """
+        if json_string is None or len(json_string) == 0:
+            return None
+        d = json.loads(json_string)
+        ptype_str = d.get('type', None)
+        if ptype_str is None:
+            return None
+
+        ptype = PathInfo.type_from_str(ptype_str)
+        ret = cls(ptype)
+        ret.payload = d['payload'] if ptype == PathRepresentationType.Graph else Path.from_dict(d['payload'])
+        ret.strict = True if d.get('strict', None) in {'True', 'true'} else False
+        return ret
+
+    def __str__(self):
+        return self.__repr__()
+
+    def __repr__(self):
+        return self.to_json()
diff --git a/fim/slivers/switch_fabric.py b/fim/slivers/switch_fabric.py
deleted file mode 100644
index 7dc5cc0..0000000
--- a/fim/slivers/switch_fabric.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python3
-# MIT License
-#
-# Copyright (c) 2021 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)
-import enum
-
-from .base_sliver import BaseSliver
-from .interface_info import InterfaceInfo
-
-
-class SFLayer(enum.Enum):
-    L1 = enum.auto()
-    L2 = enum.auto()
-    L3 = enum.auto()
-
-    def __repr__(self):
-        return self.name
-
-    def __str__(self):
-        return self.name
-
-
-class SFType(enum.Enum):
-    # only one subtype for now
-    SwitchFabric = enum.auto()
-
-    def __repr__(self):
-        return self.name
-
-    def __str__(self):
-        return self.name
-
-
-class SwitchFabricSliver(BaseSliver):
-    """
-    SwitchFabrics are typically invisible to everyone, but provide
-    the abstraction of switching across some number of ports
-    """
-
-    def __init__(self):
-        super().__init__()
-        self.layer = None
-        self.interface_info = None
-
-    def set_layer(self, layer: SFLayer):
-        self.layer = layer
-
-    def get_layer(self) -> SFLayer:
-        return self.layer
-
-    @staticmethod
-    def layer_from_str(layer: str) -> SFLayer:
-        if layer is None:
-            return None
-        for t in SFLayer:
-            if layer == str(t):
-                return t
-
-    @staticmethod
-    def type_from_str(ntype: str) -> SFType:
-        if ntype is None:
-            return None
-        for t in SFType:
-            if ntype == str(t):
-                return t
-
-
-class SwitchFabricInfo:
-
-    def __init__(self):
-        self.switch_fabrics = {}
-
-    def add_switch_fabric(self, sf_info: SwitchFabricSliver):
-        self.switch_fabrics[sf_info.resource_name] = sf_info
-
-    def remove_switch_fabric(self, name: str):
-        assert name is not None
-
-        if name in self.switch_fabrics.keys():
-            self.switch_fabrics.pop(name)
-
-    def get_switch_fabric(self, name: str):
-        assert name is not None
-
-        return self.switch_fabrics.get(name, None)
diff --git a/fim/user/README.md b/fim/user/README.md
index b6f9fe2..d87ecf9 100644
--- a/fim/user/README.md
+++ b/fim/user/README.md
@@ -40,13 +40,14 @@ Some example scripts that *should* run.
 
 ```python
 import fim.user as fu
+
 t = fu.ExperimentTopology()
 n1 = t.add_node(name='n1', site='RENC')
 t.add_node(name='n2', site='RENC')
 cap = fu.Capacities().set_fields(ram=1000, core=8)
 n1.set_property('capacities', cap)
 nic1 = t.nodes['n1'].add_component(ctype=fu.ComponentType.SharedNIC, model='ConnectX-6', name='nic1')
-nic2 = t.nodes['n2'].add_component(ctype=fu.ComponentType.SharedNIC, model='ConnectX-6', name='nic2')
+nic2 = t.nodes['n2'].add_component(ctype=fu.ComponentType.SmartNIC, model='ConnectX-6', name='nic2')
 cap = fu.Capacities()
 cap.set_fields(bw=50, unit=1)
 lab = fu.Labels()
@@ -54,7 +55,7 @@ lab.set_fields(ipv4="192.168.1.12")
 nic1.set_properties(capacities=cap, labels=lab)
 nic1.get_property('capacities')
 nic1.unset_property('labels')
-t.add_link(name='l1', interfaces=t.interface_list, ltype=fu.LinkType.Wave)
+t.add_network_service(name='s1', interfaces=t.interface_list, ltype=fu.ServiceType.L2Bridge)
 t.draw()
 t.serialize('test_slice.graphml')
 print(t)
diff --git a/fim/user/__init__.py b/fim/user/__init__.py
index e0df728..3a0edcd 100644
--- a/fim/user/__init__.py
+++ b/fim/user/__init__.py
@@ -2,9 +2,10 @@
 import fim.user.node as node
 import fim.user.link as link
 import fim.user.component as comp
-import fim.user.switch_fabric as sf
+import fim.user.network_service as ns
 import fim.slivers.network_node as nnode
 import fim.slivers.network_link as nlink
+import fim.slivers.network_service as nss
 import fim.slivers.attached_components as atcomp
 import fim.slivers.component_catalog as cata
 import fim.slivers.interface_info as iinfo
@@ -26,9 +27,11 @@
 Capacities = caplab.Capacities
 Labels = caplab.Labels
 ReservationInfo = caplab.ReservationInfo
-Layer = sf.SFLayer
+Layer = nss.NSLayer
 Link = link.Link
 LinkType = nlink.LinkType
+ServiceType = ns.ServiceType
+NetworkService = ns.NetworkService
 TopologyDetail = topo.TopologyDetail
 Delegation = dlg.Delegation
 Delegations = dlg.Delegations
diff --git a/fim/user/component.py b/fim/user/component.py
index f442cf4..7ecdd6c 100644
--- a/fim/user/component.py
+++ b/fim/user/component.py
@@ -31,10 +31,11 @@
 from fim.view_only_dict import ViewOnlyDict
 from ..graph.abc_property_graph import ABCPropertyGraph
 from .model_element import ModelElement, ElementType
+from .network_service import NetworkService, ServiceType
 from .interface import Interface
-from .switch_fabric import SwitchFabric, SFLayer
 from ..slivers.capacities_labels import Labels
 from ..slivers.attached_components import ComponentSliver, ComponentType
+from ..slivers.network_service import NSLayer
 from ..slivers.component_catalog import ComponentCatalog
 
 
@@ -45,13 +46,13 @@ class Component(ModelElement):
     return various dictionaries or lists:
     component.interfaces - a dictionary of interfaces
     component.interface_list - a list of interfaces
-    component.switch_fabrics - a dictionary of switch fabrics
+    component.network_services - a dictionary of network services
     """
 
     def __init__(self, *, name: str, node_id: str = None, topo: Any,
                  etype: ElementType = ElementType.EXISTING, model: str = None,
                  ctype: ComponentType = None, parent_node_id: str = None,
-                 switch_fabric_node_id: str = None, interface_node_ids: List[str] = None,
+                 network_service_node_id: str = None, interface_node_ids: List[str] = None,
                  interface_labels: List[Labels] = None, **kwargs):
         """
         Don't call this yourself, use Node.add_component(). Instantiates components based on
@@ -63,7 +64,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
         :param model: must be specified if a new component
         :param ctype: component type
         :param parent_node_id: node_id of the parent Node (for new components)
-        :param switch_fabric_node_id: node id of switch fabric if one needs to be added (for substrate models only)
+        :param network_service_node_id: node id of network_service if one needs to be added (for substrate models only)
         :param interface_node_ids: a list of node ids for expected interfaces (for substrate models only)
         :param interface_labels: a list of Labels structure to associate with each interface
         """
@@ -83,15 +84,15 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
                 raise RuntimeError("Model and component type must be specified for new components")
             if str(topo.__class__) == "<class 'fim.user.topology.SubstrateTopology'>" and \
                     (ctype == ComponentType.SharedNIC or ctype == ComponentType.SmartNIC) and \
-                    (switch_fabric_node_id is None or interface_node_ids is None or interface_labels is None):
+                    (network_service_node_id is None or interface_node_ids is None or interface_labels is None):
                 raise RuntimeError('For substrate topologies and components with network interfaces '
-                                   'static switch_fabric node id, interface node ids and interface labels'
+                                   'static network service node id, interface node ids and interface labels'
                                    'must be specified')
             super().__init__(name=name, node_id=node_id, topo=topo)
             cata = ComponentCatalog()
 
             comp_sliver = cata.generate_component(name=name, model=model, ctype=ctype,
-                                                  switch_fabric_node_id=switch_fabric_node_id,
+                                                  ns_node_id=network_service_node_id,
                                                   interface_node_ids=interface_node_ids,
                                                   interface_labels=interface_labels)
             comp_sliver.node_id = node_id
@@ -156,17 +157,17 @@ def __get_interface_by_id(self, node_id: str) -> Interface:
         return Interface(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id,
                          topo=self.topo)
 
-    def __get_sf_by_id(self, node_id: str) -> SwitchFabric:
+    def __get_ns_by_id(self, node_id: str) -> NetworkService:
         """
-        Get an switch fabric of a node by its node_id, return SwitchFabric object
+        Get an network service of a node by its node_id, return NetworkService object
         :param node_id:
         :return:
         """
         assert node_id is not None
         _, node_props = self.topo.graph_model.get_node_properties(node_id=node_id)
         assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
-        return SwitchFabric(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id,
-                            topo=self.topo)
+        return NetworkService(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id,
+                              topo=self.topo)
 
     def __list_interfaces(self) -> ViewOnlyDict:
         """
@@ -194,18 +195,18 @@ def __list_of_interfaces(self) -> Tuple[Interface]:
             ret.append(i)
         return tuple(ret)
 
-    def __list_switch_fabrics(self) -> ViewOnlyDict:
+    def __list_network_services(self) -> ViewOnlyDict:
         """
-        List all switch fabric children of a node as a dictionary organized
-        by switch fabric name. Modifying the dictionary will not affect
+        List all network service children of a node as a dictionary organized
+        by network service name. Modifying the dictionary will not affect
         the underlying model, but modifying Components in the dictionary will.
         :return:
         """
-        node_id_list = self.topo.graph_model.get_all_network_node_or_component_sfs(parent_node_id=self.node_id)
+        node_id_list = self.topo.graph_model.get_all_network_node_or_component_nss(parent_node_id=self.node_id)
         # Could consider using frozendict or other immutable idioms
         ret = dict()
         for nid in node_id_list:
-            c = self.__get_sf_by_id(nid)
+            c = self.__get_ns_by_id(nid)
             ret[c.name] = c
         return ViewOnlyDict(ret)
 
@@ -222,8 +223,8 @@ def __getattr__(self, item):
             return self.__list_interfaces()
         if item == 'interface_list':
             return self.__list_of_interfaces()
-        if item == 'switch_fabrics':
-            return self.__list_switch_fabrics()
+        if item == 'network_services':
+            return self.__list_network_services()
         raise RuntimeError(f'Attribute {item} not available')
 
     def __repr__(self):
diff --git a/fim/user/interface.py b/fim/user/interface.py
index a4a6231..4b1849d 100644
--- a/fim/user/interface.py
+++ b/fim/user/interface.py
@@ -48,7 +48,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
         :param node_id:
         :param topo:
         :param etype: is this supposed to exist or new should be created
-        :param parent_node_id: parent switch fabric or parent interface
+        :param parent_node_id: parent network service or parent interface
         :param itype: node type if it is new
         :param kwargs: any additional properties
         """
diff --git a/fim/user/link.py b/fim/user/link.py
index 0cf8e8c..1785d44 100644
--- a/fim/user/link.py
+++ b/fim/user/link.py
@@ -32,19 +32,18 @@
 from fim.graph.abc_property_graph import ABCPropertyGraph
 from fim.user.interface import Interface
 from fim.slivers.network_link import NetworkLinkSliver, LinkType
-from fim.slivers.switch_fabric import SFLayer
 
 
 class Link(ModelElement):
     """
-    A link object in a topology. In addition to public methods the following calls
+    A link object in a topology connecting interfaces together.
+    In addition to public methods the following calls
     return various dictionaries or lists:
     link.interface_list - a list of interfaces attached to this link
     """
     def __init__(self, *, name: str, node_id: str = None, topo: Any,
                  etype: ElementType = ElementType.EXISTING,
                  interfaces: List[Interface] = None,
-                 layer: SFLayer = SFLayer.L2,
                  ltype: LinkType = None, technology: str = None, **kwargs):
         """
         Don't call this method yourself, call topology.add_link()
@@ -55,7 +54,6 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
         :param topo:
         :param etype: is this supposed to exist or new should be created
         :param interfaces: list of interface objects to connect
-        :param layer: layer of the link, defaults to L2
         :param ltype: link type if new
         :param technology: link technology
         :param kwargs: any additional properties
@@ -77,14 +75,14 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
             # FIXME isinstance
             if interfaces is None or len(interfaces) == 0 or (not isinstance(interfaces, tuple) and
                                                               not isinstance(interfaces, list)):
-                raise RuntimeError("When creating new links you must specify the list of interfaces to connect")
-
+                raise RuntimeError("When creating new links you must specify the list of interfaces to connect.")
             self._interfaces = interfaces
             sliver = NetworkLinkSliver()
             sliver.node_id = self.node_id
             sliver.set_name(self.name)
             sliver.set_type(ltype)
-            sliver.set_layer(layer)
+            # set layer based on type
+            sliver.set_layer(NetworkLinkSliver.LinkConstraints[ltype].layer)
             sliver.set_technology(technology)
             sliver.set_properties(**kwargs)
 
@@ -101,7 +99,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
             if existing_node_id != node_id:
                 raise RuntimeError(f'Link name {name} is not unique within the topology.')
             # collect a list of interfaces it attaches to
-            interface_list = self.topo.graph_model.get_all_link_interfaces(link_id=self.node_id)
+            interface_list = self.topo.graph_model.get_all_ns_or_link_connection_points(link_id=self.node_id)
             name_id_tuples = list()
             # need to look up their names - a bit inefficient, need to think about this /ib
             for iff in interface_list:
@@ -111,7 +109,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
 
     def get_property(self, pname: str) -> Any:
         """
-        Retrieve a interface property
+        Retrieve a link property
         :param pname:
         :return:
         """
@@ -121,7 +119,7 @@ def get_property(self, pname: str) -> Any:
 
     def set_property(self, pname: str, pval: Any):
         """
-        Set a interface property
+        Set a link property
         :param pname:
         :param pval:
         :return:
diff --git a/fim/user/network_service.py b/fim/user/network_service.py
new file mode 100644
index 0000000..705d4b5
--- /dev/null
+++ b/fim/user/network_service.py
@@ -0,0 +1,339 @@
+#!/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 Tuple, Any, List
+
+import uuid
+
+from fim.view_only_dict import ViewOnlyDict
+
+from .model_element import ModelElement, ElementType
+
+from fim.graph.abc_property_graph import ABCPropertyGraph
+from fim.user.interface import Interface, InterfaceType
+from fim.user.link import Link, LinkType
+from fim.slivers.network_service import NetworkServiceSliver, ServiceType
+
+
+class NetworkService(ModelElement):
+    """
+    A network service object in a topology that can pass packets between interfaces.
+    Unlike a link, network service owns its interfaces. Typical topology fragment is
+    component - network service - interface - link - interface - network service
+    In addition to public methods the following calls
+    return various dictionaries or lists:
+    service.interface_list - a list of interfaces attached to this service
+    """
+    def __init__(self, *, name: str, node_id: str = None, topo: Any,
+                 etype: ElementType = ElementType.EXISTING,
+                 parent_node_id: str = None,
+                 interfaces: List[Interface] = None,
+                 nstype: ServiceType = None, technology: str = None, **kwargs):
+        """
+        Don't call this method yourself, call topology.add_network_service()
+        node_id will be generated if not provided for experiment topologies
+
+        :param name:
+        :param node_id:
+        :param topo:
+        :param etype: is this supposed to exist or new should be created
+        :param parent_node_id: node_id of the parent Node if any (for new components)
+        :param interfaces: list of interface objects to connect
+        :param nstype: service type if new
+        :param technology: service technology
+        :param kwargs: any additional properties
+        """
+        assert name is not None
+        assert topo is not None
+
+        if etype == ElementType.NEW:
+            # node id myst be specified for new nodes in substrate topologies
+            if node_id is None:
+                node_id = str(uuid.uuid4())
+            super().__init__(name=name, node_id=node_id, topo=topo)
+            if nstype is None:
+                raise RuntimeError("When creating new services you must specify ServiceType")
+            # cant use isinstance as it would create circular import dependencies
+            # We do more careful checks for ExperimentTopologies, but for substrate we let things loose
+            if str(self.topo.__class__) == "<class 'fim.user.topology.ExperimentTopology'>":
+                if interfaces is None or len(interfaces) == 0 or (not isinstance(interfaces, tuple) and
+                                                                  not isinstance(interfaces, list)):
+                    raise RuntimeError("When creating new services in ExperimentTopology you "
+                                       "must specify the list of interfaces to connect.")
+                # check the number of instances of this service
+                if NetworkServiceSliver.ServiceConstraints[nstype].num_instances != NetworkServiceSliver.NO_LIMIT:
+                    services = topo.graph_model.get_all_nodes_by_class_and_type(label=ABCPropertyGraph.CLASS_NetworkService,
+                                                                                ntype=str(nstype))
+                    if len(services) + 1 > NetworkServiceSliver.ServiceConstraints[nstype].num_instances:
+                        raise RuntimeError(f"Service type {nstype} cannot have {len(services) + 1} instances. "
+                                           f"Limit: {NetworkServiceSliver.ServiceConstraints[nstype].num_instances}")
+                # check the number of interfaces
+                if NetworkServiceSliver.ServiceConstraints[nstype].num_interfaces != NetworkServiceSliver.NO_LIMIT:
+                    if len(interfaces) > NetworkServiceSliver.ServiceConstraints[nstype].num_interfaces:
+                        raise RuntimeError(f"Service of type {nstype} cannot have {len(interfaces)} interfaces. "
+                                           f"Limit: {NetworkServiceSliver.ServiceConstraints[nstype].num_interfaces}")
+                # check the number of sites spanned by this service
+                if NetworkServiceSliver.ServiceConstraints[nstype].num_sites != NetworkServiceSliver.NO_LIMIT:
+                    # trace ownership of each interface and count the sites involved
+                    sites = set()
+                    for interface in interfaces:
+                        owner = topo.get_owner_node(interface)
+                        sites.add(owner.get_property('site'))
+                    if len(sites) > NetworkServiceSliver.ServiceConstraints[nstype].num_sites:
+                        raise RuntimeError(f"Service of type {nstype} cannot span {len(sites)} sites. "
+                                           f"Limit: {NetworkServiceSliver.ServiceConstraints[nstype].num_sites}.")
+            sliver = NetworkServiceSliver()
+            sliver.node_id = self.node_id
+            sliver.set_name(self.name)
+            sliver.set_type(nstype)
+            # set based on service type
+            sliver.set_layer(NetworkServiceSliver.ServiceConstraints[nstype].layer)
+            sliver.set_technology(technology)
+            sliver.set_properties(**kwargs)
+            self.topo.graph_model.add_network_service_sliver(parent_node_id=parent_node_id, network_service=sliver)
+            self._interfaces = list()
+            if interfaces is not None and len(interfaces) > 0:
+                for i in interfaces:
+                    self.connect_interface(interface=i)
+        else:
+            assert node_id is not None
+            super().__init__(name=name, node_id=node_id, topo=topo)
+            # check that this node exists
+            existing_node_id = self.topo.\
+                graph_model.find_node_by_name(node_name=name,
+                                              label=ABCPropertyGraph.CLASS_NetworkService)
+            if existing_node_id != node_id:
+                raise RuntimeError(f'Service name {name} is not unique within the topology.')
+            # collect a list of interface nodes it attaches to
+            interface_list = self.topo.graph_model.get_all_ns_or_link_connection_points(link_id=self.node_id)
+            name_id_tuples = list()
+            # need to look up their names - a bit inefficient, need to think about this /ib
+            for iff in interface_list:
+                _, props = self.topo.graph_model.get_node_properties(node_id=iff)
+                name_id_tuples.append((props[ABCPropertyGraph.PROP_NAME], iff))
+            self._interfaces = [Interface(node_id=tup[1], topo=topo, name=tup[0]) for tup in name_id_tuples]
+
+    def connect_interface(self, *, interface: Interface):
+        """
+        Connect a (compute or switch) node interface to network service by transparently
+        creating a peer service interface and a link between them
+        :param interface
+        :return:
+        """
+        assert interface is not None
+        assert isinstance(interface, Interface)
+
+        # we can only connect interfaces connected to (compute or switch) nodes
+        if self.topo.get_owner_node(interface) is None:
+            raise RuntimeError(f'Interface {interface} is not owned by a node, as expected.')
+        # we can only connect interfaces that aren't already connected
+        peer_id = self.topo.graph_model.find_peer_connection_point(node_id=interface.node_id)
+        if peer_id is not None:
+            raise RuntimeError(f'Interface {interface} is already connected to another service.')
+        # create a peer interface, create a link between them
+        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.get_property('type') == InterfaceType.SharedPort:
+            ltype = LinkType.L2Path
+        else:
+            ltype = LinkType.Patch
+
+        peer_link = Link(name=peer_if.name + '-link', topo=self.topo, etype=ElementType.NEW,
+                         interfaces=[interface, peer_if], ltype=ltype)
+        self._interfaces.append(peer_if)
+
+    def disconnect_interface(self, *, interface: Interface) -> None:
+        """
+        Disconnect a node interface from the network service.
+        Transparently remove peer service interface and link between them.
+        :param interface:
+        :return:
+        """
+        assert interface is not None
+
+        # find parent network service
+        owner = self.topo.get_parent_element(interface)
+        # find id of the interface node
+        interface_node_id = self.topo.graph_model.find_connection_point_by_name(parent_node_id=owner.node_id,
+                                                                                iname=interface.name)
+        node_id = self.topo.graph_model.find_peer_connection_point(node_id=interface_node_id)
+        if node_id is None:
+            return
+
+        self.topo.graph_model.remove_cp_and_links(node_id=node_id)
+        # remove from interfaces list as well
+        to_remove = None
+        for i in self._interfaces:
+            if i.node_id == node_id:
+                to_remove = i
+        if to_remove is not None:
+            self._interfaces.remove(to_remove)
+
+    def add_interface(self, *, name: str, node_id: str = None, itype: InterfaceType = InterfaceType.TrunkPort,
+                      **kwargs):
+        """
+        Add an interface to node (mostly needed in substrate topologies)
+        :param name:
+        :param node_id:
+        :param itype: interface type e.g. TrunkPort, AccessPort or VINT
+        :param kwargs: additional parameters
+        :return:
+        """
+        assert name is not None
+        # cant use isinstance as it would create circular import dependencies
+        if str(self.topo.__class__) == "<class 'fim.user.topology.ExperimentTopology'>":
+            raise RuntimeError("Do not need to add interface to NetworkService in Experiment topology")
+
+        # check uniqueness
+        if name in self.__list_interfaces().keys():
+            raise RuntimeError('Interface names must be unique within a switch fabric')
+        iff = Interface(name=name, node_id=node_id, parent_node_id=self.node_id,
+                        etype=ElementType.NEW, topo=self.topo, itype=itype,
+                        **kwargs)
+        return iff
+
+    def remove_interface(self, *, name: str) -> None:
+        """
+        Remove an interface from the switch fabric, disconnect from links (in substrate topologies). Remove links
+        if they have nothing else connecting to them.
+        :param name:
+        :return:
+        """
+        assert name is not None
+        # cant use isinstance as it would create circular import dependencies
+        if str(self.topo.__class__) == "<class 'fim.user.topology.ExperimentTopology'>":
+            raise RuntimeError("Cannot remove interface from NetworkService in Experiment topology")
+        node_id = self.topo.graph_model.find_connection_point_by_name(parent_node_id=self.node_id,
+                                                                      iname=name)
+        self.topo.graph_model.remove_cp_and_links(node_id=node_id)
+
+    def get_property(self, pname: str) -> Any:
+        """
+        Retrieve a service property
+        :param pname:
+        :return:
+        """
+        _, node_properties = self.topo.graph_model.get_node_properties(node_id=self.node_id)
+        if_sliver = self.topo.graph_model.network_service_sliver_from_graph_properties_dict(node_properties)
+        return if_sliver.get_property(pname)
+
+    def set_property(self, pname: str, pval: Any):
+        """
+        Set a service property
+        :param pname:
+        :param pval:
+        :return:
+        """
+        service_sliver = NetworkServiceSliver()
+        service_sliver.set_property(prop_name=pname, prop_val=pval)
+        # write into the graph
+        prop_dict = self.topo.graph_model.network_service_sliver_to_graph_properties_dict(service_sliver)
+        self.topo.graph_model.update_node_properties(node_id=self.node_id, props=prop_dict)
+
+    def set_properties(self, **kwargs):
+        """
+        Set multiple properties of the service
+        :param kwargs:
+        :return:
+        """
+        service_sliver = NetworkServiceSliver()
+        service_sliver.set_properties(**kwargs)
+        # write into the graph
+        prop_dict = self.topo.graph_model.network_service_sliver_to_graph_properties_dict(service_sliver)
+        self.topo.graph_model.update_node_properties(node_id=self.node_id, props=prop_dict)
+
+    @staticmethod
+    def list_properties() -> Tuple[str]:
+        return NetworkServiceSliver.list_properties()
+
+    def __get_interface_by_id(self, node_id: str) -> Interface:
+        """
+        Get an interface of network service by its node_id, return Interface object
+        :param node_id:
+        :return:
+        """
+        assert node_id is not None
+        _, node_props = self.topo.graph_model.get_node_properties(node_id=node_id)
+        assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
+        return Interface(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id,
+                         topo=self.topo)
+
+    def __get_interface_by_name(self, name: str) -> Interface:
+        """
+        Get an interface of network service by its name
+        :param name:
+        :return:
+        """
+        assert name is not None
+        node_id = self.topo.graph_model.find_connection_point_by_name(parent_node_id=self.node_id,
+                                                                      iname=name)
+        return Interface(name=name, node_id=node_id, topo=self.topo)
+
+    def __list_interfaces(self) -> ViewOnlyDict:
+        """
+        List all interfaces of the network service as a dictionary
+        :return:
+        """
+        node_id_list = self.topo.graph_model.get_all_ns_or_link_connection_points(link_id=self.node_id)
+        ret = dict()
+        for nid in node_id_list:
+            c = self.__get_interface_by_id(nid)
+            ret[c.name] = c
+        return ViewOnlyDict(ret)
+
+    def __list_of_interfaces(self) -> Tuple[Interface]:
+        """
+        Return a list of all interfaces of network service
+        :return:
+        """
+
+        return tuple(self._interfaces)
+
+    def __getattr__(self, item):
+        """
+        Special handling for attributes like 'nodes' and 'links' -
+        which query into the model. They return dicts and list
+        containers. Modifying containers does not affect the underlying
+        graph mode, but modifying elements of lists or values of dicts does.
+        :param item:
+        :return:
+        """
+
+        if item == 'interface_list':
+            return self.__list_of_interfaces()
+        if item == 'interfaces':
+            return self.__list_interfaces()
+
+        raise RuntimeError(f'Attribute {item} not available')
+
+    def __repr__(self):
+        _, node_properties = self.topo.graph_model.get_node_properties(node_id=self.node_id)
+        service_sliver = self.topo.graph_model.network_service_sliver_from_graph_properties_dict(node_properties)
+        interface_names = [iff.name for iff in self._interfaces]
+        return service_sliver.__repr__() + str(interface_names)
+
+    def __str__(self):
+        return self.__repr__()
\ No newline at end of file
diff --git a/fim/user/node.py b/fim/user/node.py
index 9975eb9..32a4dda 100644
--- a/fim/user/node.py
+++ b/fim/user/node.py
@@ -31,14 +31,14 @@
 from fim.view_only_dict import ViewOnlyDict
 from .model_element import ModelElement, ElementType
 from .component import Component, ComponentType
-from .switch_fabric import SwitchFabric
+from .network_service import NetworkService
 from .interface import Interface
 
 from ..graph.abc_property_graph import ABCPropertyGraph, PropertyGraphQueryException
 
 from ..slivers.network_node import NodeSliver
 from ..slivers.network_node import NodeType
-from ..slivers.switch_fabric import SFLayer
+from ..slivers.network_service import ServiceType
 
 
 class Node(ModelElement):
@@ -51,7 +51,7 @@ class Node(ModelElement):
     direct_interfaces - returns a dictionary of direct interfaces, i.e. those
     not attached to components (mostly relevant to switches, not servers
     or VMs)
-    switch_fabrics - returns a dictionary of switch fabrics (mostly relevant
+    network_services - returns a dictionary of network_services (mostly relevant
     to switches, not servers or VMs)
     """
 
@@ -143,17 +143,17 @@ def list_properties() -> Tuple[str]:
         return tuple(NodeSliver.list_properties())
 
     def add_component(self, *, name: str, node_id: str = None, ctype: ComponentType,
-                      model: str,  switch_fabric_node_id: str=None, interface_node_ids=None,
+                      model: str, network_service_node_id: str=None, interface_node_ids=None,
                       interface_labels=None, **kwargs) -> Component:
         """
         Add a component of specified type, model and name to this node. When working with substrate
-        topologies you must specify the switch_fabric_node_id and provide a list of interface node ids.
+        topologies you must specify the network_service_node_id and provide a list of interface node ids.
         :param name:
         :param node_id:
         :param ctype:
         :param model:
         :param name:
-        :param switch_fabric_node_id: switch fabric identifier for substrate models
+        :param network_service_node_id: network service identifier for substrate models
         :param interface_node_ids: interface identifiers for substrate models
         :param interface_labels: list of labels for interfaces in substrate models
         :param kwargs: additional properties of the component
@@ -165,14 +165,14 @@ def add_component(self, *, name: str, node_id: str = None, ctype: ComponentType,
             raise RuntimeError('Component names must be unique within node.')
         # add component node and populate properties
         c = Component(name=name, node_id=node_id, topo=self.topo, etype=ElementType.NEW,
-                      ctype=ctype, model=model, switch_fabric_node_id=switch_fabric_node_id,
+                      ctype=ctype, model=model, network_service_node_id=network_service_node_id,
                       interface_node_ids=interface_node_ids, interface_labels=interface_labels,
                       parent_node_id=self.node_id, **kwargs)
         return c
 
-    def add_switch_fabric(self, *, name: str, node_id: str = None, layer: SFLayer, **kwargs):
+    def add_network_service(self, *, name: str, node_id: str = None, nstype: ServiceType, **kwargs) -> NetworkService:
         """
-        Add a switch fabric to node (mostly needed in substrate topologies)
+        Add a network service to node (mostly needed in substrate topologies)
         :param name:
         :param node_id:
         :param layer:
@@ -181,34 +181,34 @@ def add_switch_fabric(self, *, name: str, node_id: str = None, layer: SFLayer, *
         """
         assert name is not None
         # make sure name is unique within the node
-        if name in self.__list_switch_fabrics().keys():
-            raise RuntimeError('SwitchFabric names must be unique within node.')
-        sf = SwitchFabric(name=name, node_id=node_id, layer=layer, parent_node_id=self.node_id,
-                          etype=ElementType.NEW, topo=self.topo, **kwargs)
-        return sf
+        if name in self.__list_network_services().keys():
+            raise RuntimeError('NetworkService names must be unique within node.')
+        ns = NetworkService(name=name, node_id=node_id, parent_node_id=self.node_id, topo=self.topo,
+                            etype=ElementType.NEW, nstype=nstype, **kwargs)
+        return ns
 
     def remove_component(self, name: str) -> None:
         """
-        Remove a component from the node (and switch fabrics and their interfaces)
+        Remove a component from the node (and network services and their interfaces)
         :param name:
         :return:
         """
         assert name is not None
         node_id = self.topo.graph_model.find_component_by_name(parent_node_id=self.node_id,
                                                                component_name=name)
-        self.topo.graph_model.remove_component_with_sfs_cps_and_links(node_id=node_id)
+        self.topo.graph_model.remove_component_with_nss_cps_and_links(node_id=node_id)
 
-    def remove_switch_fabric(self, name: str) -> None:
+    def remove_network_service(self, name: str) -> None:
         """
-        Remove a switch fabric from the node (and all its interfaces)
+        Remove a network service from the node (and all its interfaces)
         :param name:
         :return:
         """
         assert name is not None
 
-        node_id = self.topo.graph_model.find_sf_by_name(parent_node_id=self.node_id,
-                                                        sfname=name)
-        self.topo.graph_model.remove_sf_with_cps_and_links(node_id=node_id)
+        node_id = self.topo.graph_model.find_ns_by_name(parent_node_id=self.node_id,
+                                                        nsname=name)
+        self.topo.graph_model.remove_ns_with_cps_and_links(node_id=node_id)
 
     def __get_component_by_name(self, name: str) -> Component:
         """
@@ -233,27 +233,27 @@ def _get_component_by_id(self, node_id: str) -> Component:
         return Component(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id,
                          topo=self.topo)
 
-    def __get_sf_by_name(self, name: str) -> SwitchFabric:
+    def __get_ns_by_name(self, name: str) -> NetworkService:
         """
-        Find SwitchFabric of a node by its name, return SwitchFabric object
+        Find NetworkService of a node by its name, return NetworkService object
         :param name:
         :return:
         """
         assert name is not None
-        node_id = self.topo.graph_model.find_sf_by_name(parent_node_id=self.node_id, component_name=name)
-        return SwitchFabric(name=name, node_id=node_id, topo=self.topo)
+        node_id = self.topo.graph_model.find_ns_by_name(parent_node_id=self.node_id, component_name=name)
+        return NetworkService(name=name, node_id=node_id, topo=self.topo)
 
-    def __get_sf_by_id(self, node_id: str) -> SwitchFabric:
+    def __get_ns_by_id(self, node_id: str) -> NetworkService:
         """
-        Get a switch fabric of a node by its node_id, return SwitchFabric object
+        Get a network service of a node by its node_id, return NetworkService object
         :param node_id:
         :return:
         """
         assert node_id is not None
         _, node_props = self.topo.graph_model.get_node_properties(node_id=node_id)
         assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
-        return SwitchFabric(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id,
-                            topo=self.topo)
+        return NetworkService(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id,
+                              topo=self.topo)
 
     def __get_interface_by_id(self, node_id: str) -> Interface:
         """
@@ -282,17 +282,17 @@ def __list_components(self) -> ViewOnlyDict:
             ret[c.name] = c
         return ViewOnlyDict(ret)
 
-    def __list_switch_fabrics(self) -> ViewOnlyDict:
+    def __list_network_services(self) -> ViewOnlyDict:
         """
-        List all switch fabric children of a node as a dictionary organized
-        by switch fabric name. Modifying the dictionary will not affect
-        the underlying model, but modifying Components in the dictionary will.
+        List all network service children of a node as a dictionary organized
+        by network service name. Modifying the dictionary will not affect
+        the underlying model, but modifying NetworkServices in the dictionary will.
         :return:
         """
-        node_id_list = self.topo.graph_model.get_all_network_node_or_component_sfs(parent_node_id=self.node_id)
+        node_id_list = self.topo.graph_model.get_all_network_node_or_component_nss(parent_node_id=self.node_id)
         ret = dict()
         for nid in node_id_list:
-            c = self.__get_sf_by_id(nid)
+            c = self.__get_ns_by_id(nid)
             ret[c.name] = c
         return ViewOnlyDict(ret)
 
@@ -358,8 +358,8 @@ def __getattr__(self, item):
             return self.__list_of_interfaces()
         if item == 'direct_interfaces':
             return self.__list_direct_interfaces()
-        if item == 'switch_fabrics':
-            return self.__list_switch_fabrics()
+        if item == 'network_services':
+            return self.__list_network_services()
         raise RuntimeError(f'Attribute {item} not available')
 
     def __repr__(self):
diff --git a/fim/user/switch_fabric.py b/fim/user/switch_fabric.py
deleted file mode 100644
index cd16141..0000000
--- a/fim/user/switch_fabric.py
+++ /dev/null
@@ -1,227 +0,0 @@
-#!/usr/bin/env python3
-# MIT License
-#
-# Copyright (c) 2021 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 Any, List, Dict, Tuple
-import uuid
-
-from fim.view_only_dict import ViewOnlyDict
-from .model_element import ModelElement, ElementType
-from .interface import Interface
-from ..slivers.switch_fabric import SwitchFabricSliver, SFType, SFLayer
-from ..slivers.interface_info import InterfaceType
-from ..graph.abc_property_graph import ABCPropertyGraph
-
-
-class SwitchFabric(ModelElement):
-    """
-    SwitchFabric class
-    """
-    def __init__(self, *, name: str, node_id: str = None, topo: Any,
-                 etype: ElementType = ElementType.EXISTING,
-                 layer: SFLayer = SFLayer.L2, parent_node_id: str = None,
-                 **kwargs):
-        """
-        Create a new switch fabric. Don't call this yourself, call Node.add_switch_fabric().
-        NodeID is mandatory in substrate models.
-        :param name:
-        :param node_id:
-        :param topo:
-        :param layer: layer of the switch fabric (defaults to L2)
-        :param parent_node_id: id of parent node for new nodes
-        :param etype:
-        :param kwargs:
-        """
-        assert name is not None
-        assert topo is not None
-
-        if etype == ElementType.NEW:
-            # cant use isinstance as it would create circular import dependencies
-            if str(topo.__class__) == "<class 'fim.user.topology.SubstrateTopology'>" and node_id is None:
-                raise RuntimeError(
-                    "When adding switch fabrics to substrate topology nodes you must specify static Node ID")
-            if node_id is None:
-                node_id = str(uuid.uuid4())
-            if parent_node_id is None:
-                raise RuntimeError("For new switch fabrics parent node id must be specified")
-            if str(topo.__class__) == "<class 'fim.user.topology.SubstrateTopology'>" and node_id is None:
-                raise RuntimeError("When adding switch fabrics to substrate topology nodes "
-                                   "you must specify static Node ID")
-            super().__init__(name=name, node_id=node_id, topo=topo)
-            sfsliver = SwitchFabricSliver()
-            sfsliver.node_id = node_id
-            sfsliver.set_name(name)
-            sfsliver.set_type(SFType.SwitchFabric)
-            sfsliver.set_layer(layer)
-            sfsliver.set_properties(**kwargs)
-            self.topo.graph_model.add_switch_fabric_sliver(parent_node_id=parent_node_id, switch_fabric=sfsliver)
-        else:
-            assert node_id is not None
-            super().__init__(name=name, node_id=node_id, topo=topo)
-            if not self.topo.graph_model.check_node_name(node_id=node_id, name=name,
-                                                         label=ABCPropertyGraph.CLASS_SwitchFabric):
-                raise RuntimeError(f"SwitchFabric with this id and name {name} doesn't exist")
-
-    def add_interface(self, *, name: str, node_id: str = None, itype: InterfaceType = InterfaceType.TrunkPort,
-                      **kwargs):
-        """
-        Add an interface to node (mostly needed in substrate topologies)
-        :param name:
-        :param node_id:
-        :param itype: interface type e.g. TrunkPort, AccessPort or VINT
-        :param kwargs: additional parameters
-        :return:
-        """
-        assert name is not None
-        # check uniqueness
-        if name in self.__list_interfaces().keys():
-            raise RuntimeError('Interface names must be unique within a switch fabric')
-        iff = Interface(name=name, node_id=node_id, parent_node_id=self.node_id,
-                        etype=ElementType.NEW, topo=self.topo, itype=itype,
-                        **kwargs)
-        return iff
-
-    def remove_interface(self, *, name: str) -> None:
-        """
-        Remove an interface from the switch fabric, disconnect from links. Remove links
-        if they have nothing else connecting to them.
-        :param name:
-        :return:
-        """
-        assert name is not None
-
-        node_id = self.topo.graph_model.find_connection_point_by_name(parent_node_id=self.node_id,
-                                                                      iname=name)
-        self.topo.graph_model.remove_cp_and_links(node_id=node_id)
-
-    def get_property(self, pname: str) -> Any:
-        """
-        Retrieve a switch fabric property
-        :param pname:
-        :return:
-        """
-        _, node_properties = self.topo.graph_model.get_node_properties(node_id=self.node_id)
-        sf_sliver = self.topo.graph_model.switch_fabric_sliver_from_graph_properties_dict(node_properties)
-        return sf_sliver.get_property(pname)
-
-    def set_property(self, pname: str, pval: Any):
-        """
-        Set a switch fabric property
-        :param pname:
-        :param pval:
-        :return:
-        """
-        sf_sliver = SwitchFabricSliver()
-        sf_sliver.set_property(prop_name=pname, prop_val=pval)
-        # write into the graph
-        prop_dict = self.topo.graph_model.switch_fabric_sliver_to_graph_properties_dict(sf_sliver)
-        self.topo.graph_model.update_node_properties(node_id=self.node_id, props=prop_dict)
-
-    def set_properties(self, **kwargs):
-        """
-        Set multiple properties of the switch fabtic
-        :param kwargs:
-        :return:
-        """
-        sf_sliver = SwitchFabricSliver()
-        sf_sliver.set_properties(**kwargs)
-        # write into the graph
-        prop_dict = self.topo.graph_model.switch_fabric_sliver_to_graph_properties_dict(sf_sliver)
-        self.topo.graph_model.update_node_properties(node_id=self.node_id, props=prop_dict)
-
-    @staticmethod
-    def list_properties() -> Tuple[str]:
-        return SwitchFabricSliver.list_properties()
-
-    def __get_interface_by_id(self, node_id: str) -> Interface:
-        """
-        Get an interface of switch fabric by its node_id, return Interface object
-        :param node_id:
-        :return:
-        """
-        assert node_id is not None
-        _, node_props = self.topo.graph_model.get_node_properties(node_id=node_id)
-        assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
-        return Interface(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id,
-                         topo=self.topo)
-
-    def __get_interface_by_name(self, name: str) -> Interface:
-        """
-        Get an interface of switch fabric by its name
-        :param name:
-        :return:
-        """
-        assert name is not None
-        node_id = self.topo.graph_model.find_connection_point_by_name(parent_node_id=self.node_id,
-                                                                      iname=name)
-        return Interface(name=name, node_id=node_id, topo=self.topo)
-
-    def __list_interfaces(self) -> ViewOnlyDict:
-        """
-        List all interfaces of the switch fabric as a dictionary
-        :return:
-        """
-        node_id_list = self.topo.graph_model.get_all_sf_connection_points(parent_node_id=self.node_id)
-        # Could consider using frozendict or other immutable idioms
-        ret = dict()
-        for nid in node_id_list:
-            c = self.__get_interface_by_id(nid)
-            ret[c.name] = c
-        return ViewOnlyDict(ret)
-
-    def __list_of_interfaces(self) -> Tuple[Interface]:
-        """
-        Return a list of all interfaces of switch fabric
-        :return:
-        """
-        node_id_list = self.topo.graph_model.get_all_sf_connection_points(parent_node_id=self.node_id)
-        ret = list()
-        for nid in node_id_list:
-            c = self.__get_interface_by_id(nid)
-            ret.append(c)
-        return tuple(ret)
-
-    def __getattr__(self, item):
-        """
-        Special handling for attributes like 'components' and 'interfaces' -
-        which query into the model. They return dicts and list
-        containers. Modifying containers does not affect the underlying
-        graph mode, but modifying elements of lists or values of dicts does.
-        :param item:
-        :return:
-        """
-        if item == 'interfaces':
-            return self.__list_interfaces()
-        if item == 'interface_list':
-            return self.__list_of_interfaces()
-        raise RuntimeError(f'Attribute {item} not available')
-
-    def __repr__(self):
-        _, node_properties = self.topo.graph_model.get_node_properties(node_id=self.node_id)
-        sf_sliver = self.topo.graph_model.switch_fabric_sliver_from_graph_properties_dict(node_properties)
-        return sf_sliver.__repr__()
-
-    def __str__(self):
-        return self.__repr__()
-
diff --git a/fim/user/topology.py b/fim/user/topology.py
index 031eafb..223ec36 100644
--- a/fim/user/topology.py
+++ b/fim/user/topology.py
@@ -38,7 +38,7 @@
 from .model_element import ModelElement
 from ..slivers.network_node import NodeType
 from ..slivers.network_link import LinkType
-from ..slivers.switch_fabric import SFLayer
+from ..slivers.network_service import NSLayer, ServiceType
 from ..graph.slices.abc_asm import ABCASMPropertyGraph
 from ..graph.slices.networkx_asm import NetworkxASM
 from ..graph.abc_property_graph import ABCPropertyGraph, GraphFormat
@@ -51,7 +51,7 @@
 from .node import Node
 from .component import Component
 from .composite_node import CompositeNode
-from .switch_fabric import SwitchFabric
+from .network_service import NetworkService
 from .interface import Interface
 from .link import Link
 
@@ -81,6 +81,7 @@ def __init__(self, graph_file: str = None, graph_string: str = None, logger=None
     def get_parent_element(self, e: ModelElement) -> ModelElement or None:
         """
         Instantiate a ModelElement parent of this element or return None
+        for Links, Nodes and sometimes NetworkServices
         :param e:
         :return:
         """
@@ -96,8 +97,8 @@ def get_parent_element(self, e: ModelElement) -> ModelElement or None:
             if node_id is None:
                 raise RuntimeError(f'Component {e} has no parent')
             return Node(name=node_name, node_id=node_id, topo=self)
-        # SFs have nodes or components as parents
-        if isinstance(e, SwitchFabric):
+        # NSs may have nodes or components as parents or be by themselves
+        if isinstance(e, NetworkService):
             node_name, node_id = self.graph_model.get_parent(node_id=e.node_id,
                                                              rel=ABCPropertyGraph.REL_HAS,
                                                              parent=ABCPropertyGraph.CLASS_Component)
@@ -110,17 +111,46 @@ def get_parent_element(self, e: ModelElement) -> ModelElement or None:
             if node_id is not None:
                 return Node(name=node_name, node_id=node_id, topo=self)
             else:
-                raise RuntimeError(f'SwitchFabric {e} has no parent')
-        # interfaces have SFs as parents
+                return None
+        # interfaces have NSs as parents
         if isinstance(e, Interface):
             node_name, node_id = self.graph_model.get_parent(node_id=e.node_id,
                                                              rel=ABCPropertyGraph.REL_CONNECTS,
-                                                             parent=ABCPropertyGraph.CLASS_SwitchFabric)
+                                                             parent=ABCPropertyGraph.CLASS_NetworkService)
             if node_id is None:
                 raise RuntimeError(f'Interface {e} has no parent')
-            return SwitchFabric(name=node_name, node_id=node_id, topo=self)
+            return NetworkService(name=node_name, node_id=node_id, topo=self)
         raise RuntimeError(f'Unable to determine parent of element {e}')
 
+    def get_owner_node(self, e: ModelElement) -> ModelElement or None:
+        """
+        Get an owner Node element for Component, NetworkService or Interface. For Link and Node
+        return None
+        :param e:
+        :return:
+        """
+        assert e is not None
+        if isinstance(e, Node) or isinstance(e, Link):
+            # nodes or links don't have parents
+            return None
+        if isinstance(e, Component):
+            return self.get_parent_element(e)
+        if isinstance(e, NetworkService):
+            # either it is directly owned by a switch or via a component or by itself
+            pe = self.get_parent_element(e)
+            if pe is None:
+                return None
+            if isinstance(pe, Node):
+                return pe
+            else:
+                return self.get_parent_element(pe)
+        if isinstance(e, Interface):
+            # figure out the owner of the parent network service
+            ns = self.get_parent_element(e)
+            if ns is not None:
+                return self.get_owner_node(ns)
+        return RuntimeError(f'Unable to determine owner node of element {e}')
+
     def add_node(self, *, name: str, node_id: str = None, site: str, ntype: NodeType = NodeType.VM,
                  **kwargs) -> Node:
         """
@@ -152,26 +182,26 @@ def remove_node(self, name: str):
         :return:
         """
         assert name is not None
-        self.graph_model.remove_network_node_with_components_sfs_cps_and_links(
+        self.graph_model.remove_network_node_with_components_nss_cps_and_links(
             node_id=self.__get_node_by_name(name=name).node_id)
 
-    def add_link(self, *, name: str, node_id: str = None, ltype: LinkType = None,
-                 interfaces: List[Interface], layer: SFLayer = None, technology: str = None,
+    def add_link(self, *, name: str, node_id: str = None, ltype: LinkType,
+                 interfaces: List[Interface], technology: str = None,
                  **kwargs) -> Link:
         """
-        Add link between listed interfaces with specified parameters
+        Add link between listed interfaces with specified parameters (for experiment
+        topologies you want add_network_service instead)
         :param name:
         :param node_id:
         :param ltype:
         :param interfaces:
-        :param layer:
         :param technology:
         :param kwargs:
         :return:
         """
         # add link to graph
-        link = Link(name=name, node_id=node_id, ltype=ltype, interfaces=interfaces, **kwargs,
-                    etype=ElementType.NEW, topo=self, layer=layer, technology=technology)
+        link = Link(name=name, node_id=node_id, ltype=ltype, interfaces=interfaces,
+                    etype=ElementType.NEW, topo=self, technology=technology, **kwargs)
         return link
 
     def remove_link(self, name: str):
@@ -183,6 +213,31 @@ def remove_link(self, name: str):
         assert name is not None
         self.graph_model.remove_network_link(node_id=self.__get_link_by_name(name=name).node_id)
 
+    def add_network_service(self, *, name: str, node_id: str = None, nstype: ServiceType,
+                            interfaces: List[Interface], technology: str = None, **kwargs) -> NetworkService:
+        """
+        Add a network service to a topology. Interfaces can be specified upfront or added/removed later
+        :param name:
+        :param node_id:
+        :param nstype:
+        :param interfaces:
+        :param technology:
+        :param kwargs:
+        :return:
+        """
+        ns = NetworkService(name=name, node_id=node_id, topo=self, nstype=nstype,
+                            etype=ElementType.NEW, interfaces=interfaces, technology=technology)
+        return ns
+
+    def remove_network_service(self, name: str):
+        """
+        Remove a network service and associated service interfaces
+        :param name:
+        :return:
+        """
+        assert name is not None
+        self.graph_model.remove_ns_with_cps_and_links(node_id=self.__get_ns_by_name(name=name).node_id)
+
     def __get_node_by_name(self, name: str) -> Node:
         """
         Find node by its name, return Node object
@@ -205,6 +260,17 @@ def __get_link_by_name(self, name: str) -> Link:
                                                      label=ABCPropertyGraph.CLASS_Link)
         return Link(name=name, node_id=node_id, topo=self)
 
+    def __get_ns_by_name(self, name: str) -> NetworkService:
+        """
+        Find a network service by its name, return NetworkService object
+        :param name:
+        :return:
+        """
+        assert name is not None
+        node_id = self.graph_model.find_node_by_name(node_name=name,
+                                                     label=ABCPropertyGraph.CLASS_NetworkService)
+        return NetworkService(name=name, node_id=node_id, topo=self)
+
     def __get_node_by_id(self, node_id: str) -> Node:
         """
         Get node by its node_id, return Node object
@@ -227,6 +293,17 @@ def __get_link_by_id(self, node_id: str) -> Link:
         assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
         return Link(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id, topo=self)
 
+    def __get_ns_by_id(self, node_id: str) -> NetworkService:
+        """
+        Get network service by its node id, return NetworkService object
+        :param node_id:
+        :return:
+        """
+        assert node_id is not None
+        _, node_props = self.graph_model.get_node_properties(node_id=node_id)
+        assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
+        return NetworkService(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id, topo=self)
+
     def __list_nodes(self) -> ViewOnlyDict:
         """
         List all NetworkNodes in the topology as a dictionary
@@ -241,6 +318,20 @@ def __list_nodes(self) -> ViewOnlyDict:
             ret[n.name] = n
         return ViewOnlyDict(ret)
 
+    def __list_network_services(self) -> ViewOnlyDict:
+        """
+        List all NetworkServices in the topology as a dictionary organized by name.
+        Modifying the dictionary will not affect the underlying model, but modifying
+        NetworkServices in the dictionary will.
+        :return:
+        """
+        node_id_list = self.graph_model.get_all_network_service_nodes()
+        ret = dict()
+        for nid in node_id_list:
+            n = self.__get_ns_by_id(nid)
+            ret[n.name] = n
+        return ViewOnlyDict(ret)
+
     def __list_links(self) -> ViewOnlyDict:
         """
         List all Links in the topology as a dictionary organized by Link name.
@@ -276,6 +367,8 @@ def __getattr__(self, item):
             return self.__list_nodes()
         if item == 'links':
             return self.__list_links()
+        if item == 'network_services':
+            return self.__list_network_services()
         if item == 'interface_list':
             return self.__list_of_interfaces()
         raise RuntimeError(f'Attribute {item} not available')
@@ -353,23 +446,33 @@ def draw(self, *, file_name: str = None, interactive: bool = False,
         elif topo_detail == TopologyDetail.Derived:
             # collect all network nodes and links, draw edges between them
             network_nodes = self.graph_model.get_all_network_nodes()
+            service_nodes = self.graph_model.get_all_network_service_nodes()
             links = self.graph_model.get_all_network_links()
 
             derived_graph = nx.Graph()
             for n in network_nodes:
                 _, props = self.graph_model.get_node_properties(node_id=n)
                 derived_graph.add_node(props[ABCPropertyGraph.PROP_NAME])
-            for l in links:
-                _, props = self.graph_model.get_node_properties(node_id=l)
+            for ns in service_nodes:
+                _, props = self.graph_model.get_node_properties(node_id=ns)
                 derived_graph.add_node(props[ABCPropertyGraph.PROP_NAME])
 
-            for n in self.nodes.values():
-                node_ints = n.interfaces.values()
-                for nint in node_ints:
-                    for l in self.links.values():
-                        # this works because of custom ModelElement.__eq__()
-                        if nint in l.interface_list:
-                            derived_graph.add_edge(n.name, l.name)
+            # link interfaces of NSs and NSs to nodes
+            for ns in self.network_services.values():
+                for nint in ns.interfaces.values():
+                    peer_int_id = self.graph_model.find_peer_connection_point(node_id=nint.node_id)
+                    if peer_int_id is None:
+                        continue
+                    _, peer_props = self.graph_model.get_node_properties(node_id=peer_int_id)
+                    peer_int = Interface(node_id=peer_int_id, name=peer_props[ABCPropertyGraph.PROP_NAME],
+                                         topo=self)
+                    peer_int_parent = self.get_parent_element(peer_int)
+                    if peer_int_parent is None:
+                        continue
+                    derived_graph.add_edge(ns.name, peer_int_parent.name)
+                for n in self.nodes.values():
+                    if self.get_owner_node(ns) == n:
+                        derived_graph.add_edge(ns.name, n.name)
 
             pos = layout(derived_graph)
             nx.draw_networkx(derived_graph, pos=pos)
@@ -465,7 +568,7 @@ def as_arm(self):
     def __copy_to_delegations(e: ModelElement, atype: DelegationType,
                               delegation_id: str) -> Delegations or None:
         """
-        From a model element (node, component, switch_fabric, interface), copy
+        From a model element (node, component, network service, interface), copy
         capacities or labels to a Delegations and return Delegations or none if
         no capacities or labels specified. Return None if element is a stitch node.
         :param e:
@@ -525,8 +628,8 @@ def single_delegation(self, *, delegation_id: str, label_pools: Pools, capacity_
                                                                  delegation_id=delegation_id)
                         if delegations is not None:
                             delegations_dicts[t][i.node_id] = delegations
-                # switchfabrics (for eg switches)
-                for sf in n.switch_fabrics.values():
+                # network services (for eg switches)
+                for sf in n.network_services.values():
                     delegations = self.__copy_to_delegations(e=sf, atype=t,
                                                              delegation_id=delegation_id)
                     if delegations is not None:
@@ -563,7 +666,7 @@ def add_node(self, *, name: str, node_id: str = None, site: str, ntype: NodeType
         raise RuntimeError('Cannot add node to advertisement')
 
     def add_link(self, *, name: str, node_id: str = None, ltype: LinkType = None,
-                 interfaces: List[Interface], layer: SFLayer = None, technology: str = None,
+                 interfaces: List[Interface], layer: NSLayer = None, technology: str = None,
                  **kwargs) -> Link:
         raise RuntimeError('Cannot add link to advertisement')
 
diff --git a/requirements.txt b/requirements.txt
index d7c2227..3e0216f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 astroid==2.4.2
-attrs==20.2.0
+attrs==20.3.0
 bleach==3.3.0
 certifi==2020.12.5
 chardet==4.0.0
@@ -8,7 +8,7 @@ cycler==0.10.0
 decorator==4.4.2
 idna==2.10
 importlib-metadata==3.7.3
-iniconfig==1.0.1
+iniconfig==1.1.1
 isort==4.3.21
 keyring==21.5.0
 kiwisolver==1.3.1
@@ -26,23 +26,23 @@ pkginfo==1.6.1
 pluggy==0.13.1
 py==1.10.0
 pycodestyle==2.6.0
-Pygments==2.7.4
+Pygments==2.9.0
 pyparsing==2.4.7
 pytest==6.2.1
 python-dateutil==2.8.1
 pytz==2020.1
 PyYAML==5.4
-readme-renderer==26.0
+readme-renderer==29.0
 recordclass==0.14.3
 requests==2.25.1
 requests-toolbelt==0.9.1
 rfc3986==1.4.0
 six==1.15.0
-toml==0.10.1
+toml==0.10.2
 tqdm==4.54.1
 typed-ast==1.4.1
 typing-extensions==3.7.4.3
-urllib3==1.26.4
+urllib3==1.26.5
 webencodings==0.5.1
 wrapt==1.12.1
 zipp==3.4.0
diff --git a/setup.py b/setup.py
index 0a933f9..6bc110c 100644
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@
 
 setuptools.setup(
   name="fabric_fim",
-  version="0.47",
+  version="0.51",
   author="Ilya Baldin, Komal Thareja",
   description="FABRIC Information Model Library",
   url="https://github.com/fabric-testbed/InformationModel",


Test patch:
diff --git a/test/catalog_test.py b/test/catalog_test.py
index 91af5bf..e4afa00 100644
--- a/test/catalog_test.py
+++ b/test/catalog_test.py
@@ -11,9 +11,9 @@ def testCatalog(self):
         cata = ComponentCatalog()
 
         c = cata.generate_component(name='myNIC', model='ConnectX-6', ctype=ComponentType.SmartNIC)
-        self.assertIsNotNone(c.switch_fabric_info)
-        self.assertEqual(len(c.switch_fabric_info.get_switch_fabric('myNIC-l2sf').interface_info.interfaces.keys()), 2)
+        self.assertIsNotNone(c.network_service_info)
+        self.assertEqual(len(c.network_service_info.get_network_service('myNIC-l2ovs').interface_info.interfaces.keys()), 2)
         c1 = cata.generate_component(name='myGPU', model='RTX6000', ctype=ComponentType.GPU)
-        self.assertIsNone(c1.switch_fabric_info)
+        self.assertIsNone(c1.network_service_info)
         with self.assertRaises(CatalogException):
             cata.generate_component(name='some', model='blah', ctype=ComponentType.SmartNIC)
diff --git a/test/slice_topology_test.py b/test/slice_topology_test.py
index 763b9bb..333caa3 100644
--- a/test/slice_topology_test.py
+++ b/test/slice_topology_test.py
@@ -64,7 +64,7 @@ def testNodesAndComponents(self):
         assert(len(self.topo.interface_list) == 4)
         assert(len(nic1.interface_list) == 1)
         assert(len(nic2.interface_list) == 2)
-        assert(len(list(nic1.switch_fabrics.values())[0].interface_list) == 1)
+        assert(len(list(nic1.network_services.values())[0].interface_list) == 1)
 
         self.assertTrue(len(gpu1.interfaces) == 0)
         self.assertTrue(len(nic1.interfaces) == 1)
@@ -86,10 +86,12 @@ def testNodesAndComponents(self):
         # interfaces and links
         self.assertEqual(len(self.topo.interface_list), 4)
 
-        self.topo.add_link(name='l1', ltype=f.LinkType.Wave, interfaces=self.topo.interface_list)
+        #print(f"testNodeAndComponents Interfaces {self.topo.interface_list}")
+        self.topo.add_network_service(name='s1', nstype=f.ServiceType.L2Bridge, interfaces=self.topo.interface_list)
 
         with self.assertRaises(AssertionError) as e:
-            self.topo.add_link(name='l1', ltype=f.LinkType.Wave, interfaces=self.topo.interface_list)
+            self.topo.add_network_service(name='s1', nstype=f.ServiceType.L2Bridge,
+                                          interfaces=self.topo.interface_list[0:2])
 
         #print(self.topo)
 
@@ -99,25 +101,93 @@ def testNodesAndComponents(self):
         n1.unset_property('node_map')
         assert n1.get_property('node_map') is None
 
-        self.assertEqual(len(self.topo.links), 1)
+        self.assertEqual(len(self.topo.links), 4)
+        # 4 services - 3 in NICs, one global
+        self.assertEqual(len(self.topo.network_services), 4)
         # removal checks
         self.topo.remove_node(name='Node2')
 
         self.assertTrue(len(self.topo.links), 1)
         self.assertTrue(len(self.topo.nodes), 2)
 
-        # should remove the link since only one interface left
         n1.remove_component(name='nic1')
+        self.assertEqual(len(self.topo.links), 1)
+
+        n3.remove_component(name='nic3')
         self.assertEqual(len(self.topo.links), 0)
+
         # GPU left
         self.assertTrue(len(n1.components), 1)
-        self.assertTrue(len(self.topo.interface_list), 1)
+
         # remove remaining nodes
         self.topo.remove_node(name='node3')
         self.topo.remove_node(name='Node1')
         self.assertEqual(len(self.topo.nodes), 0)
         self.assertEqual(len(self.topo.interface_list), 0)
         self.assertEqual(len(self.topo.links), 0)
+        self.assertEqual(len(self.topo.network_services), 1)
+        self.topo.remove_network_service(name='s1')
+        self.assertEqual(len(self.topo.network_services), 0)
+
+    def testNetworkServices(self):
+        n1 = self.topo.add_node(name='Node1', site='RENC')
+        n2 = self.topo.add_node(name='Node2', site='UKY', ntype=f.NodeType.Server)
+        n3 = self.topo.add_node(name='Node3', site='RENC', management_ip='123.45.67.98',
+                                image_ref='http://some.image', image_type='image type')
+
+        # component checks
+        n1.add_component(ctype=f.ComponentType.GPU, model='RTX6000', name='gpu1')
+        n1.add_component(ctype=f.ComponentType.SharedNIC, model='ConnectX-6', name='nic1')
+        n2.add_component(ctype=f.ComponentType.SmartNIC, model='ConnectX-6', name='nic2')
+        n3.add_component(ctype=f.ComponentType.SharedNIC, model='ConnectX-6', name='nic3')
+
+        gpu1 = n1.components['gpu1']
+        nic1 = n1.components['nic1']
+        nic2 = n2.components['nic2']
+
+        p1 = nic2.interfaces['nic2-p1']
+        p2 = nic2.interfaces['nic2-p2']
+
+        cap = f.Capacities()
+        cap.set_fields(bw=50, unit=1)
+        nic1.set_properties(capacities=cap)
+        lab = f.Labels()
+        lab.set_fields(ipv4="192.168.1.12")
+        nic1.set_properties(labels=lab)
+
+        #s1 = self.topo.add_network_service(name='s1', nstype=f.ServiceType.L2Bridge, interfaces=self.topo.interface_list)
+
+        s1 = self.topo.add_network_service(name='s1', nstype=f.ServiceType.L2STS, interfaces=self.topo.interface_list)
+
+        print(f'S1 has these interfaces: {s1.interface_list}')
+        self.assertEqual(len(s1.interface_list), 4)
+
+        s1p = self.topo.network_services['s1']
+
+        print(f'S1p has these interfaces: {s1p.interface_list}')
+
+        s1.disconnect_interface(interface=p1)
+
+        print(f'S1 has these interfaces: {s1.interface_list}')
+        self.assertEqual(len(s1.interface_list), 3)
+
+        self.topo.remove_network_service('s1')
+
+        s2 = self.topo.add_network_service(name='s2', nstype=f.ServiceType.L2PTP,
+                                           interfaces=self.topo.interface_list[0:2])
+        self.assertEqual(len(s2.interface_list), 2)
+        print(f'S2 has these interfaces: {s2.interface_list}')
+
+        print(f'There are {self.topo.links} left in topology')
+        self.assertEqual(len(self.topo.links), 2)
+        print(f'Network services {self.topo.network_services}')
+        self.assertEqual(len(self.topo.network_services), 4)
+        self.topo.remove_network_service('s2')
+        self.assertEqual(len(self.topo.network_services), 3)
+        n1.remove_component('nic1')
+        self.assertEqual(len(self.topo.network_services), 2)
+
+        #self.topo.add_link(name='l3', ltype=f.LinkType.L2Bridge, interfaces=self.topo.interface_list)
 
     def testDeepSliver(self):
         self.topo.add_node(name='n1', site='RENC')
@@ -125,11 +195,11 @@ def testDeepSliver(self):
         deep_sliver = self.topo.graph_model.build_deep_node_sliver(node_id=self.topo.nodes['n1'].node_id)
         self.assertNotEqual(deep_sliver, None)
         self.assertNotEqual(deep_sliver.attached_components_info, None)
-        self.assertNotEqual(deep_sliver.attached_components_info.devices['nic1'].switch_fabric_info, None)
+        self.assertNotEqual(deep_sliver.attached_components_info.devices['nic1'].network_service_info, None)
         self.assertNotEqual(deep_sliver.attached_components_info.
-                            devices['nic1'].switch_fabric_info.switch_fabrics['nic1-l2sf'].interface_info, None)
+                            devices['nic1'].network_service_info.network_services['nic1-l2ovs'].interface_info, None)
         self.assertEqual(len(deep_sliver.attached_components_info.
-                             devices['nic1'].switch_fabric_info.switch_fabrics['nic1-l2sf'].
+                             devices['nic1'].network_service_info.network_services['nic1-l2ovs'].
                              interface_info.interfaces), 1)
 
     def testSerDes(self):
diff --git a/test/sliver_test.py b/test/sliver_test.py
index d7c20fe..f5ebe79 100644
--- a/test/sliver_test.py
+++ b/test/sliver_test.py
@@ -3,7 +3,8 @@
 from fim.slivers.attached_components import ComponentSliver, ComponentType, AttachedComponentsInfo
 from fim.slivers.network_node import NodeSliver, NodeType
 from fim.slivers.network_link import NetworkLinkSliver
-from fim.slivers.switch_fabric import SwitchFabricSliver, SwitchFabricInfo
+from fim.slivers.network_service import NetworkServiceSliver, NetworkServiceInfo
+from fim.slivers.path_info import ERO, PathInfo, Path
 
 
 class TestSlivers(unittest.TestCase):
@@ -45,3 +46,16 @@ def testNodeSliver(self):
                           management_ip='192.168.1.1')
         with self.assertRaises(ValueError) as ve:
             ns.set_property('management_ip', '192.168.1.x')
+
+    def testNetworkServiceSliver(self):
+        ns = NetworkServiceSliver()
+        p = Path()
+        p.set_symmetric(['a', 'b', 'c'])
+        pi = PathInfo()
+        pi.set(payload=p)
+        ns.set_properties(path_info=pi)
+        e = ERO()
+        e.set(payload=p)
+        ns.set_properties(ero=e)
+        pi1 = ns.get_property('path_info')
+        assert(pi.get() == pi1.get())
diff --git a/test/substrate_topology_test.py b/test/substrate_topology_test.py
index f2fd34b..da5672b 100644
--- a/test/substrate_topology_test.py
+++ b/test/substrate_topology_test.py
@@ -165,7 +165,7 @@ def testRENCSiteAd(self):
         # Usually slot 7, second port not connected
         gpuw_shnic = gpuw.add_component(name=gpuw.name + '-shnic', model='ConnectX-6',
                                         node_id=gpuw.node_id + '-shnic',
-                                        switch_fabric_node_id=gpuw.node_id + '-shnic-sf',
+                                        network_service_node_id=gpuw.node_id + '-shnic-sf',
                                         interface_node_ids=[mac_to_node_id(pf_mac)],
                                         interface_labels=[f.Labels().set_fields(bdf=vf_ids,
                                                                                 mac=vf_macs,
@@ -183,7 +183,7 @@ def testRENCSiteAd(self):
         vf_vlans = [1001, 1002, 1003, 1004]
         fnw_shnic = fnw.add_component(name=fnw.name + '-shnic', model='ConnectX-6',
                                       node_id=fnw.node_id + '-shnic',
-                                      switch_fabric_node_id=fnw.node_id + '-shnic-sf',
+                                      network_service_node_id=fnw.node_id + '-shnic-sf',
                                       interface_node_ids=[mac_to_node_id(pf_mac)],
                                       interface_labels=[f.Labels().set_fields(bdf=vf_ids,
                                                                               mac=vf_macs,
@@ -201,7 +201,7 @@ def testRENCSiteAd(self):
         vf_vlans = [1001, 1002, 1003, 1004]
         snw_shnic = snw.add_component(name=snw.name + '-shnic', model='ConnectX-6',
                                       node_id=snw.node_id + '-shnic',
-                                      switch_fabric_node_id=snw.node_id + '-shnic-sf',
+                                      network_service_node_id=snw.node_id + '-shnic-sf',
                                       interface_node_ids=[mac_to_node_id(pf_mac)],
                                       interface_labels=[f.Labels().set_fields(bdf=vf_ids,
                                                                               mac=vf_macs,
@@ -217,7 +217,7 @@ def testRENCSiteAd(self):
 
         fnw_nic1 = fnw.add_component(name=fnw.name + '-nic1', model='ConnectX-6',
                                      node_id=fnw.node_id + '-nic1',
-                                     switch_fabric_node_id=fnw.node_id + '-nic1-sf',
+                                     network_service_node_id=fnw.node_id + '-nic1-sf',
                                      interface_node_ids=[mac_to_node_id('04:3F:72:B7:15:74'),
                                                          mac_to_node_id('04:3F:72:B7:15:75')],
                                      interface_labels=[f.Labels().set_fields(mac='04:3F:72:B7:15:74',
@@ -232,7 +232,7 @@ def testRENCSiteAd(self):
 
         fnw_nic2 = fnw.add_component(name=fnw.name + '-nic2', model='ConnectX-6',
                                      node_id=fnw.node_id + '-nic2',
-                                     switch_fabric_node_id=fnw.node_id + '-nic2-sf',
+                                     network_service_node_id=fnw.node_id + '-nic2-sf',
                                      interface_node_ids=[mac_to_node_id('04:3F:72:B7:19:5C'),
                                                          mac_to_node_id('04:3F:72:B7:19:5D')],
                                      interface_labels=[f.Labels().set_fields(mac='04:3F:72:B7:19:5C',
@@ -247,7 +247,7 @@ def testRENCSiteAd(self):
 
         snw_nic1 = snw.add_component(name=snw.name + '-nic1', model='ConnectX-5',
                                      node_id=snw.node_id + '-nic1',
-                                     switch_fabric_node_id=snw.node_id + '-nic1-sf',
+                                     network_service_node_id=snw.node_id + '-nic1-sf',
                                      interface_node_ids=[mac_to_node_id('0C:42:A1:BE:8F:D4'),
                                                          mac_to_node_id('0C:42:A1:BE:8F:D5')],
                                      interface_labels=[f.Labels().set_fields(mac='0C:42:A1:BE:8F:D4',
@@ -262,7 +262,7 @@ def testRENCSiteAd(self):
 
         snw_nic2 = snw.add_component(name=snw.name + '-nic2', model='ConnectX-5',
                                      node_id=snw.node_id + '-nic2',
-                                     switch_fabric_node_id=snw.node_id + '-nic2-sf',
+                                     network_service_node_id=snw.node_id + '-nic2-sf',
                                      interface_node_ids=[mac_to_node_id('0C:42:A1:BE:8F:E8'),
                                                          mac_to_node_id('0C:42:A1:BE:8F:E9')],
                                      interface_labels=[f.Labels().set_fields(mac='0C:42:A1:BE:8F:E8',
@@ -286,8 +286,9 @@ def testRENCSiteAd(self):
                                     node_id='FOC2450R1BL',
                                     capacities=f.Capacities().set_fields(unit=1),
                                     ntype=f.NodeType.Switch)
-        dp_sf = switch.add_switch_fabric(name=switch.name+'-sf', layer=f.Layer.L2,
-                                         node_id=switch.node_id + '-sf')
+        dp_ns = switch.add_network_service(name=switch.name+'-ns',
+                                           node_id=switch.node_id + '-ns',
+                                           nstype=f.ServiceType.MPLS)
         # add ports
         port_caps = f.Capacities()
         port_caps1 = f.Capacities()
@@ -295,42 +296,42 @@ def testRENCSiteAd(self):
         port_caps1.set_fields(bw=25)
 
         # FIXME: don't have port MAC addresses yet - placeholders
-        sp1 = dp_sf.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
+        sp1 = dp_ns.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:00'),
                                   capacities=port_caps)
 
-        sp2 = dp_sf.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
+        sp2 = dp_ns.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:01'),
                                   capacities=port_caps)
 
-        sp3 = dp_sf.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
+        sp3 = dp_ns.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:02'),
                                   capacities=port_caps)
-        sp4 = dp_sf.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
+        sp4 = dp_ns.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:03'),
                                   capacities=port_caps)
-        sp5 = dp_sf.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
+        sp5 = dp_ns.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:04'),
                                   capacities=port_caps)
-        sp6 = dp_sf.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
+        sp6 = dp_ns.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:05'),
                                   capacities=port_caps)
 
-        sp7 = dp_sf.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
+        sp7 = dp_ns.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:06'),
                                   capacities=port_caps)
 
         # FIXME: what to do about breakout ports in slownets?
-        sp8 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
+        sp8 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:07'),
                                   capacities=port_caps1)
-        sp9 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
+        sp9 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:08'),
                                   capacities=port_caps1)
-        sp10 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
+        sp10 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('00:00:00:00:00:09'),
                                    capacities=port_caps1)
-        sp11 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
+        sp11 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('00:00:00:00:00:0A'),
                                    capacities=port_caps1)
 
@@ -339,41 +340,41 @@ def testRENCSiteAd(self):
         #
         # FIXME: Link node ids need to come from somewhere, could be an extension of interface ID
         # FIXME: on the switch, or something else
-        l1 = self.topo.add_link(name='l1', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l1 = self.topo.add_link(name='l1', ltype=f.LinkType.Patch,
                                 interfaces=[gpuw_shnic.interfaces['renc-w1-shnic-p1'], sp1],
                                 node_id=sp1.node_id + '-DAC')
 
-        l2 = self.topo.add_link(name='l2', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l2 = self.topo.add_link(name='l2', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_shnic.interfaces['renc-w2-shnic-p1'], sp2],
                                 node_id=sp2.node_id + '-DAC')
 
-        l3 = self.topo.add_link(name='l3', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l3 = self.topo.add_link(name='l3', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic1.interfaces['renc-w2-nic1-p1'], sp3],
                                 node_id=sp3.node_id + '-DAC')
-        l4 = self.topo.add_link(name='l4', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l4 = self.topo.add_link(name='l4', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic1.interfaces['renc-w2-nic1-p2'], sp4],
                                 node_id=sp4.node_id + '-DAC')
-        l5 = self.topo.add_link(name='l5', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l5 = self.topo.add_link(name='l5', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic2.interfaces['renc-w2-nic2-p1'], sp5],
                                 node_id=sp5.node_id + '-DAC')
-        l6 = self.topo.add_link(name='l6', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l6 = self.topo.add_link(name='l6', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic2.interfaces['renc-w2-nic2-p2'], sp6],
                                 node_id=sp6.node_id + '-DAC')
 
-        l8 = self.topo.add_link(name='l7', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l8 = self.topo.add_link(name='l7', ltype=f.LinkType.Patch,
                                 interfaces=[snw_shnic.interfaces['renc-w3-shnic-p1'], sp7],
                                 node_id=sp7.node_id + '-DAC')
 
-        l9 = self.topo.add_link(name='l8', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l9 = self.topo.add_link(name='l8', ltype=f.LinkType.Patch,
                                 interfaces=[snw_nic1.interfaces['renc-w3-nic1-p1'], sp8],
                                 node_id=sp8.node_id + '-DAC')
-        l10 = self.topo.add_link(name='l9', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l10 = self.topo.add_link(name='l9', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic1.interfaces['renc-w3-nic1-p2'], sp9],
                                  node_id=sp9.node_id + '-DAC')
-        l11 = self.topo.add_link(name='l10', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l11 = self.topo.add_link(name='l10', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic2.interfaces['renc-w3-nic2-p1'], sp10],
                                  node_id=sp10.node_id + '-DAC')
-        l12 = self.topo.add_link(name='l11', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l12 = self.topo.add_link(name='l11', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic2.interfaces['renc-w3-nic2-p2'], sp11],
                                  node_id=sp11.node_id + '-DAC')
         #
@@ -589,7 +590,7 @@ def testUKYSiteAd(self):
         # Usually slot 7, second port not connected (Uky in slot 6)
         gpuw_shnic = gpuw.add_component(name=gpuw.name + '-shnic', model='ConnectX-6',
                                         node_id=gpuw.node_id + '-shnic',
-                                        switch_fabric_node_id=gpuw.node_id + '-shnic-sf',
+                                        network_service_node_id=gpuw.node_id + '-shnic-sf',
                                         # there is one interface and we need one name
                                         interface_node_ids=[mac_to_node_id(pf_mac)],
                                         interface_labels=[f.Labels().set_fields(bdf=vf_ids,
@@ -612,7 +613,7 @@ def testUKYSiteAd(self):
         vf_vlans = [1001, 1002, 1003, 1004]
         fnw_shnic = fnw.add_component(name=fnw.name + '-shnic', model='ConnectX-6',
                                       node_id=fnw.node_id + '-shnic',
-                                      switch_fabric_node_id=fnw.node_id + '-shnic-sf',
+                                      network_service_node_id=fnw.node_id + '-shnic-sf',
                                       # there is one interface and we need one name
                                       interface_node_ids=[mac_to_node_id(pf_mac)],
                                       interface_labels=[f.Labels().set_fields(bdf=vf_ids,
@@ -631,7 +632,7 @@ def testUKYSiteAd(self):
         vf_vlans = [1001, 1002, 1003, 1004]
         snw_shnic = snw.add_component(name=snw.name + '-shnic', model='ConnectX-6',
                                       node_id=snw.node_id + '-shnic',
-                                      switch_fabric_node_id=snw.node_id + '-shnic-sf',
+                                      network_service_node_id=snw.node_id + '-shnic-sf',
                                       # there is one interface and we need one name
                                       interface_node_ids=[mac_to_node_id(pf_mac)],
                                       interface_labels=[f.Labels().set_fields(bdf=vf_ids,
@@ -648,7 +649,7 @@ def testUKYSiteAd(self):
 
         fnw_nic1 = fnw.add_component(name=fnw.name + '-nic1', model='ConnectX-6',
                                      node_id=fnw.node_id + '-nic1',
-                                     switch_fabric_node_id=fnw.node_id + '-nic1-sf',
+                                     network_service_node_id=fnw.node_id + '-nic1-sf',
                                      interface_node_ids=[mac_to_node_id('0C:42:A1:EA:C7:50'),
                                                          mac_to_node_id('0C:42:A1:EA:C7:51')],
                                      interface_labels=[f.Labels().set_fields(mac='0C:42:A1:EA:C7:50',
@@ -670,7 +671,7 @@ def testUKYSiteAd(self):
 
         fnw_nic2 = fnw.add_component(name=fnw.name + '-nic2', model='ConnectX-6',
                                      node_id=fnw.node_id + '-nic2',
-                                     switch_fabric_node_id=fnw.node_id + '-nic2-sf',
+                                     network_service_node_id=fnw.node_id + '-nic2-sf',
                                      interface_node_ids=[mac_to_node_id('0C:42:A1:78:F8:04'),
                                                          mac_to_node_id('0C:42:A1:78:F8:05')],
                                      interface_labels=[f.Labels().set_fields(mac='0C:42:A1:78:F8:04',
@@ -685,7 +686,7 @@ def testUKYSiteAd(self):
 
         snw_nic1 = snw.add_component(name=snw.name + '-nic1', model='ConnectX-5',
                                      node_id=snw.node_id + '-nic1',
-                                     switch_fabric_node_id=snw.node_id + '-nic1-sf',
+                                     network_service_node_id=snw.node_id + '-nic1-sf',
                                      interface_node_ids=[mac_to_node_id('0C:42:A1:BE:8F:F8'),
                                                          mac_to_node_id('0C:42:A1:BE:8F:F9')],
                                      interface_labels=[f.Labels().set_fields(mac='0C:42:A1:BE:8F:F8',
@@ -700,7 +701,7 @@ def testUKYSiteAd(self):
 
         snw_nic2 = snw.add_component(name=snw.name + '-nic2', model='ConnectX-5',
                                      node_id=snw.node_id + '-nic2',
-                                     switch_fabric_node_id=snw.node_id + '-nic2-sf',
+                                     network_service_node_id=snw.node_id + '-nic2-sf',
                                      interface_node_ids=[mac_to_node_id('0C:42:A1:BE:8F:DC'),
                                                          mac_to_node_id('0C:42:A1:BE:8F:DD')],
                                      interface_labels=[f.Labels().set_fields(mac='0C:42:A1:BE:8F:DC',
@@ -724,8 +725,9 @@ def testUKYSiteAd(self):
                                     node_id='FOC2451R0XZ',
                                     capacities=f.Capacities().set_fields(unit=1),
                                     ntype=f.NodeType.Switch)
-        dp_sf = switch.add_switch_fabric(name=switch.name+'-sf', layer=f.Layer.L2,
-                                         node_id=switch.node_id + '-sf')
+        dp_ns = switch.add_network_service(name=switch.name+'-ns',
+                                           node_id=switch.node_id + '-ns',
+                                           nstype=f.ServiceType.MPLS)
         # add ports
         port_caps = f.Capacities()
         port_caps1 = f.Capacities()
@@ -733,42 +735,42 @@ def testUKYSiteAd(self):
         port_caps1.set_fields(bw=25)
 
         # FIXME: don't have port MAC addresses yet - placeholders
-        sp1 = dp_sf.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
+        sp1 = dp_ns.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:00'),
                                   capacities=port_caps)
 
-        sp2 = dp_sf.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
+        sp2 = dp_ns.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:01'),
                                   capacities=port_caps)
 
-        sp3 = dp_sf.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
+        sp3 = dp_ns.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:02'),
                                   capacities=port_caps)
-        sp4 = dp_sf.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
+        sp4 = dp_ns.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:03'),
                                   capacities=port_caps)
-        sp5 = dp_sf.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
+        sp5 = dp_ns.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:04'),
                                   capacities=port_caps)
-        sp6 = dp_sf.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
+        sp6 = dp_ns.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:05'),
                                   capacities=port_caps)
 
-        sp7 = dp_sf.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
+        sp7 = dp_ns.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:06'),
                                   capacities=port_caps)
 
         # FIXME: what to do about breakout ports in slownets?
-        sp8 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
+        sp8 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:07'),
                                   capacities=port_caps1)
-        sp9 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
+        sp9 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:08'),
                                   capacities=port_caps1)
-        sp10 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
+        sp10 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('10:00:00:00:00:09'),
                                    capacities=port_caps1)
-        sp11 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
+        sp11 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('10:00:00:00:00:0A'),
                                    capacities=port_caps1)
 
@@ -777,41 +779,41 @@ def testUKYSiteAd(self):
         #
         # FIXME: Link node ids need to come from somewhere, could be an extension of interface ID
         # FIXME: on the switch, or something else
-        l1 = self.topo.add_link(name='l1', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l1 = self.topo.add_link(name='l1', ltype=f.LinkType.Patch,
                                 interfaces=[gpuw_shnic.interfaces['uky-w1-shnic-p1'], sp1],
                                 node_id=sp1.node_id + '-DAC')
 
-        l2 = self.topo.add_link(name='l2', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l2 = self.topo.add_link(name='l2', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_shnic.interfaces['uky-w2-shnic-p1'], sp2],
                                 node_id=sp2.node_id + '-DAC')
 
-        l3 = self.topo.add_link(name='l3', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l3 = self.topo.add_link(name='l3', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic1.interfaces['uky-w2-nic1-p1'], sp3],
                                 node_id=sp3.node_id + '-DAC')
-        l4 = self.topo.add_link(name='l4', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l4 = self.topo.add_link(name='l4', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic1.interfaces['uky-w2-nic1-p2'], sp4],
                                 node_id=sp4.node_id + '-DAC')
-        l5 = self.topo.add_link(name='l5', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l5 = self.topo.add_link(name='l5', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic2.interfaces['uky-w2-nic2-p1'], sp5],
                                 node_id=sp5.node_id + '-DAC')
-        l6 = self.topo.add_link(name='l6', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l6 = self.topo.add_link(name='l6', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic2.interfaces['uky-w2-nic2-p2'], sp6],
                                 node_id=sp6.node_id + '-DAC')
 
-        l8 = self.topo.add_link(name='l7', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l8 = self.topo.add_link(name='l7', ltype=f.LinkType.Patch,
                                 interfaces=[snw_shnic.interfaces['uky-w3-shnic-p1'], sp7],
                                 node_id=sp7.node_id + '-DAC')
 
-        l9 = self.topo.add_link(name='l8', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l9 = self.topo.add_link(name='l8', ltype=f.LinkType.Patch,
                                 interfaces=[snw_nic1.interfaces['uky-w3-nic1-p1'], sp8],
                                 node_id=sp8.node_id + '-DAC')
-        l10 = self.topo.add_link(name='l9', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l10 = self.topo.add_link(name='l9', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic1.interfaces['uky-w3-nic1-p2'], sp9],
                                  node_id=sp9.node_id + '-DAC')
-        l11 = self.topo.add_link(name='l10', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l11 = self.topo.add_link(name='l10', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic2.interfaces['uky-w3-nic2-p1'], sp10],
                                  node_id=sp10.node_id + '-DAC')
-        l12 = self.topo.add_link(name='l11', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l12 = self.topo.add_link(name='l11', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic2.interfaces['uky-w3-nic2-p2'], sp11],
                                  node_id=sp11.node_id + '-DAC')
         #
@@ -1024,7 +1026,7 @@ def testLBNLSiteAd(self):
         vf_vlans = [1001, 1002, 1003, 1004]
         gpuw_shnic = gpuw.add_component(name=gpuw.name + '-shnic', model='ConnectX-6',
                                         node_id=gpuw.node_id + '-shnic',
-                                        switch_fabric_node_id=gpuw.node_id + '-shnic-sf',
+                                        network_service_node_id=gpuw.node_id + '-shnic-sf',
                                         # there is one interface and we need one name
                                         interface_node_ids=[mac_to_node_id(pf_mac)],
                                         interface_labels=[f.Labels().set_fields(bdf=vf_ids,
@@ -1043,7 +1045,7 @@ def testLBNLSiteAd(self):
         vf_vlans = [1001, 1002, 1003, 1004]
         fnw_shnic = fnw.add_component(name=fnw.name + '-shnic', model='ConnectX-6',
                                       node_id=fnw.node_id + '-shnic',
-                                      switch_fabric_node_id=fnw.node_id + '-shnic-sf',
+                                      network_service_node_id=fnw.node_id + '-shnic-sf',
                                       # there is one interface and we need one name
                                       interface_node_ids=[mac_to_node_id(pf_mac)],
                                       interface_labels=[f.Labels().set_fields(bdf=vf_ids,
@@ -1062,7 +1064,7 @@ def testLBNLSiteAd(self):
         vf_vlans = [1001, 1002, 1003, 1004]
         snw_shnic = snw.add_component(name=snw.name + '-shnic', model='ConnectX-6',
                                       node_id=snw.node_id + '-shnic',
-                                      switch_fabric_node_id=snw.node_id + '-shnic-sf',
+                                      network_service_node_id=snw.node_id + '-shnic-sf',
                                       # there is one interface and we need one name
                                       interface_node_ids=[mac_to_node_id(pf_mac)],
                                       interface_labels=[f.Labels().set_fields(bdf=vf_ids,
@@ -1079,7 +1081,7 @@ def testLBNLSiteAd(self):
 
         fnw_nic1 = fnw.add_component(name=fnw.name + '-nic1', model='ConnectX-6',
                                      node_id=fnw.node_id + '-nic1',
-                                     switch_fabric_node_id=fnw.node_id + '-nic1-sf',
+                                     network_service_node_id=fnw.node_id + '-nic1-sf',
                                      interface_node_ids=[mac_to_node_id('04:3F:72:B7:15:6C'),
                                                          mac_to_node_id('04:3F:72:B7:15:6D')],
                                      interface_labels=[f.Labels().set_fields(mac='04:3F:72:B7:15:6C',
@@ -1094,7 +1096,7 @@ def testLBNLSiteAd(self):
 
         fnw_nic2 = fnw.add_component(name=fnw.name + '-nic2', model='ConnectX-6',
                                      node_id=fnw.node_id + '-nic2',
-                                     switch_fabric_node_id=fnw.node_id + '-nic2-sf',
+                                     network_service_node_id=fnw.node_id + '-nic2-sf',
                                      interface_node_ids=[mac_to_node_id('04:3F:72:B7:18:AC'),
                                                          mac_to_node_id('04:3F:72:B7:18:AD')],
                                      interface_labels=[f.Labels().set_fields(mac='04:3F:72:B7:18:AC',
@@ -1109,7 +1111,7 @@ def testLBNLSiteAd(self):
 
         snw_nic1 = snw.add_component(name=snw.name + '-nic1', model='ConnectX-5',
                                      node_id=snw.node_id + '-nic1',
-                                     switch_fabric_node_id=snw.node_id + '-nic1-sf',
+                                     network_service_node_id=snw.node_id + '-nic1-sf',
                                      interface_node_ids=[mac_to_node_id('0C:42:A1:91:75:12'),
                                                          mac_to_node_id('0C:42:A1:91:75:13')],
                                      interface_labels=[f.Labels().set_fields(mac='0C:42:A1:91:75:12',
@@ -1124,7 +1126,7 @@ def testLBNLSiteAd(self):
 
         snw_nic2 = snw.add_component(name=snw.name + '-nic2', model='ConnectX-5',
                                      node_id=snw.node_id + '-nic2',
-                                     switch_fabric_node_id=snw.node_id + '-nic2-sf',
+                                     network_service_node_id=snw.node_id + '-nic2-sf',
                                      interface_node_ids=[mac_to_node_id('0C:42:A1:91:75:0E'),
                                                          mac_to_node_id('0C:42:A1:91:75:0F')],
                                      interface_labels=[f.Labels().set_fields(mac='0C:42:A1:91:75:0E',
@@ -1148,8 +1150,9 @@ def testLBNLSiteAd(self):
                                     node_id='FOC2450R1AR',
                                     capacities=f.Capacities().set_fields(unit=1),
                                     ntype=f.NodeType.Switch)
-        dp_sf = switch.add_switch_fabric(name=switch.name+'-sf', layer=f.Layer.L2,
-                                         node_id=switch.node_id + '-sf')
+        dp_ns = switch.add_network_service(name=switch.name+'-ns',
+                                           node_id=switch.node_id + '-ns',
+                                           nstype=f.ServiceType.MPLS)
         # add ports
         port_caps = f.Capacities()
         port_caps1 = f.Capacities()
@@ -1157,42 +1160,42 @@ def testLBNLSiteAd(self):
         port_caps1.set_fields(bw=25)
 
         # FIXME: don't have port MAC addresses yet - placeholders
-        sp1 = dp_sf.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
+        sp1 = dp_ns.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:00'),
                                   capacities=port_caps)
 
-        sp2 = dp_sf.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
+        sp2 = dp_ns.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:01'),
                                   capacities=port_caps)
 
-        sp3 = dp_sf.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
+        sp3 = dp_ns.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:02'),
                                   capacities=port_caps)
-        sp4 = dp_sf.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
+        sp4 = dp_ns.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:03'),
                                   capacities=port_caps)
-        sp5 = dp_sf.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
+        sp5 = dp_ns.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:04'),
                                   capacities=port_caps)
-        sp6 = dp_sf.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
+        sp6 = dp_ns.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:05'),
                                   capacities=port_caps)
 
-        sp7 = dp_sf.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
+        sp7 = dp_ns.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:06'),
                                   capacities=port_caps)
 
         # FIXME: what to do about breakout ports in slownets?
-        sp8 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
+        sp8 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:07'),
                                   capacities=port_caps1)
-        sp9 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
+        sp9 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:08'),
                                   capacities=port_caps1)
-        sp10 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
+        sp10 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('20:00:00:00:00:09'),
                                    capacities=port_caps1)
-        sp11 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
+        sp11 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('20:00:00:00:00:0A'),
                                    capacities=port_caps1)
 
@@ -1201,41 +1204,41 @@ def testLBNLSiteAd(self):
         #
         # FIXME: Link node ids need to come from somewhere, could be an extension of interface ID
         # FIXME: on the switch, or something else
-        l1 = self.topo.add_link(name='l1', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l1 = self.topo.add_link(name='l1', ltype=f.LinkType.Patch,
                                 interfaces=[gpuw_shnic.interfaces['lbnl-w1-shnic-p1'], sp1],
                                 node_id=sp1.node_id + '-DAC')
 
-        l2 = self.topo.add_link(name='l2', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l2 = self.topo.add_link(name='l2', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_shnic.interfaces['lbnl-w2-shnic-p1'], sp2],
                                 node_id=sp2.node_id + '-DAC')
 
-        l3 = self.topo.add_link(name='l3', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l3 = self.topo.add_link(name='l3', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic1.interfaces['lbnl-w2-nic1-p1'], sp3],
                                 node_id=sp3.node_id + '-DAC')
-        l4 = self.topo.add_link(name='l4', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l4 = self.topo.add_link(name='l4', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic1.interfaces['lbnl-w2-nic1-p2'], sp4],
                                 node_id=sp4.node_id + '-DAC')
-        l5 = self.topo.add_link(name='l5', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l5 = self.topo.add_link(name='l5', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic2.interfaces['lbnl-w2-nic2-p1'], sp5],
                                 node_id=sp5.node_id + '-DAC')
-        l6 = self.topo.add_link(name='l6', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l6 = self.topo.add_link(name='l6', ltype=f.LinkType.Patch,
                                 interfaces=[fnw_nic2.interfaces['lbnl-w2-nic2-p2'], sp6],
                                 node_id=sp6.node_id + '-DAC')
 
-        l8 = self.topo.add_link(name='l7', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l8 = self.topo.add_link(name='l7', ltype=f.LinkType.Patch,
                                 interfaces=[snw_shnic.interfaces['lbnl-w3-shnic-p1'], sp7],
                                 node_id=sp7.node_id + '-DAC')
 
-        l9 = self.topo.add_link(name='l8', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l9 = self.topo.add_link(name='l8', ltype=f.LinkType.Patch,
                                 interfaces=[snw_nic1.interfaces['lbnl-w3-nic1-p1'], sp8],
                                 node_id=sp8.node_id + '-DAC')
-        l10 = self.topo.add_link(name='l9', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l10 = self.topo.add_link(name='l9', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic1.interfaces['lbnl-w3-nic1-p2'], sp9],
                                  node_id=sp9.node_id + '-DAC')
-        l11 = self.topo.add_link(name='l10', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l11 = self.topo.add_link(name='l10', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic2.interfaces['lbnl-w3-nic2-p1'], sp10],
                                  node_id=sp10.node_id + '-DAC')
-        l12 = self.topo.add_link(name='l11', ltype=f.LinkType.DAC, layer=f.Layer.L2,
+        l12 = self.topo.add_link(name='l11', ltype=f.LinkType.Patch,
                                  interfaces=[snw_nic2.interfaces['lbnl-w3-nic2-p2'], sp11],
                                  node_id=sp11.node_id + '-DAC')
         #
@@ -1316,8 +1319,10 @@ def testNetworkAd(self):
                                     node_id='FOC2450R1BL',
                                     capacities=f.Capacities().set_fields(unit=1),
                                     ntype=f.NodeType.Switch, stitch_node=True)
-        dp_sf = switch.add_switch_fabric(name=switch.name+'-sf', layer=f.Layer.L2,
-                                         node_id=switch.node_id + '-sf', stitch_node=True)
+        dp_ns = switch.add_network_service(name=switch.name+'-ns',
+                                           node_id=switch.node_id + '-ns',
+                                           nstype=f.ServiceType.MPLS,
+                                           stitch_node=True)
         # add ports
         port_caps = f.Capacities()
         port_caps1 = f.Capacities()
@@ -1328,51 +1333,51 @@ def testNetworkAd(self):
         #port_labs = f.Labels()
         #port_labs.set_fields(vlan_range='1000-2000')
         # FIXME: don't have port MAC addresses yet - placeholders
-        sp1 = dp_sf.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
+        sp1 = dp_ns.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:00'),
                                   capacities=port_caps, stitch_node=True)
 
-        sp2 = dp_sf.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
+        sp2 = dp_ns.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:01'),
                                   capacities=port_caps, stitch_node=True)
 
-        sp3 = dp_sf.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
+        sp3 = dp_ns.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:02'),
                                   capacities=port_caps, stitch_node=True)
-        sp4 = dp_sf.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
+        sp4 = dp_ns.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:03'),
                                   capacities=port_caps, stitch_node=True)
-        sp5 = dp_sf.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
+        sp5 = dp_ns.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:04'),
                                   capacities=port_caps, stitch_node=True)
-        sp6 = dp_sf.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
+        sp6 = dp_ns.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:05'),
                                   capacities=port_caps, stitch_node=True)
 
-        sp7 = dp_sf.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
+        sp7 = dp_ns.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:06'),
                                   capacities=port_caps, stitch_node=True)
 
         # FIXME: what to do about breakout ports in slownets?
-        sp8 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
+        sp8 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:07'),
                                   capacities=port_caps1, stitch_node=True)
-        sp9 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
+        sp9 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('00:00:00:00:00:08'),
                                   capacities=port_caps1, stitch_node=True)
-        sp10 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
+        sp10 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('00:00:00:00:00:09'),
                                    capacities=port_caps1, stitch_node=True)
-        sp11 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
+        sp11 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('00:00:00:00:00:0A'),
                                    capacities=port_caps1, stitch_node=True)
 
         # FIXME: fake ports towards UKY and LBNL
-        renc_uky = dp_sf.add_interface(name='HundredGigE 0/0/0/26', itype=f.InterfaceType.TrunkPort,
+        renc_uky = dp_ns.add_interface(name='HundredGigE 0/0/0/26', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('00:00:00:00:00:10'),
                                    capacities=port_caps)
 
-        renc_lbnl = dp_sf.add_interface(name='HundredGigE 0/0/0/27', itype=f.InterfaceType.TrunkPort,
+        renc_lbnl = dp_ns.add_interface(name='HundredGigE 0/0/0/27', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('00:00:00:00:00:11'),
                                    capacities=port_caps)
 
@@ -1383,9 +1388,10 @@ def testNetworkAd(self):
                                     node_id='FOC2451R0XZ',
                                     capacities=f.Capacities().set_fields(unit=1),
                                     ntype=f.NodeType.Switch, stitch_node=True)
-        dp_sf = switch.add_switch_fabric(name=switch.name+'-sf', layer=f.Layer.L2,
-                                         node_id=switch.node_id + '-sf',
-                                         stitch_node=True)
+        dp_ns = switch.add_network_service(name=switch.name+'-ns',
+                                           node_id=switch.node_id + '-ns',
+                                           nstype=f.ServiceType.MPLS,
+                                           stitch_node=True)
         # add ports
         port_caps = f.Capacities()
         port_caps1 = f.Capacities()
@@ -1396,51 +1402,51 @@ def testNetworkAd(self):
         #port_labs = f.Labels()
         #port_labs.set_fields(vlan_range='1000-2000')
         # FIXME: don't have port MAC addresses yet - placeholders
-        sp1 = dp_sf.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
+        sp1 = dp_ns.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:00'),
                                   capacities=port_caps, stitch_node=True)
 
-        sp2 = dp_sf.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
+        sp2 = dp_ns.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:01'),
                                   capacities=port_caps, stitch_node=True)
 
-        sp3 = dp_sf.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
+        sp3 = dp_ns.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:02'),
                                   capacities=port_caps, stitch_node=True)
-        sp4 = dp_sf.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
+        sp4 = dp_ns.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:03'),
                                   capacities=port_caps, stitch_node=True)
-        sp5 = dp_sf.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
+        sp5 = dp_ns.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:04'),
                                   capacities=port_caps, stitch_node=True)
-        sp6 = dp_sf.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
+        sp6 = dp_ns.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:05'),
                                   capacities=port_caps, stitch_node=True)
 
-        sp7 = dp_sf.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
+        sp7 = dp_ns.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:06'),
                                   capacities=port_caps, stitch_node=True)
 
         # FIXME: what to do about breakout ports in slownets?
-        sp8 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
+        sp8 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:07'),
                                   capacities=port_caps1, stitch_node=True)
-        sp9 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
+        sp9 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('10:00:00:00:00:08'),
                                   capacities=port_caps1, stitch_node=True)
-        sp10 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
+        sp10 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('10:00:00:00:00:09'),
                                    capacities=port_caps1, stitch_node=True)
-        sp11 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
+        sp11 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('10:00:00:00:00:0A'),
                                    capacities=port_caps1, stitch_node=True)
 
         # FIXME: fake ports towards RENC and LBNL
-        uky_renc = dp_sf.add_interface(name='HundredGigE 0/0/0/26', itype=f.InterfaceType.TrunkPort,
+        uky_renc = dp_ns.add_interface(name='HundredGigE 0/0/0/26', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('10:00:00:00:00:10'),
                                    capacities=port_caps)
 
-        uky_lbnl = dp_sf.add_interface(name='HundredGigE 0/0/0/27', itype=f.InterfaceType.TrunkPort,
+        uky_lbnl = dp_ns.add_interface(name='HundredGigE 0/0/0/27', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('10:00:00:00:00:11'),
                                    capacities=port_caps)
 
@@ -1452,8 +1458,10 @@ def testNetworkAd(self):
                                     node_id='FOC2450R1AR',
                                     capacities=f.Capacities().set_fields(unit=1),
                                     ntype=f.NodeType.Switch, stitch_node=True)
-        dp_sf = switch.add_switch_fabric(name=switch.name+'-sf', layer=f.Layer.L2,
-                                         node_id=switch.node_id + '-sf', stitch_node=True)
+        dp_ns = switch.add_network_service(name=switch.name+'-ns',
+                                           node_id=switch.node_id + '-ns',
+                                           nstype=f.ServiceType.MPLS,
+                                           stitch_node=True)
         # add ports
         port_caps = f.Capacities()
         port_caps1 = f.Capacities()
@@ -1464,45 +1472,45 @@ def testNetworkAd(self):
         #port_labs = f.Labels()
         #port_labs.set_fields(vlan_range='1000-2000')
         # FIXME: don't have port MAC addresses yet - placeholders
-        sp1 = dp_sf.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
+        sp1 = dp_ns.add_interface(name='HundredGigE 0/0/0/5', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:00'),
                                   capacities=port_caps,
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/5',
                                                                mac='20:00:00:00:00:00'),
                                   stitch_node=True)
 
-        sp2 = dp_sf.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
+        sp2 = dp_ns.add_interface(name='HundredGigE 0/0/0/13', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:01'),
                                   capacities=port_caps,
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/13',
                                                                mac='20:00:00:00:00:01'),
                                   stitch_node=True)
 
-        sp3 = dp_sf.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
+        sp3 = dp_ns.add_interface(name='HundredGigE 0/0/0/15', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:02'),
                                   capacities=port_caps,
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/15',
                                                                mac='20:00:00:00:00:02'),
                                   stitch_node=True)
-        sp4 = dp_sf.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
+        sp4 = dp_ns.add_interface(name='HundredGigE 0/0/0/9', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:03'),
                                   capacities=port_caps,
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/9',
                                                                mac='20:00:00:00:00:03'),
                                   stitch_node=True)
-        sp5 = dp_sf.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
+        sp5 = dp_ns.add_interface(name='HundredGigE 0/0/0/17', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:04'),
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/17',
                                                                mac='20:00:00:00:00:04'),
                                   capacities=port_caps, stitch_node=True)
-        sp6 = dp_sf.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
+        sp6 = dp_ns.add_interface(name='HundredGigE 0/0/0/19', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:05'),
                                   capacities=port_caps,
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/19',
                                                                mac='20:00:00:00:00:05'),
                                   stitch_node=True)
 
-        sp7 = dp_sf.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
+        sp7 = dp_ns.add_interface(name='HundredGigE 0/0/0/21', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:06'),
                                   capacities=port_caps,
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/21',
@@ -1510,25 +1518,25 @@ def testNetworkAd(self):
                                   stitch_node=True)
 
         # FIXME: what to do about breakout ports in slownets?
-        sp8 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
+        sp8 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.1', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:07'),
                                   capacities=port_caps1,
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/25.1',
                                                                mac='20:00:00:00:00:07'),
                                   stitch_node=True)
-        sp9 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
+        sp9 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.2', itype=f.InterfaceType.TrunkPort,
                                   node_id=mac_to_node_id('20:00:00:00:00:08'),
                                   capacities=port_caps1,
                                   labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/25.2',
                                                                mac='20:00:00:00:00:08'),
                                   stitch_node=True)
-        sp10 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
+        sp10 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.3', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('20:00:00:00:00:09'),
                                    capacities=port_caps1,
                                    labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/25.3',
                                                                 mac='20:00:00:00:00:09'),
                                    stitch_node=True)
-        sp11 = dp_sf.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
+        sp11 = dp_ns.add_interface(name='HundredGigE 0/0/0/25.4', itype=f.InterfaceType.TrunkPort,
                                    node_id=mac_to_node_id('20:00:00:00:00:0A'),
                                    capacities=port_caps1,
                                    labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/25.4',
@@ -1536,28 +1544,28 @@ def testNetworkAd(self):
                                    stitch_node=True)
 
         # FIXME: fake ports towards UKY and RENC
-        lbnl_renc = dp_sf.add_interface(name='HundredGigE 0/0/0/26', itype=f.InterfaceType.TrunkPort,
+        lbnl_renc = dp_ns.add_interface(name='HundredGigE 0/0/0/26', itype=f.InterfaceType.TrunkPort,
                                         node_id=mac_to_node_id('20:00:00:00:00:10'),
                                         labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/26',
                                                                      mac='20:00:00:00:00:10'),
                                         capacities=port_caps)
 
-        lbnl_uky = dp_sf.add_interface(name='HundredGigE 0/0/0/27', itype=f.InterfaceType.TrunkPort,
+        lbnl_uky = dp_ns.add_interface(name='HundredGigE 0/0/0/27', itype=f.InterfaceType.TrunkPort,
                                        node_id=mac_to_node_id('20:00:00:00:00:11'),
                                        labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/27',
                                                                     mac='20:00:00:00:00:11'),
                                        capacities=port_caps)
 
         # add 3 links
-        l1 = self.topo.add_link(name='l1', ltype=f.LinkType.L2Path, layer=f.Layer.L2,
+        l1 = self.topo.add_link(name='l1', ltype=f.LinkType.L2Path,
                                 interfaces=[renc_uky, uky_renc],
                                 node_id=renc_uky.node_id + '-Wave')
 
-        l2 = self.topo.add_link(name='l2', ltype=f.LinkType.L2Path, layer=f.Layer.L2,
+        l2 = self.topo.add_link(name='l2', ltype=f.LinkType.L2Path,
                                 interfaces=[uky_lbnl, lbnl_uky],
                                 node_id=uky_lbnl.node_id + '-Wave')
 
-        l3 = self.topo.add_link(name='l3', ltype=f.LinkType.L2Path, layer=f.Layer.L2,
+        l3 = self.topo.add_link(name='l3', ltype=f.LinkType.L2Path,
                                 interfaces=[renc_lbnl, lbnl_renc],
                                 node_id=renc_lbnl.node_id + '-Wave')