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

Issue:
Fixing the bug for reloading graph with the same GraphID. Now instead…
… of skipping the load with an error message that can get lost both Neo4j and NetworkX importers delete graph with the same id (if present) and then load; updated test cases and version (1.1rc6) 


Discussion / hints:

Fix patch:
diff --git a/fim/__init__.py b/fim/__init__.py
index 08cb9bc..5ad511e 100644
--- a/fim/__init__.py
+++ b/fim/__init__.py
@@ -1,2 +1,2 @@
 #
-__VERSION__ = "1.0.7"
+__VERSION__ = "1.1.0"
diff --git a/fim/graph/abc_property_graph.py b/fim/graph/abc_property_graph.py
index 01f0789..5e89927 100644
--- a/fim/graph/abc_property_graph.py
+++ b/fim/graph/abc_property_graph.py
@@ -45,8 +45,11 @@
 from fim.slivers.network_node import NodeSliver, CompositeNodeSliver
 from fim.slivers.network_link import NetworkLinkSliver
 from fim.slivers.path_info import PathInfo, ERO
+from fim.slivers.tags import Tags
+from fim.slivers.measurement_data import MeasurementData
 from fim.slivers.network_service import NetworkServiceSliver, NetworkServiceInfo, NSLayer
 from fim.graph.abc_property_graph_constants import ABCPropertyGraphConstants
+from fim.slivers.gateway import Gateway
 
 
 class GraphFormat(Enum):
@@ -91,7 +94,11 @@ class ABCPropertyGraph(ABCPropertyGraphConstants):
         "structural_info": ABCPropertyGraphConstants.PROP_STRUCTURAL_INFO,
         "ero": ABCPropertyGraphConstants.PROP_ERO,
         "path_info": ABCPropertyGraphConstants.PROP_PATH_INFO,
-        "controller_url": ABCPropertyGraphConstants.PROP_CONTROLLER_URL
+        "controller_url": ABCPropertyGraphConstants.PROP_CONTROLLER_URL,
+        "gateway": ABCPropertyGraphConstants.PROP_GATEWAY,
+        "mf_data": ABCPropertyGraphConstants.PROP_MEAS_DATA,
+        "tags": ABCPropertyGraphConstants.PROP_TAGS,
+        "boot_script": ABCPropertyGraphConstants.PROP_BOOT_SCRIPT
     }
 
     @abstractmethod
@@ -498,6 +505,11 @@ def base_sliver_to_graph_properties_dict(sliver: BaseSliver) -> Dict[str, str]:
             prop_dict[ABCPropertyGraph.PROP_NODE_MAP] = json.dumps(sliver.node_map)
         # boolean is always there. use json dumps for simplicity
         prop_dict[ABCPropertyGraph.PROP_STITCH_NODE] = json.dumps(sliver.stitch_node)
+        # this is already a JSON dict
+        if sliver.mf_data is not None:
+            prop_dict[ABCPropertyGraph.PROP_MEAS_DATA] = sliver.mf_data.data
+        if sliver.tags is not None:
+            prop_dict[ABCPropertyGraph.PROP_TAGS] = sliver.tags.to_json()
 
         return prop_dict
 
@@ -522,6 +534,8 @@ def node_sliver_to_graph_properties_dict(sliver: NodeSliver) -> Dict[str, str]:
             prop_dict[ABCPropertyGraph.PROP_SITE] = sliver.site
         if sliver.location is not None:
             prop_dict[ABCPropertyGraph.PROP_LOCATION] = sliver.location.to_json()
+        if sliver.boot_script is not None:
+            prop_dict[ABCPropertyGraph.PROP_BOOT_SCRIPT] = sliver.boot_script
 
         return prop_dict
 
@@ -574,6 +588,8 @@ def network_service_sliver_to_graph_properties_dict(sliver: NetworkServiceSliver
             prop_dict[ABCPropertyGraph.PROP_CONTROLLER_URL] = sliver.controller_url
         if sliver.site is not None:
             prop_dict[ABCPropertyGraph.PROP_SITE] = sliver.site
+        if sliver.gateway is not None:
+            prop_dict[ABCPropertyGraph.PROP_GATEWAY] = sliver.gateway.to_json()
 
         return prop_dict
 
@@ -639,7 +655,8 @@ def sliver_to_dict(sliver) -> Dict[str, Any]:
         elif type(sliver) == InterfaceSliver:
             d = ABCPropertyGraph.interface_sliver_to_graph_properties_dict(sliver)
         else:
-            raise RuntimeError(f'JSON Conversion for type {type(sliver)} is not supported.')
+            raise PropertyGraphQueryException(msg=f'JSON Conversion for type {type(sliver)} is not supported.',
+                                              graph_id=None, node_id=None)
 
         return d
 
@@ -680,7 +697,11 @@ def set_base_sliver_properties_from_graph_properties_dict(sliver: BaseSliver, d:
                               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]) if
-                              d.get(ABCPropertyGraph.PROP_STITCH_NODE, None) is not None else False
+                              d.get(ABCPropertyGraph.PROP_STITCH_NODE, None) is not None else False,
+                              tags=Tags.from_json(d.get(ABCPropertyGraph.PROP_TAGS, None)),
+                              mf_data=MeasurementData(d[ABCPropertyGraph.PROP_MEAS_DATA])
+                                                      if d.get(ABCPropertyGraph.PROP_MEAS_DATA, None)
+                                                         is not None else None
                               )
 
     @staticmethod
@@ -698,7 +719,8 @@ def node_sliver_from_graph_properties_dict(d: Dict[str, str]) -> NodeSliver:
                          allocation_constraints=d.get(ABCPropertyGraph.PROP_ALLOCATION_CONSTRAINTS, None),
                          service_endpoint=d.get(ABCPropertyGraph.PROP_SERVICE_ENDPOINT, None),
                          site=d.get(ABCPropertyGraphConstants.PROP_SITE, None),
-                         location=Location.from_json(d.get(ABCPropertyGraph.PROP_LOCATION, None))
+                         location=Location.from_json(d.get(ABCPropertyGraph.PROP_LOCATION, None)),
+                         boot_script=d.get(ABCPropertyGraph.PROP_BOOT_SCRIPT, None)
                          )
         return n
 
@@ -737,7 +759,8 @@ def network_service_sliver_from_graph_properties_dict(d: Dict[str, str]) -> Netw
                           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),
-                          site=d.get(ABCPropertyGraphConstants.PROP_SITE, None)
+                          site=d.get(ABCPropertyGraphConstants.PROP_SITE, None),
+                          gateway=Gateway.from_json(d.get(ABCPropertyGraphConstants.PROP_GATEWAY, None))
                           )
         return ns
 
@@ -1024,7 +1047,8 @@ def add_network_node_sliver(self, *, sliver: NodeSliver):
 
         if not self.check_node_unique(label=ABCPropertyGraph.CLASS_NetworkNode,
                                       name=sliver.resource_name):
-            raise RuntimeError(f'Node name {sliver.resource_name} must be unique.')
+            raise PropertyGraphQueryException(msg=f'Node name {sliver.resource_name} must be unique.',
+                                              graph_id=self.graph_id, node_id=None)
 
         props = self.node_sliver_to_graph_properties_dict(sliver)
         self.add_node(node_id=sliver.node_id, label=ABCPropertyGraph.CLASS_NetworkNode, props=props)
@@ -1083,7 +1107,8 @@ def add_network_service_sliver(self, *, parent_node_id: str, network_service: Ne
         if parent_node_id is None and not self.check_node_unique(label=ABCPropertyGraph.CLASS_NetworkService,
                                                                  name=network_service.resource_name):
             # slice-wide network services must have unique names
-            raise RuntimeError(f'Network service name {network_service.resource_name} must be unique.')
+            raise PropertyGraphQueryException(msg=f'Network service name {network_service.resource_name} must be unique.',
+                                              graph_id=self.graph_id, node_id=parent_node_id)
 
         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)
@@ -1097,17 +1122,21 @@ def add_network_service_sliver(self, *, parent_node_id: str, network_service: Ne
 
     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
+        Add interface to a network service, linking back to parent (node or network service),
+        if provided.
+        For most interface slivers there should be a parent with the exception
+        of FacilityPorts/StitchPorts.
+
+        :param parent_node_id: network service id or other parent if available
         :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)
+        if parent_node_id is not None:
+            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]:
         """
@@ -1133,9 +1162,10 @@ def get_all_node_or_component_connection_points(self, parent_node_id: str) -> Li
         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:
+                ABCPropertyGraph.CLASS_Component not in labels and \
+                ABCPropertyGraph.CLASS_CompositeNode not in labels:
             raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=parent_node_id,
-                                              msg="Parent node type is not NetworkNode or Component")
+                                              msg="Parent node type is not NetworkNode, CompositeNode 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,
@@ -1174,6 +1204,25 @@ def get_stitch_nodes(self) -> List[str]:
         :return:
         """
 
+    def find_peer_connection_points(self, *, node_id: str) -> List[str] or None:
+        """
+        Find the ids of the peer connection points to this one (connected over a Link)
+        if they exist
+        :param node_id: id of the interface/connection point
+        :return:
+        """
+        assert node_id is not None
+
+        # 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
+
+        return [x[1] for x in candidates]
+
 
 class ABCGraphImporter(ABC):
     @abstractmethod
diff --git a/fim/graph/abc_property_graph_constants.py b/fim/graph/abc_property_graph_constants.py
index 1fae57c..e3ddc7e 100644
--- a/fim/graph/abc_property_graph_constants.py
+++ b/fim/graph/abc_property_graph_constants.py
@@ -66,13 +66,18 @@ class ABCPropertyGraphConstants(ABC):
     PROP_ERO = "ERO"
     PROP_PATH_INFO = "PathInfo"
     PROP_CONTROLLER_URL = "ControllerURL"
+    PROP_GATEWAY = "Gateway"
+    PROP_TAGS = "Tags"
+    PROP_MEAS_DATA = "MeasurementData"
+    PROP_BOOT_SCRIPT = "BootScript"
     # 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_ERO, PROP_PATH_INFO, PROP_CAPACITY_HINTS]
+                           PROP_ERO, PROP_PATH_INFO, PROP_CAPACITY_HINTS, PROP_GATEWAY,
+                           PROP_TAGS, PROP_MEAS_DATA]
     # these properties cannot be unset
-    NO_UNSET_PROPERTIES = [GRAPH_ID, NODE_ID, PROP_TYPE, PROP_CLASS]
+    NO_UNSET_PROPERTIES = [GRAPH_ID, NODE_ID, PROP_TYPE, PROP_CLASS, PROP_NAME]
 
     CLASS_NetworkNode = 'NetworkNode'
     CLASS_NetworkService = 'NetworkService'
diff --git a/fim/graph/data/graph_validation_rules.json b/fim/graph/data/graph_validation_rules.json
index a80e59d..f7d1b5c 100644
--- a/fim/graph/data/graph_validation_rules.json
+++ b/fim/graph/data/graph_validation_rules.json
@@ -12,20 +12,20 @@
     "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\"])",
-    "msg": "All NetworkNodes muse be of type Server or Switch"
+    "rule": "MATCH (n:NetworkNode {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"Server\", \"Switch\", \"VM\", \"Container\", \"NAS\", \"Facility\"])",
+    "msg": "All NetworkNodes muse be of type Server, Switch, VM, Container, NAS or Facility"
   },
   {
     "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 SharedNIC, SmartNIC, GPU, FPGA or NVME"
   },
   {
-    "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:ConnectionPoint {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [\"AccessPort\", \"TrunkPort\", \"ServicePort\", \"DedicatedPort\", \"SharedPort\", \"vInt\", \"FacilityPort\", \"StitchPort\"])",
+    "msg": "All ConnectionPoints must be of type AccessPort, TrunkPort, ServicePort, DedicatedPort, SharedPort, vInt, FacilityPort or StitchPort"
   },
   {
-    "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:NetworkService {GraphID: $graphId}) RETURN ALL(r IN collect(n) WHERE r.Type IN [ \"P4\", \"OVS\", \"MPLS\", \"VLAN\", \"L2Path\", \"L2Bridge\", \"L2PTP\", \"L2STS\", \"FABNetv4\", \"FABNetv6\", \"L3VPN\", \"PortMirror\"])",
+    "msg": "All NetworkServices must be of type P4, OVS, MPLS, VLAN, 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 [\"Wave\", \"Patch\", \"L2Path\"])",
diff --git a/fim/graph/neo4j_property_graph.py b/fim/graph/neo4j_property_graph.py
index 9c8a016..e4fe9bc 100644
--- a/fim/graph/neo4j_property_graph.py
+++ b/fim/graph/neo4j_property_graph.py
@@ -706,6 +706,8 @@ def _import_graph(self, graphml_file: str, graph_id: str) -> None:
 
         assert graphml_file is not None
         assert graph_id is not None
+        # delete this graph if it exists
+        self.delete_graph(graph_id=graph_id)
         try:
             with self.driver.session() as session:
                 self.log.debug(f"Loading graph {graph_id} into Neo4j")
diff --git a/fim/graph/networkx_property_graph.py b/fim/graph/networkx_property_graph.py
index aff7876..c338307 100644
--- a/fim/graph/networkx_property_graph.py
+++ b/fim/graph/networkx_property_graph.py
@@ -687,10 +687,8 @@ def add_graph(self, graph_id: str, graph: nx.Graph) -> None:
             # check this graph_id isn't already present
             existing_graph_nodes = list(nxq.search_nodes(self.graphs, {'eq': [ABCPropertyGraph.GRAPH_ID, graph_id]}))
             if len(existing_graph_nodes) > 0:
-                # graph already present, warn and exit
-                if self.log is not None:
-                    self.log.warn('Attempting to insert a graph with the same GraphID, skipping')
-                return
+                # graph already present, delete it so we can replace
+                self.del_graph(graph_id)
             # relabel incoming graph nodes to integers, then merge
             temp_graph = nx.convert_node_labels_to_integers(graph, first_label=self.start_id)
             # set/overwrite GraphID property on all nodes
@@ -707,10 +705,8 @@ def add_graph_direct(self, graph_id: str, graph: nx.Graph) -> None:
             # check this graph_id isn't already present
             existing_graph_nodes = list(nxq.search_nodes(self.graphs, {'eq': [ABCPropertyGraph.GRAPH_ID, graph_id]}))
             if len(existing_graph_nodes) > 0:
-                # graph already present, warn and exit
-                if self.log is not None:
-                    self.log.warn('Attempting to insert a graph with the same GraphID, skipping')
-                return
+                # graph already present, delete so we can replace
+                self.del_graph(graph_id)
             # relabel incoming graph nodes to integers, then merge
             temp_graph = nx.convert_node_labels_to_integers(graph, first_label=self.start_id)
             self.start_id = self.start_id + len(temp_graph.nodes())
diff --git a/fim/graph/resources/abc_bqm.py b/fim/graph/resources/abc_bqm.py
index 0e38af3..6aba186 100644
--- a/fim/graph/resources/abc_bqm.py
+++ b/fim/graph/resources/abc_bqm.py
@@ -39,18 +39,28 @@ class ABCBQMPropertyGraph(ABCPropertyGraph):
     def __init__(self, *, graph_id: str, importer, logger=None):
         super().__init__(graph_id=graph_id, importer=importer, logger=logger)
 
-    def get_all_composite_node_components(self, parent_node_id: str) -> List[str]:
-        """
-        Return a list of components, children of a prent (presumably composite node)
-        :param parent_node_id:
-        :return:
-        """
+    def __get_node_components(self, parent_node_id: str, claz: str) -> List[str]:
         assert parent_node_id is not None
         # check that parent is a NetworkNode
         labels, parent_props = self.get_node_properties(node_id=parent_node_id)
-        if ABCPropertyGraph.CLASS_CompositeNode not in labels:
+        if claz not in labels:
             raise PropertyGraphQueryException(graph_id=self.graph_id, node_id=parent_node_id,
-                                              msg="Parent node type is not Composite")
+                                              msg=f"Parent node type is not {claz}")
         return self.get_first_neighbor(node_id=parent_node_id, rel=ABCPropertyGraph.REL_HAS,
                                        node_label=ABCPropertyGraph.CLASS_Component)
 
+    def get_all_composite_node_components(self, parent_node_id: str) -> List[str]:
+        """
+        Return a list of components, children of a prent (presumably composite node)
+        :param parent_node_id:
+        :return:
+        """
+        return self.__get_node_components(parent_node_id, ABCPropertyGraph.CLASS_CompositeNode)
+
+    def get_all_network_node_components(self, parent_node_id: str) -> List[str]:
+        """
+        Return a list of components, children of a prent (presumably composite node)
+        :param parent_node_id:
+        :return:
+        """
+        return self.__get_node_components(parent_node_id, ABCPropertyGraph.CLASS_NetworkNode)
diff --git a/fim/graph/resources/neo4j_cbm.py b/fim/graph/resources/neo4j_cbm.py
index 2b60ff0..53a7726 100644
--- a/fim/graph/resources/neo4j_cbm.py
+++ b/fim/graph/resources/neo4j_cbm.py
@@ -119,7 +119,7 @@ def merge_adm(self, *, adm: ABCPropertyGraph) -> None:
 
         # clone ADM with temporary ID - we will be changing its properties
         temp_adm_graph_id = str(uuid.uuid4())
-        self.log.info('CREATED TEMPORARY ADM GRAPH ID ' + temp_adm_graph_id +
+        self.log.debug('CREATED TEMPORARY ADM GRAPH ID ' + temp_adm_graph_id +
                       ' when merging graph ' + adm.graph_id + ' into ' + self.graph_id)
         temp_graph = adm.clone_graph(new_graph_id=temp_adm_graph_id)
         # crude typecasting
@@ -132,7 +132,7 @@ def merge_adm(self, *, adm: ABCPropertyGraph) -> None:
         temp_adm_graph.rewrite_delegations(real_adm_id=adm.graph_id)
 
         # update the structural info adm_graph_ids field to a single member list on all CBM nodes
-        si = StructuralInfo().set_fields(adm_graph_ids=[adm.graph_id])
+        si = StructuralInfo(adm_graph_ids=[adm.graph_id])
         temp_adm_graph.update_nodes_property(prop_name=self.PROP_STRUCTURAL_INFO,
                                              prop_val=si.to_json())
 
diff --git a/fim/graph/resources/networkx_abqm.py b/fim/graph/resources/networkx_abqm.py
index 3455497..d01b7a0 100644
--- a/fim/graph/resources/networkx_abqm.py
+++ b/fim/graph/resources/networkx_abqm.py
@@ -94,27 +94,6 @@ def check_node_name(self, *, node_id: str, label: str, name: str) -> bool:
                                             ]}))
         return len(graph_nodes) > 0
 
-    def get_all_node_or_component_connection_points(self, parent_node_id: str) -> List[str]:
-        """
-        Get a list of interfaces attached via network services to a composite node
-        :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 ABCPropertyGraphConstants.CLASS_CompositeNode not in labels:
-            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_NetworkService,
-                                                     rel2=ABCPropertyGraphConstants.REL_CONNECTS,
-                                                     node2_label=ABCPropertyGraphConstants.CLASS_ConnectionPoint)
-        ret = list()
-        # return only interface IDs, not interested in NetworkServices
-        for tup in sfs_ifs:
-            ret.append(tup[1])
-        return ret
-
 
 class NetworkXABQMFactory:
     """
diff --git a/fim/graph/slices/abc_asm.py b/fim/graph/slices/abc_asm.py
index 726ab13..669e2af 100644
--- a/fim/graph/slices/abc_asm.py
+++ b/fim/graph/slices/abc_asm.py
@@ -189,24 +189,3 @@ 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 find_peer_connection_point(self, *, node_id: str) -> str or None:
-        """
-        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:
-        """
-        assert node_id is not None
-
-        # 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=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/slivers/base_sliver.py b/fim/slivers/base_sliver.py
index 8b6416f..790f0bc 100644
--- a/fim/slivers/base_sliver.py
+++ b/fim/slivers/base_sliver.py
@@ -31,6 +31,8 @@
 
 from fim.slivers.capacities_labels import Capacities, CapacityHints, Labels, ReservationInfo, StructuralInfo
 from fim.slivers.delegations import Delegations
+from fim.slivers.tags import Tags
+from fim.slivers.measurement_data import MeasurementData
 
 
 class BaseSliver(ABC):
@@ -55,6 +57,8 @@ def __init__(self):
         self.details = None
         self.node_map = None
         self.stitch_node = False
+        self.tags = None # list of strings, limited in length
+        self.mf_data = None # opaque JSON object limited in length
 
     def set_type(self, resource_type):
         self.resource_type = resource_type
@@ -63,18 +67,21 @@ def get_type(self):
         return self.resource_type
 
     def set_name(self, resource_name: str):
+        assert(resource_name is None or isinstance(resource_name, str))
         self.resource_name = resource_name
 
     def get_name(self):
         return self.resource_name
 
     def set_model(self, resource_model: str):
+        assert(resource_model is None or isinstance(resource_model, str))
         self.resource_model = resource_model
 
     def get_model(self):
         return self.resource_model
 
     def set_capacities(self, cap: Capacities) -> None:
+        assert (cap is None or isinstance(cap, Capacities))
         assert(cap is None or isinstance(cap, Capacities))
         self.capacities = cap
 
@@ -144,7 +151,12 @@ def get_details(self) -> str:
         return self.details
 
     def set_node_map(self, node_map: Tuple[str, str]) -> None:
-        self.node_map = node_map
+        assert(node_map is None or isinstance(node_map, tuple) or
+               isinstance(node_map, list))
+        if node_map is not None:
+            self.node_map = tuple(node_map)
+        else:
+            self.node_map = None
 
     def get_node_map(self) -> Tuple[str, str] or None:
         return tuple(self.node_map) if self.node_map is not None else None
@@ -155,6 +167,21 @@ def set_stitch_node(self, stitch_node: bool) -> None:
     def get_stitch_node(self) -> bool:
         return self.stitch_node
 
+    def set_tags(self, tags: Tags) -> None:
+        assert(tags is None or isinstance(tags, Tags))
+        self.tags = tags
+
+    def get_tags(self) -> Tags or None:
+        return self.tags
+
+    def set_mf_data(self, mf_data: MeasurementData or None) -> None:
+        assert(mf_data is None or
+               isinstance(mf_data, MeasurementData))
+        self.mf_data = mf_data
+
+    def get_mf_data(self) -> MeasurementData or None:
+        return self.mf_data
+
     def set_properties(self, **kwargs):
         """
         Lets you set multiple properties exposed via setter methods
diff --git a/fim/slivers/capacities_labels.py b/fim/slivers/capacities_labels.py
index 12ab5ea..362b447 100644
--- a/fim/slivers/capacities_labels.py
+++ b/fim/slivers/capacities_labels.py
@@ -25,6 +25,7 @@
 # Author: Ilya Baldin (ibaldin@renci.org)
 from typing import List, Dict, Tuple
 import json
+import re
 
 from abc import ABC, abstractmethod
 
@@ -36,10 +37,28 @@
 
 class JSONField(ABC):
 
+    @classmethod
+    def update(cls, lab, **kwargs):
+        """
+        quasi-copy constructor if kwargs are ommitted,
+        otherwise also sets additional fields. Creates a new
+        instance of appropriate subclass and returns it.
+        DOES NOT UPDATE IN PLACE!
+        :param lab:
+        :param kwargs:
+        :return:
+        """
+        assert isinstance(lab, JSONField)
+        inst = lab.__class__()
+        for k, v in lab.__dict__.items():
+            inst.__setattr__(k, v)
+        inst._set_fields(**kwargs)
+        return inst
+
     @abstractmethod
-    def set_fields(self, **kwargs):
+    def _set_fields(self, **kwargs):
         """
-        Set the fields of this object
+        Abstract private set_fields method
         :param kwargs:
         :return:
         """
@@ -70,7 +89,7 @@ def from_json(cls, json_string: str):
             return None
         d = json.loads(json_string)
         ret = cls()
-        ret.set_fields(**d)
+        ret._set_fields(**d)
         return ret
 
     def to_dict(self) -> Dict[str, str] or None:
@@ -104,7 +123,11 @@ 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', 'burst_size': 'Mbits'}
+    UNITS = {'cpu': '', 'unit': '',
+             'core': '', 'ram': 'G',
+             'disk': 'G', 'bw': 'Gbps',
+             'burst_size': 'Mbits',
+             'mtu': 'B'}
 
     def __init__(self, **kwargs):
         self.cpu = 0
@@ -114,9 +137,10 @@ def __init__(self, **kwargs):
         self.bw = 0
         self.burst_size = 0
         self.unit = 0
-        self.set_fields(**kwargs)
+        self.mtu = 0
+        self._set_fields(**kwargs)
 
-    def set_fields(self, **kwargs):
+    def _set_fields(self, **kwargs):
         """
         Universal integer setter for all fields.
         Values should be non-negative integers. Throws a CapacityException
@@ -186,18 +210,21 @@ def negative_fields(self) -> List[str]:
 
         return ret
 
-    def positive_fields(self, fields: List[str]) -> bool:
+    def positive_fields(self, fields: List[str] or str) -> bool:
         """
         Return true if indicated fields are positive >0
-        :param fields:
+        :param fields: string or list of strings
         :return:
         """
         assert fields is not None
+        if isinstance(fields, str):
+            fields = [fields]
         for f in fields:
             if self.__dict__[f] <= 0:
                 return False
         return True
 
+
     def __str__(self):
         d = self.__dict__.copy()
         for k in self.__dict__:
@@ -247,9 +274,9 @@ class CapacityHints(JSONField):
     """
     def __init__(self, **kwargs):
         self.instance_type = None
-        self.set_fields(**kwargs)
+        self._set_fields(**kwargs)
 
-    def set_fields(self, **kwargs):
+    def _set_fields(self, **kwargs):
         """
         Universal setter for all fields. Values should be strings.
         Throws a LabelException if you try to set a non-existent field.
@@ -290,13 +317,48 @@ class Labels(JSONField):
     Class implementing various encodings of labels field, encoding
     and decoding from JSON dictionaries of properties
     """
+    VALIDATORS = {
+        'bdf': ('[0-9a-fA-F]{1,4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9a-fA-F]+', "0000:00:00.0"),
+        'mac': ('([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}', "00:11:22:33:44:55"),
+        'ipv4': (r'(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)',
+                 "192.168.1.1"),
+        'ipv4_range': (r'(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)-'
+                      r'(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)',
+                       "192.168.1.1-192.168.1.10"),
+        'ipv4_subnet': (r'(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/[\d]{1,2}',
+                        "192.168.1.0/24"),
+        'ipv6': (r'(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}',
+                 "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
+        'ipv6_range': (r'(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}-(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}',
+                      "2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:8334"),
+        # we allow fewer than 128 (and not necessarily 64) bits to be specified, unlike ipv6 address
+        # where we require the full 128 even if some of them are just ':::'
+        'ipv6_subnet': (r'(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}/[\d]{1,2}',
+                        "2001:0db8:85a3:0000:0000/48"),
+        'asn': (r'[\d]+', "12345"),
+        'vlan': (r'[\d]{1,4}', "1234"),
+        'vlan_range': (r'[\d]{1,4}-[\d]{1,4}', "100-200"),
+        'inner_vlan': (r'[\d]{1,4}', "1234")
+    }
+    LAMBDA_VALIDATORS = {
+        'vlan': ((lambda v: True if 0 < int(v) <= 4096 else False), "1-4096"),
+        'inner_vlan': ((lambda v: True if 0 < int(v) <= 4096 else False), "1-4096"),
+        'vlan_range': ((lambda v: True if 0 < int(v.split('-')[0]) <= 4096 and
+                                         0 < int(v.split('-')[1]) <= 4096 and
+                                         int(v.split('-')[0]) < int(v.split('-')[1]) else False),
+                       "1-4096"),
+        'asn': ((lambda a: True if 0 < int(a) < 65536 else False), "1-65535")
+    }
+
     def __init__(self, **kwargs):
         self.bdf = None
         self.mac = None
         self.ipv4 = None
         self.ipv4_range = None
+        self.ipv4_subnet = None
         self.ipv6 = None
         self.ipv6_range = None
+        self.ipv6_subnet = None
         self.asn = None
         self.vlan = None
         self.vlan_range = None
@@ -306,9 +368,9 @@ def __init__(self, **kwargs):
         self.local_name = None
         self.local_type = None
         self.device_name = None
-        self.set_fields(**kwargs)
+        self._set_fields(**kwargs)
 
-    def set_fields(self, **kwargs):
+    def _set_fields(self, **kwargs):
         """
         Universal setter for all fields. Values should be strings or lists of strings.
         Throws a LabelException if you try to set a non-existent field.
@@ -321,6 +383,33 @@ def set_fields(self, **kwargs):
             try:
                 # will toss an exception if field is not defined
                 self.__getattribute__(k)
+                if self.VALIDATORS.get(k, None) is not None:
+                    if isinstance(v, list):
+                        for i in v:
+                            matches = re.match('^' + self.VALIDATORS[k][0] + '$', i)
+                            if matches is None:
+                                raise LabelException(f'Provided label value {i} for {k} does not match the allowed '
+                                                     f'regular expression {self.VALIDATORS[k][0]}, valid example is '
+                                                     f'{self.VALIDATORS[k][1]}')
+                    else:
+                        matches = re.match('^' + self.VALIDATORS[k][0] + '$', v)
+                        if matches is None:
+                            raise LabelException(f'Provided label value {v} for {k} does not match the allowed '
+                                                 f'regular expression {self.VALIDATORS[k][0]}, valid example is '
+                                                 f'{self.VALIDATORS[k][1]}')
+                if self.LAMBDA_VALIDATORS.get(k, None) is not None:
+                    if isinstance(v, list):
+                        for i in v:
+                            res = self.LAMBDA_VALIDATORS[k][0](i)
+                            if res is False:
+                                raise LabelException(f'Provided label value {i} for {k} must be in a valid '
+                                                     f'range {self.LAMBDA_VALIDATORS[k][1]}')
+                    else:
+                        res = self.LAMBDA_VALIDATORS[k][0](v)
+                        if res is False:
+                            raise LabelException(f'Provided label value {v} for {k} must be in a valid '
+                                                 f'range {self.LAMBDA_VALIDATORS[k][1]}')
+
                 self.__setattr__(k, v)
             except AttributeError:
                 raise LabelException(f"Unable to set field {k} of labels, no such field available "
@@ -354,9 +443,9 @@ class ReservationInfo(JSONField):
     def __init__(self, **kwargs):
         self.reservation_id = None
         self.reservation_state = None
-        self.set_fields(**kwargs)
+        self._set_fields(**kwargs)
 
-    def set_fields(self, **kwargs):
+    def _set_fields(self, **kwargs):
         """
         Universal setter for all fields. Values should be strings or lists of strings.
         Throws a ReservationInfoException if you try to set a non-existent field.
@@ -382,12 +471,13 @@ class StructuralInfo(JSONField):
     Structural info on the for sliver objects (things like - is it a stitching node,
     what is parent graph or subgraph)
     """
-    def __init__(self):
+    def __init__(self, **kwargs):
         self.sub_graph_id = None
         self.parent_graph_id = None
         self.adm_graph_ids = None
+        self._set_fields(**kwargs)
 
-    def set_fields(self, **kwargs):
+    def _set_fields(self, **kwargs):
         """
         Universal setter for all fields. Values are strings or boolean.
         Throws a
@@ -413,9 +503,9 @@ class Location(JSONField):
     """
     def __init__(self, **kwargs):
         self.postal = None
-        self.set_fields(**kwargs)
+        self._set_fields(**kwargs)
 
-    def set_fields(self, **kwargs):
+    def _set_fields(self, **kwargs):
         """
         Universal setter for location fields. Values are strings.
         :param kwargs:
diff --git a/fim/slivers/component_catalog.py b/fim/slivers/component_catalog.py
index 78c6a8a..767215a 100644
--- a/fim/slivers/component_catalog.py
+++ b/fim/slivers/component_catalog.py
@@ -146,9 +146,9 @@ def generate_component(self, *, name: str, ctype: ComponentType = None, model: s
                 if isliver.labels is not None:
                     if isliver.labels.bdf is not None and \
                             isinstance(isliver.labels.bdf, list):
-                        isliver.labels.set_fields(local_name=[interface_name for k in range(len(isliver.labels.bdf))])
+                        isliver.labels._set_fields(local_name=[interface_name for k in range(len(isliver.labels.bdf))])
                     else:
-                        isliver.labels.set_fields(local_name=interface_name)
+                        isliver.labels._set_fields(local_name=interface_name)
                 else:
                     isliver.set_labels(Labels(local_name=interface_name))
                 # if labels are lists, extract the length to make it the number of units
diff --git a/fim/slivers/delegations.py b/fim/slivers/delegations.py
index 400098d..fd48f45 100644
--- a/fim/slivers/delegations.py
+++ b/fim/slivers/delegations.py
@@ -278,10 +278,10 @@ def from_json(cls, *, json_str: str, atype: DelegationType):
                     pool_id = v[ABCPropertyGraphConstants.FIELD_POOL_ID]
                 if atype == DelegationType.CAPACITY:
                     caporlabdict = v[ABCPropertyGraphConstants.FIELD_CAPACITIES]
-                    caporlab = Capacities().set_fields(**caporlabdict)
+                    caporlab = Capacities(**caporlabdict)
                 else:
                     caporlabdict = v[ABCPropertyGraphConstants.FIELD_LABELS]
-                    caporlab = Labels().set_fields(**caporlabdict)
+                    caporlab = Labels(**caporlabdict)
             elif ABCPropertyGraphConstants.FIELD_POOL in v.keys():
                 # pool reference
                 format = DelegationFormat.PoolReference
diff --git a/fim/slivers/gateway.py b/fim/slivers/gateway.py
new file mode 100644
index 0000000..c399211
--- /dev/null
+++ b/fim/slivers/gateway.py
@@ -0,0 +1,101 @@
+#!/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 fim.slivers.capacities_labels import Labels
+
+
+class Gateway:
+
+    def __init__(self, lab: Labels or None):
+        """
+        Labels specify IPv4 or IPv6 subnet, IPv4 or IPv6 gateway address and an optional MAC.
+        Specify the following fields on Labels object:
+        - ipv4_subnet and ipv4
+        or
+        - ipv6_subnet and ipv6
+        and an optional mac
+        :param lab:
+        """
+        if lab is None:
+            self.lab = None
+            return
+        # labels must only specify IPv4 or v6 subnet and optional mac
+        if lab.ipv4_subnet is not None and lab.ipv4 is not None:
+            self.lab = Labels(ipv4_subnet=lab.ipv4_subnet, ipv4=lab.ipv4)
+        elif lab.ipv6_subnet is not None and lab.ipv6 is not None:
+            self.lab = Labels(ipv6_subnet=lab.ipv6_subnet, ipv6=lab.ipv6)
+        else:
+            raise GatewayException('Gateway must specify IPv4 or IPv6 subnet, gateway address and an optional MAC')
+        if lab.mac is not None:
+            self.lab.mac = lab.mac
+
+    @property
+    def gateway(self) -> str:
+        if self.lab is not None:
+            return self.lab.ipv4 if self.lab.ipv4 is not None else self.lab.ipv6
+        return None
+
+    @property
+    def subnet(self) -> str:
+        if self.lab is not None:
+            return self.lab.ipv4_subnet if self.lab.ipv4_subnet is not None else self.lab.ipv6_subnet
+        return None
+
+    @property
+    def mac(self) -> str:
+        if self.lab is not None:
+            return self.lab.mac
+        return None
+
+    def to_json(self) -> str:
+        if self.lab is not None:
+            return self.lab.to_json()
+        return None
+
+    @classmethod
+    def from_json(cls, json_string: str):
+        return Gateway(Labels.from_json(json_string))
+
+    def __str__(self):
+        ar = list()
+        if self.lab is None:
+            return ""
+        if self.lab.ipv4_subnet is not None:
+            ar.append("IPv4 subnet: " + self.lab.ipv4_subnet + " GW: " + self.lab.ipv4)
+        else:
+            ar.append("IPv6: " + self.lab.ipv6_subnet + " GW: " + self.lab.ipv6)
+
+        if self.lab.mac is not None:
+            ar.append("(MAC: " + self.lab.mac + ")")
+        return " ".join(ar)
+
+    def __repr__(self):
+        return self.__str__()
+
+
+class GatewayException(Exception):
+
+    def __init__(self, msg: str):
+        assert msg is not None
+        super().__init__(f"Gateway exception: {msg}")
\ No newline at end of file
diff --git a/fim/slivers/instance_catalog.py b/fim/slivers/instance_catalog.py
index 5e2dc05..da0ba49 100644
--- a/fim/slivers/instance_catalog.py
+++ b/fim/slivers/instance_catalog.py
@@ -53,7 +53,7 @@ def __read_catalog() -> ViewOnlyDict:
             with open(catalog_file) as f:
                 catalog = json.load(f)
             assert isinstance(catalog, dict)
-            InstanceCatalog.__catalog_instance = {k: Capacities().set_fields(**v) for (k, v) in catalog.items()}
+            InstanceCatalog.__catalog_instance = {k: Capacities(**v) for (k, v) in catalog.items()}
         return ViewOnlyDict(InstanceCatalog.__catalog_instance)
 
     def get_instance_capacities(self, *, instance_type: str) -> Capacities or None:
diff --git a/fim/slivers/interface_info.py b/fim/slivers/interface_info.py
index 18f7ba2..2b41ac9 100644
--- a/fim/slivers/interface_info.py
+++ b/fim/slivers/interface_info.py
@@ -40,6 +40,8 @@ class InterfaceType(enum.Enum):
     DedicatedPort = enum.auto()
     SharedPort = enum.auto()
     vInt = enum.auto()
+    StitchPort = enum.auto()
+    FacilityPort = enum.auto()
 
     def help(self) -> str:
         return 'An ' + self.name
@@ -54,7 +56,7 @@ def __str__(self):
 class InterfaceSliver(BaseSliver):
 
     def __init__(self):
-        # addresses are labels, no need for additional fields
+        # addresses are stored on labels, bandwidth on capacities
         super().__init__()
 
     @staticmethod
diff --git a/fim/slivers/measurement_data.py b/fim/slivers/measurement_data.py
new file mode 100644
index 0000000..057ac97
--- /dev/null
+++ b/fim/slivers/measurement_data.py
@@ -0,0 +1,76 @@
+#!/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 abc import ABC
+from typing import Any
+
+import json
+
+
+class MeasurementData:
+    """
+    Measurement data attaches as a property on any graph node and is represented as an opaque JSON blob
+    """
+    def __init__(self, data: Any or None):
+        """
+        data has to be a valid JSON string
+        :param data:
+        """
+
+        if data is not None and isinstance(data, str):
+            if len(data) > 1024*1024:
+                raise MeasurementDataError(f'MeasurementData JSON string is too long ({len(data)} > 1024*1024')
+            try:
+                json.loads(data)
+            except json.JSONDecodeError:
+                raise MeasurementDataError(f'Unable to decode measurement data {data} as valid JSON ')
+            self._data = data
+        elif data is not None:
+            try:
+                self._data = json.dumps(data)
+                if len(self._data) > 1024*1024:
+                    raise MeasurementDataError(f'MeasurementData object is too large: {len(self._data)} > 1024*1024')
+            except TypeError:
+                raise MeasurementDataError(f'Unable to encode data as valid JSON')
+        else:
+            self._data = "{}"
+
+    @property
+    def data(self):
+        if self._data is not None:
+            return json.loads(self._data)
+        return self._data
+
+    def __str__(self):
+        return str(self._data)
+
+    def __repr(self):
+        return str(self)
+
+
+class MeasurementDataError(Exception):
+
+    def __init__(self, msg):
+        super().__init__(f'MeasurementDataError: invalid measurement data due to {msg}')
\ No newline at end of file
diff --git a/fim/slivers/network_node.py b/fim/slivers/network_node.py
index bcfec58..9a5a45a 100644
--- a/fim/slivers/network_node.py
+++ b/fim/slivers/network_node.py
@@ -39,6 +39,7 @@ class NodeType(enum.Enum):
     Container = enum.auto()
     Switch = enum.auto()
     NAS = enum.auto()
+    Facility = enum.auto()
 
     def help(self) -> str:
         return 'A ' + self.name
@@ -70,6 +71,7 @@ def __init__(self):
         self.network_service_info = None
         self.site = None
         self.location = None
+        self.boot_script = None
 
     #
     # Setters are only needed for things we want users to be able to set
@@ -120,6 +122,13 @@ def set_location(self, location: Location):
     def get_location(self) -> Location:
         return self.location
 
+    def set_boot_script(self, boot_script: str):
+        assert(boot_script is None or isinstance(boot_script, str))
+        self.boot_script = boot_script
+
+    def get_boot_script(self) -> str:
+        return self.boot_script
+
     @staticmethod
     def type_from_str(ntype: str) -> NodeType or None:
         if ntype is None:
diff --git a/fim/slivers/network_service.py b/fim/slivers/network_service.py
index 77595d6..84dd184 100644
--- a/fim/slivers/network_service.py
+++ b/fim/slivers/network_service.py
@@ -29,6 +29,7 @@
 
 from .base_sliver import BaseSliver
 from .path_info import PathRepresentationType, ERO, PathInfo
+from .gateway import Gateway
 
 
 class NSLayer(enum.Enum):
@@ -69,6 +70,7 @@ class ServiceType(enum.Enum):
     FABNetv6 = enum.auto() # FABRIC IPv6 routed network
     PortMirror = enum.auto() # FABRIC port mirroring service
     L3VPN = enum.auto() # FABRIC L3 VPN service
+    VLAN = enum.auto() # a local VLAN (internal to site)
 
     def help(self) -> str:
         return NetworkServiceSliver.ServiceConstraints[self].desc
@@ -102,6 +104,9 @@ class NetworkServiceSliver(BaseSliver):
         ServiceType.OVS: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT, num_sites=1,
                                                  num_instances=NO_LIMIT,
                                                  desc='An OVS generic service.'),
+        ServiceType.VLAN: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=NO_LIMIT, num_sites=1,
+                                                 num_instances=NO_LIMIT,
+                                                 desc='An local VLAN service in a site.'),
         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,
@@ -119,11 +124,11 @@ class NetworkServiceSliver(BaseSliver):
         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,
+        ServiceType.FABNetv4: ServiceConstraintRecord(layer=NSLayer.L3, num_interfaces=NO_LIMIT, num_sites=1,
+                                                      num_instances=NO_LIMIT,
                                                       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,
+        ServiceType.FABNetv6: ServiceConstraintRecord(layer=NSLayer.L3, num_interfaces=NO_LIMIT, num_sites=1,
+                                                      num_instances=NO_LIMIT,
                                                       desc='A routed IPv6 (publicly addressed) FABRIC network.'),
         ServiceType.PortMirror: ServiceConstraintRecord(layer=NSLayer.L2, num_interfaces=2, num_sites=1,
                                                         num_instances=1,
@@ -143,6 +148,7 @@ def __init__(self):
         self.path_info = None
         self.controller_url = None
         self.site = None
+        self.gateway = None
 
     #
     # Setters are only needed for things we want users to be able to set
@@ -189,6 +195,12 @@ def set_site(self, site: str):
     def get_site(self) -> str:
         return self.site
 
+    def set_gateway(self, gw: Gateway):
+        self.gateway = gw
+
+    def get_gateway(self) -> Gateway:
+        return self.gateway
+
     @staticmethod
     def type_from_str(ltype: str) -> ServiceType or None:
         if ltype is None:
diff --git a/fim/slivers/stitch_port.py b/fim/slivers/stitch_port.py
deleted file mode 100644
index 0100a72..0000000
--- a/fim/slivers/stitch_port.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/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: Komal Thareja (kthare10@renci.org)
-from .base_sliver import BaseSliver
-from .interface_info import InterfaceInfo
-
-
-class StitchPortSliver(BaseSliver):
-    def __init__(self):
-        super().__init__()
-        self.interface_info = None
-
-    def set_interface_info(self, interface_info: InterfaceInfo):
-        self.interface_info = interface_info
-
-    def get_interface_info(self) -> InterfaceInfo:
-        return self.interface_info
diff --git a/fim/slivers/tags.py b/fim/slivers/tags.py
new file mode 100644
index 0000000..b903bf9
--- /dev/null
+++ b/fim/slivers/tags.py
@@ -0,0 +1,98 @@
+#!/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 abc import ABC
+
+import re
+import json
+
+
+class Tags:
+    """
+    A class representing a list of string tags that are no longer than a specified limit
+    """
+    TAG_PATTERN="^[\\w-]{1,255}$"
+    compiled_pattern = re.compile(TAG_PATTERN)
+
+    def __init__(self, *kwargs):
+        """
+        Provide tags as a list or individual parameters
+        :param kwargs:
+        """
+        self.tags = list()
+        for k in kwargs:
+            if isinstance(k, list) or isinstance(k, tuple):
+                for t in k:
+                    Tags._check(t)
+                    self.tags.append(t)
+            else:
+                Tags._check(k)
+                self.tags.append(k)
+
+    def to_json(self) -> str:
+        """
+        Convert list of tags to JSON
+        :return:
+        """
+        return json.dumps(self.tags)
+
+    def __str__(self) -> str:
+        return str(self.tags)
+
+    def __repr(self) -> str:
+        return str(self)
+
+    @classmethod
+    def from_json(cls, json_string: str):
+        """
+        Create a Tags object from JSON string
+        :return:
+        """
+        if json_string is None or len(json_string) == 0 or json_string == "None":
+            return None
+
+        d = json.loads(json_string)
+        ret = cls(d)
+        return ret
+
+    @staticmethod
+    def _check(tag: str):
+        """
+        Check that the tag conforms to our rules - it is a string of regular characters
+        no longer than limit
+        :param tag:
+        :return:
+        """
+        if not isinstance(tag, str) or Tags.compiled_pattern.match(tag) is None:
+            raise TagException(f'Tag {tag} does not match expected pattern {Tags.TAG_PATTERN}')
+
+    def __iter__(self):
+        return iter(list(self.tags))
+
+
+class TagException(Exception):
+
+    def __init__(self, msg):
+        super().__init__(f'TagException: {msg}')
diff --git a/fim/user/README.md b/fim/user/README.md
index 8e151b1..97d8a01 100644
--- a/fim/user/README.md
+++ b/fim/user/README.md
@@ -44,14 +44,14 @@ 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)
+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.SmartNIC, model='ConnectX-6', name='nic2')
 cap = fu.Capacities()
-cap.set_fields(bw=50, unit=1)
+cap.__set_fields(bw=50, unit=1)
 lab = fu.Labels()
-lab.set_fields(ipv4="192.168.1.12")
+lab.__set_fields(ipv4="192.168.1.12")
 nic1.set_properties(capacities=cap, labels=lab)
 nic1.get_property('capacities')
 nic1.unset_property('labels')
diff --git a/fim/user/__init__.py b/fim/user/__init__.py
index 78b0fe9..45e8876 100644
--- a/fim/user/__init__.py
+++ b/fim/user/__init__.py
@@ -13,6 +13,10 @@
 import fim.slivers.capacities_labels as caplab
 import fim.slivers.delegations as dlg
 import fim.graph.abc_property_graph as abcpg
+import fim.slivers.path_info as pinfo
+import fim.slivers.gateway as gw
+import fim.slivers.tags as tgs
+import fim.slivers.measurement_data as mdata
 
 # push some definitions up to simplify import management
 
@@ -44,3 +48,8 @@
 Pool = dlg.Pool
 Pools = dlg.Pools
 GraphFormat = abcpg.GraphFormat
+Gateway = gw.Gateway
+PathInfo = pinfo.PathInfo
+ERO = pinfo.ERO
+Tags = tgs.Tags
+MeasurementData = mdata.MeasurementData
diff --git a/fim/user/component.py b/fim/user/component.py
index a7c1e4a..9fd8914 100644
--- a/fim/user/component.py
+++ b/fim/user/component.py
@@ -30,7 +30,7 @@
 
 from fim.view_only_dict import ViewOnlyDict
 from ..graph.abc_property_graph import ABCPropertyGraph
-from .model_element import ModelElement, ElementType
+from .model_element import ModelElement, ElementType, TopologyException
 from .network_service import NetworkService, ServiceType
 from .interface import Interface
 from ..slivers.capacities_labels import Labels
@@ -77,17 +77,17 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
         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 components to substrate topology nodes you must specify static Node ID")
+                raise TopologyException("When adding components 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("Parent node id must be specified for new components")
+                raise TopologyException("Parent node id must be specified for new components")
             if (model is None or ctype is None) and comp_model is None:
-                raise RuntimeError("Model and component type or comp_model must be specified for new components")
+                raise TopologyException("Model and component type or comp_model must be specified for new components")
             if str(topo.__class__) == "<class 'fim.user.topology.SubstrateTopology'>" and \
                     (ctype == ComponentType.SharedNIC or ctype == ComponentType.SmartNIC) and \
                     (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 '
+                raise TopologyException('For substrate topologies and components with network interfaces '
                                    'static network service node id, interface node ids and interface labels'
                                    'must be specified')
             super().__init__(name=name, node_id=node_id, topo=topo)
@@ -112,7 +112,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
             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_Component):
-                raise RuntimeError(f"Component with this id and name {name} doesn't exist")
+                raise TopologyException(f"Component with this id and name {name} doesn't exist")
 
     @property
     def type(self):
@@ -134,11 +134,14 @@ def get_property(self, pname: str) -> Any:
 
     def set_property(self, pname: str, pval: Any):
         """
-        Set a component property
+        Set a component property or unset of pval is None
         :param pname:
         :param pval:
         :return:
         """
+        if pval is None:
+            self.unset_property(pname)
+            return
         comp_sliver = ComponentSliver()
         comp_sliver.set_property(prop_name=pname, prop_val=pval)
         # write into the graph
diff --git a/fim/user/composite_node.py b/fim/user/composite_node.py
index 5bdfae6..ddb4256 100644
--- a/fim/user/composite_node.py
+++ b/fim/user/composite_node.py
@@ -28,6 +28,7 @@
 
 from fim.view_only_dict import ViewOnlyDict
 from .node import Node
+from .model_element import TopologyException
 from .interface import Interface
 
 from ..graph.abc_property_graph import ABCPropertyGraph
@@ -61,7 +62,7 @@ def __init__(self, *, name: str, node_id: str, topo: Any):
         existing_node_id = self.topo.graph_model.find_node_by_name(node_name=name,
                                                                    label=str(ABCPropertyGraph.CLASS_CompositeNode))
         if existing_node_id != node_id:
-            raise RuntimeError(f'Composite Node name {name} is not unique within the topology')
+            raise TopologyException(f'Composite Node name {name} is not unique within the topology')
 
     def __list_components(self) -> ViewOnlyDict:
         """
diff --git a/fim/user/interface.py b/fim/user/interface.py
index 5304ca9..5b6ce21 100644
--- a/fim/user/interface.py
+++ b/fim/user/interface.py
@@ -28,7 +28,7 @@
 
 import uuid
 
-from .model_element import ModelElement, ElementType
+from .model_element import ModelElement, ElementType, TopologyException
 
 from ..slivers.interface_info import InterfaceType, InterfaceSliver
 from ..graph.abc_property_graph import ABCPropertyGraph
@@ -60,11 +60,11 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
             # node id myst be specified for new nodes in substrate topologies
             if str(topo.__class__) == "<class 'fim.user.topology.SubstrateTopology'>" and \
                     node_id is None:
-                raise RuntimeError("When adding new nodes to substrate topology nodes you must specify static Node ID")
+                raise TopologyException("When adding new nodes to substrate topology nodes you must specify static Node ID")
             if node_id is None:
                 node_id = str(uuid.uuid4())
             if itype is None:
-                raise RuntimeError("When creating interfaces you must specify InterfaceType")
+                raise TopologyException("When creating interfaces you must specify InterfaceType")
             super().__init__(name=name, node_id=node_id, topo=topo)
             sliver = InterfaceSliver()
             sliver.node_id = self.node_id
@@ -78,14 +78,14 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
             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_ConnectionPoint):
-                raise RuntimeError(f"Interface with this id and name {name} doesn't exist")
+                raise TopologyException(f"Interface with this id {node_id} and name {name} doesn't exist")
 
     @property
     def type(self):
         return self.get_property('type') if self.__dict__.get('topo', None) is not None else None
 
     def add_child_interface(self):
-        raise RuntimeError("Not implemented")
+        raise TopologyException("Not implemented")
 
     def get_property(self, pname: str) -> Any:
         """
@@ -99,11 +99,14 @@ def get_property(self, pname: str) -> Any:
 
     def set_property(self, pname: str, pval: Any):
         """
-        Set a interface property
+        Set a interface property or unset if pval is None
         :param pname:
         :param pval:
         :return:
         """
+        if pval is None:
+            self.unset_property(pname)
+            return
         if_sliver = InterfaceSliver()
         if_sliver.set_property(prop_name=pname, prop_val=pval)
         # write into the graph
@@ -126,19 +129,29 @@ def set_properties(self, **kwargs):
     def list_properties() -> Tuple[str]:
         return InterfaceSliver.list_properties()
 
-    def get_peer(self):
+    def get_peers(self, itype: InterfaceType = None):
         """
-        Find a 'peer' interface connected across a Link. Returns Interface object.
+        Find 'peer' interfaces connected across a Link. Returns a list of Interface objects
+        (matching optional itype if specified, otherwise all).
+        :parm itype: optional type of peer interface we are looking for
         :return: peer Interface object or None
         """
-        peer_id = self.topo.graph_model.find_peer_connection_point(node_id=self.node_id)
-        if peer_id is None:
+        peer_ids = self.topo.graph_model.find_peer_connection_points(node_id=self.node_id)
+        if peer_ids is None:
             return None
-        clazzes, node_props = self.topo.graph_model.get_node_properties(node_id=peer_id)
-        assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
-        assert ABCPropertyGraph.CLASS_ConnectionPoint in clazzes
-        return Interface(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=peer_id,
-                         topo=self.topo)
+        ret = list()
+        for peer_id in peer_ids:
+            clazzes, node_props = self.topo.graph_model.get_node_properties(node_id=peer_id)
+            assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
+            assert ABCPropertyGraph.CLASS_ConnectionPoint in clazzes
+            i = Interface(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=peer_id,
+                          topo=self.topo)
+            if itype is not None:
+                if i.type == itype:
+                    ret.append(i)
+            else:
+                ret.append(i)
+        return ret
 
     def __repr__(self):
         _, node_properties = self.topo.graph_model.get_node_properties(node_id=self.node_id)
diff --git a/fim/user/link.py b/fim/user/link.py
index b8928a4..8a27f8a 100644
--- a/fim/user/link.py
+++ b/fim/user/link.py
@@ -27,7 +27,7 @@
 
 import uuid
 
-from .model_element import ModelElement, ElementType
+from .model_element import ModelElement, ElementType, TopologyException
 
 from fim.graph.abc_property_graph import ABCPropertyGraph
 from fim.user.interface import Interface
@@ -67,16 +67,16 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
             # node id myst be specified for new nodes in substrate topologies
             if str(topo.__class__) == "<class 'fim.user.topology.SubstrateTopology'>" and \
                     node_id is None:
-                raise RuntimeError("When adding new links to substrate topology nodes you must specify static Node ID")
+                raise TopologyException("When adding new links to substrate topology nodes you must specify static Node ID")
             if node_id is None:
                 node_id = str(uuid.uuid4())
             super().__init__(name=name, node_id=node_id, topo=topo)
             if ltype is None:
-                raise RuntimeError("When creating new links you must specify LinkType")
+                raise TopologyException("When creating new links you must specify LinkType")
             # 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 TopologyException("When creating new links you must specify the list of interfaces to connect.")
             self._interfaces = interfaces
             sliver = NetworkLinkSliver()
             sliver.node_id = self.node_id
@@ -98,7 +98,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
                 graph_model.find_node_by_name(node_name=name,
                                               label=ABCPropertyGraph.CLASS_Link)
             if existing_node_id != node_id:
-                raise RuntimeError(f'Link name {name} is not unique within the topology.')
+                raise TopologyException(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_ns_or_link_connection_points(link_id=self.node_id)
             name_id_tuples = list()
@@ -132,11 +132,14 @@ def get_property(self, pname: str) -> Any:
 
     def set_property(self, pname: str, pval: Any):
         """
-        Set a link property
+        Set a link property or unset if pval is None
         :param pname:
         :param pval:
         :return:
         """
+        if pval is None:
+            self.unset_property(pname)
+            return
         link_sliver = NetworkLinkSliver()
         link_sliver.set_property(prop_name=pname, prop_val=pval)
         # write into the graph
diff --git a/fim/user/model_element.py b/fim/user/model_element.py
index b093797..6037eae 100644
--- a/fim/user/model_element.py
+++ b/fim/user/model_element.py
@@ -30,6 +30,7 @@
 
 from ..graph.abc_property_graph import ABCPropertyGraph
 from ..slivers.capacities_labels import Capacities, Labels
+from ..slivers.measurement_data import MeasurementData
 
 
 class ElementType(enum.Enum):
@@ -104,7 +105,7 @@ def __str__(self):
     @abstractmethod
     def set_property(self, pname: str, pval):
         """
-        Set a property of a model element
+        Set a property of a model element or unset if pval is None
         :param pname:
         :param paval:
         :return:
@@ -170,4 +171,56 @@ def node_map(self):
     @node_map.setter
     def node_map(self, value):
         if self.__dict__.get('topo', None) is not None:
-            self.set_property('node_map', value)
\ No newline at end of file
+            self.set_property('node_map', value)
+
+    @property
+    def tags(self):
+        return self.get_property('tags') if self.__dict__.get('topo', None) is not None else None
+
+    @tags.setter
+    def tags(self, value):
+        if self.__dict__.get('topo', None) is not None:
+            self.set_property('tags', value)
+
+    @property
+    def mf_data(self):
+        d = self.get_property('mf_data') if self.__dict__.get('topo', None) is not None else None
+        return d.data if d is not None else None
+
+    @mf_data.setter
+    def mf_data(self, value):
+        if self.__dict__.get('topo', None) is not None:
+            if value is None or isinstance(value, MeasurementData):
+                self.set_property('mf_data', value)
+            else:
+                self.set_property('mf_data', MeasurementData(value))
+
+    def update_labels(self, **kwargs):
+        """
+        Take current labels field (even empty) and add/overwrite additional values into it
+        :param kwargs:
+        :return:
+        """
+        if self.labels is None:
+            self.set_property('labels', Labels(**kwargs))
+        else:
+            new_lab = Labels.update(self.labels, **kwargs)
+            self.set_property('labels', new_lab)
+
+    def update_capacities(self, **kwargs):
+        """
+        Take current capacities field (even empty) and add/overwrite additional values into it
+        :param kwargs:
+        :return:
+        """
+        if self.capacities is None:
+            self.set_property('capacities', Capacities(**kwargs))
+        else:
+            new_cap = Capacities.update(self.capacities, **kwargs)
+            self.set_property('capacities', new_cap)
+
+
+class TopologyException(Exception):
+
+    def __init__(self, msg: str):
+        super().__init__(msg)
\ No newline at end of file
diff --git a/fim/user/network_service.py b/fim/user/network_service.py
index 1237ece..e6cfbac 100644
--- a/fim/user/network_service.py
+++ b/fim/user/network_service.py
@@ -23,18 +23,20 @@
 #
 #
 # Author: Ilya Baldin (ibaldin@renci.org)
+import logging
 from typing import Tuple, Any, List, Set
 
 import uuid
 
 from fim.view_only_dict import ViewOnlyDict
 
-from .model_element import ModelElement, ElementType
+from .model_element import ModelElement, ElementType, TopologyException
 
 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, NSLayer, ERO, PathInfo
+from fim.slivers.gateway import Gateway
 
 
 class NetworkService(ModelElement):
@@ -76,16 +78,13 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
                 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")
+                raise TopologyException("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.")
+            if str(self.topo.__class__) == "<class 'fim.user.topology.ExperimentTopology'>" and interfaces:
                 sites = self.validate_service_constraints(nstype, interfaces)
 
+                # if a single-site service
                 if len(sites) == 1:
                     site = sites.pop()
 
@@ -113,7 +112,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any,
                 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} node id does not match the expected node id.')
+                raise TopologyException(f'Service name {name} node id does not match the expected node id.')
             # 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()
@@ -171,6 +170,15 @@ def path_info(self, value: PathInfo):
         if self.__dict__.get('topo', None) is not None:
             self.set_property('path_info', value)
 
+    @property
+    def gateway(self):
+        return self.get_property('gateway') if self.__dict__.get('topo', None) is not None else None
+
+    @gateway.setter
+    def gateway(self, gateway: Gateway):
+        if self.__dict__.get('topo', None) is not None:
+            self.set_property('gateway', gateway)
+
     def get_sliver(self) -> NetworkServiceSliver:
         """
         Get a deep sliver representation of this node from graph
@@ -184,20 +192,20 @@ def validate_service_constraints(self, nstype,  interfaces) -> Set[str]:
         Note that interfaces is a list of interfaces belonging to nodes!
         :param nstype:
         :param interfaces:
-        :return:
+        :return: a list of sites the service covers
         """
         # check the number of instances of this service
         if NetworkServiceSliver.ServiceConstraints[nstype].num_instances != NetworkServiceSliver.NO_LIMIT:
             services = self.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}")
+                raise TopologyException(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}")
+                raise TopologyException(f"Service of type {nstype} cannot have {len(interfaces)} interfaces. "
+                                        f"Limit: {NetworkServiceSliver.ServiceConstraints[nstype].num_interfaces}")
         sites = set()
         # check the number of sites spanned by this service
         if NetworkServiceSliver.ServiceConstraints[nstype].num_sites != NetworkServiceSliver.NO_LIMIT:
@@ -207,9 +215,10 @@ def validate_service_constraints(self, nstype,  interfaces) -> Set[str]:
                 if owner is None:
                     print(f'Interface {interface=} has no owner')
                 sites.add(owner.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}.")
+                raise TopologyException(f"Service of type {nstype} cannot span {len(sites)} sites. "
+                                        f"Limit: {NetworkServiceSliver.ServiceConstraints[nstype].num_sites}.")
 
         return sites
 
@@ -227,8 +236,8 @@ def __service_guardrails(sliver: NetworkServiceSliver, interface: Interface):
         # - L2S2S needs to warn that it may not work if the VMs with shared ports land on the same worker
         if sliver.get_type() == ServiceType.L2PTP and \
             interface.type == InterfaceType.SharedPort:
-            raise RuntimeError(f"Unable to connect interface {interface.name} to service {sliver.get_name()}: "
-                               f"L2P2P service currently doesn't support shared interfaces")
+            raise TopologyException(f"Unable to connect interface {interface.name} to service {sliver.get_name()}: "
+                                    f"L2P2P service currently doesn't support shared interfaces")
         if sliver.get_type() == ServiceType.L2STS and \
             interface.type == InterfaceType.SharedPort:
             print('WARNING: Current implementation of L2STS service does not support hairpins (connections withing the '
@@ -246,13 +255,13 @@ def __connect_interface(self, *, interface: Interface):
         assert interface is not None
         assert isinstance(interface, Interface)
 
-        # we can only connect interfaces connected to (compute or switch) nodes
+        # 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.')
+            raise TopologyException(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.')
+        peer_ids = self.topo.graph_model.find_peer_connection_points(node_id=interface.node_id)
+        if peer_ids is not None:
+            raise TopologyException(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)
@@ -275,28 +284,21 @@ def disconnect_interface(self, *, interface: Interface) -> None:
         """
         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:
+        peers = interface.get_peers()
+        if peers is None or len(peers) == 0:
             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)
+        if len(peers) > 1:
+            raise TopologyException(f'List of ServicePort peers for interface {interface} is more than one')
+
+        self.topo.graph_model.remove_cp_and_links(node_id=peers[0].node_id)
+        # remove from interface list as well
+        self._interfaces = list(filter((lambda x: x.node_id != peers[0].node_id), self._interfaces))
 
     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)
+        Add an interface to network service
         :param name:
         :param node_id:
         :param itype: interface type e.g. TrunkPort, AccessPort or VINT
@@ -304,13 +306,10 @@ def add_interface(self, *, name: str, node_id: str = None, itype: InterfaceType
         :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')
+            raise TopologyException('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)
@@ -318,15 +317,14 @@ def add_interface(self, *, name: str, node_id: str = None, itype: InterfaceType
 
     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.
+        Remove an ServicePort interface from the network service.
         :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")
+            raise TopologyException("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)
@@ -343,11 +341,14 @@ def get_property(self, pname: str) -> Any:
 
     def set_property(self, pname: str, pval: Any):
         """
-        Set a service property
+        Set a service property or unset of pval is None
         :param pname:
         :param pval:
         :return:
         """
+        if pval is None:
+            self.unset_property(pname)
+            return
         service_sliver = NetworkServiceSliver()
         service_sliver.set_property(prop_name=pname, prop_val=pval)
         # write into the graph
diff --git a/fim/user/node.py b/fim/user/node.py
index 228427b..8cdadfe 100644
--- a/fim/user/node.py
+++ b/fim/user/node.py
@@ -29,7 +29,7 @@
 import uuid
 
 from fim.view_only_dict import ViewOnlyDict
-from .model_element import ModelElement, ElementType
+from .model_element import ModelElement, ElementType, TopologyException
 from .component import Component, ComponentType
 from .network_service import NetworkService
 from .interface import Interface
@@ -38,7 +38,7 @@
 
 from ..slivers.network_node import NodeSliver
 from ..slivers.network_node import NodeType
-from ..slivers.network_service import ServiceType
+from ..slivers.network_service import ServiceType, NetworkServiceInfo
 from ..slivers.component_catalog import ComponentModelType
 from ..slivers.capacities_labels import CapacityHints, Location
 
@@ -56,10 +56,9 @@ class Node(ModelElement):
     network_services - returns a dictionary of network_services (mostly relevant
     to switches, not servers or VMs)
     """
-    SETTABLE_PROPERTIES = ['site', 'name']
 
     def __init__(self, *, name: str, node_id: str = None, topo: Any, etype: ElementType = ElementType.EXISTING,
-                 ntype: NodeType = None, site: str = None, **kwargs):
+                 ntype: NodeType = None, site: str = None, ns_info: NetworkServiceInfo = None, **kwargs):
         """
         Don't call this method yourself, call topology.add_node()
         node_id will be generated if not provided for experiment topologies
@@ -70,6 +69,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any, etype: ElementT
         :param etype: is this supposed to exist or new should be created
         :param ntype: node type if it is new
         :param site: node site
+        :param ns_info: NetworkServiceInfo structure
         :param kwargs: any additional properties
         """
         assert name is not None
@@ -80,20 +80,21 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any, etype: ElementT
             # node id myst be specified for new nodes in substrate topologies
             if str(topo.__class__) == "<class 'fim.user.topology.SubstrateTopology'>" and \
                     node_id is None:
-                raise RuntimeError("When adding new nodes to substrate topology nodes you must specify static Node ID")
+                raise TopologyException("When adding new nodes to substrate topology nodes you must specify static Node ID")
             if node_id is None:
                 node_id = str(uuid.uuid4())
             super().__init__(name=name, node_id=node_id, topo=topo)
             if ntype is None:
-                raise RuntimeError("When creating nodes you must specify NodeType")
+                raise TopologyException("When creating nodes you must specify NodeType")
             if site is None:
-                raise RuntimeError("When creating nodes you must specify site")
+                raise TopologyException("When creating nodes you must specify site")
 
             sliver = NodeSliver()
             sliver.node_id = self.node_id
             sliver.set_name(self.name)
             sliver.set_type(ntype)
             sliver.set_site(site)
+            sliver.network_service_info = ns_info
             sliver.set_properties(**kwargs)
             self.topo.graph_model.add_network_node_sliver(sliver=sliver)
         else:
@@ -103,7 +104,7 @@ def __init__(self, *, name: str, node_id: str = None, topo: Any, etype: ElementT
             existing_node_id = self.topo.graph_model.find_node_by_name(node_name=name,
                                                                        label=str(ABCPropertyGraph.CLASS_NetworkNode))
             if existing_node_id != node_id:
-                raise RuntimeError(f'Node name {name} is not unique within the topology')
+                raise TopologyException(f'Node name {name} is not unique within the topology')
 
     @property
     def site(self):
@@ -181,6 +182,15 @@ def direct_interfaces(self):
     def network_services(self):
         return self.__list_network_services()
 
+    @property
+    def boot_script(self):
+        return self.get_property('boot_script') if self.__dict__.get('topo', None) is not None else None
+
+    @boot_script.setter
+    def boot_script(self, value: str):
+        if self.__dict__.get('topo', None) is not None:
+            self.set_property('boot_script', value)
+
     def get_sliver(self) -> NodeSliver:
         """
         Get a deep sliver representation of this node from graph
@@ -201,11 +211,14 @@ def get_property(self, pname: str) -> Any:
 
     def set_property(self, pname: str, pval: Any):
         """
-        Set a node property
+        Set a node property or unset if pval is None
         :param pname:
         :param pval:
         :return:
         """
+        if pval is None:
+            self.unset_property(pname)
+            return
         node_sliver = NodeSliver()
         node_sliver.set_property(prop_name=pname, prop_val=pval)
         # write into the graph
@@ -249,7 +262,7 @@ def add_component(self, *, name: str, node_id: str = None, ctype: ComponentType
         assert name is not None
         # make sure name is unique within the node
         if name in self.__list_components().keys():
-            raise RuntimeError('Component names must be unique within node.')
+            raise TopologyException('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, comp_model=model_type,
@@ -260,7 +273,7 @@ def add_component(self, *, name: str, node_id: str = None, ctype: ComponentType
 
     def add_network_service(self, *, name: str, node_id: str = None, nstype: ServiceType, **kwargs) -> NetworkService:
         """
-        Add a network service to node (mostly needed in substrate topologies)
+        Add a network service to node ( needed in substrate topologies)
         :param name:
         :param node_id:
         :param layer:
@@ -270,7 +283,7 @@ def add_network_service(self, *, name: str, node_id: str = None, nstype: Service
         assert name is not None
         # make sure name is unique within the node
         if name in self.__list_network_services().keys():
-            raise RuntimeError('NetworkService names must be unique within node.')
+            raise TopologyException('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
diff --git a/fim/user/topology.py b/fim/user/topology.py
index 2149896..ff11946 100644
--- a/fim/user/topology.py
+++ b/fim/user/topology.py
@@ -35,13 +35,13 @@
 
 from fim.view_only_dict import ViewOnlyDict
 
-from .model_element import ModelElement
+from .model_element import ModelElement, TopologyException
 from ..slivers.network_node import NodeType
 from ..slivers.network_link import LinkType
 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
+from ..graph.abc_property_graph import ABCPropertyGraph, GraphFormat, PropertyGraphQueryException
 from ..graph.resources.networkx_arm import NetworkXARMGraph, NetworkXGraphImporter
 from ..slivers.delegations import Delegation, Delegations, Pools, DelegationType, DelegationFormat
 from fim.graph.resources.networkx_abqm import NetworkXAggregateBQM, NetworkXABQMFactory
@@ -96,7 +96,7 @@ def get_parent_element(self, e: ModelElement) -> ModelElement or None:
                                                              rel=ABCPropertyGraph.REL_HAS,
                                                              parent=ABCPropertyGraph.CLASS_NetworkNode)
             if node_id is None:
-                raise RuntimeError(f'Component {e} has no parent')
+                raise TopologyException(f'Component {e} has no parent')
             return Node(name=node_name, node_id=node_id, topo=self)
         # NSs may have nodes or components as parents or be by themselves
         if isinstance(e, NetworkService):
@@ -117,15 +117,17 @@ def get_parent_element(self, e: ModelElement) -> ModelElement or None:
                                                              parent=ABCPropertyGraph.CLASS_CompositeNode)
             if node_id is not None:
                 return CompositeNode(name=node_name, node_id=node_id, topo=self)
+
+            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_NetworkService)
             if node_id is None:
-                raise RuntimeError(f'Interface {e} has no parent')
+                raise TopologyException(f'Interface {e} has no parent')
             return NetworkService(name=node_name, node_id=node_id, topo=self)
-        raise RuntimeError(f'Unable to determine parent of element {e}')
+        raise TopologyException(f'Unable to determine parent of element {e}')
 
     def get_owner_node(self, e: ModelElement) -> ModelElement or None:
         """
@@ -154,7 +156,7 @@ def get_owner_node(self, e: ModelElement) -> ModelElement or None:
             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}')
+        return TopologyException(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:
@@ -188,7 +190,33 @@ def remove_node(self, name: str):
         """
         assert name is not None
         self.graph_model.remove_network_node_with_components_nss_cps_and_links(
-            node_id=self.__get_node_by_name(name=name).node_id)
+            node_id=self._get_node_by_name(name=name).node_id)
+
+    def add_facility(self, *, name: str, node_id: str = None, site: str, **kwargs) -> Node:
+        """
+        Add a facility node with VLAN service and FacilityPort interface as a single construct.
+        Works for aggregate topologies and experiment topologies the same way.
+        Link and interface node_ids are derived from node_id if it is provided.
+        :param name: name of facility (must match the advertisement)
+        :param node_id : optional node id of the facility node (all other node ids are derived if provided)
+        :param site: site of the facility (must match the advertisement)
+        :kwargs: parameters for the interface of the facility (bandwidth, mtu, LAN tags etc)
+        """
+        # should work with deep sliver reconstruction
+        facn = self.add_node(name=name, node_id=node_id, site=site, ntype=NodeType.Facility)
+        facs = facn.add_network_service(name=name + '-ns', node_id=node_id + '-ns' if node_id else None,
+                                        nstype=ServiceType.VLAN)
+        faci = facs.add_interface(name=name + '-int', node_id=node_id + '-int' if node_id else None,
+                                  itype=InterfaceType.FacilityPort, **kwargs)
+
+        return facn
+
+    def remove_facility(self, *, name: str):
+        """
+        Remove a facility and associated network service and interface, disconnecting it from a
+        service as appropriate. Same as removing a node.
+        """
+        self.remove_node(name)
 
     def add_link(self, *, name: str, node_id: str = None, ltype: LinkType,
                  interfaces: List[Interface], technology: str = None,
@@ -216,7 +244,7 @@ def remove_link(self, name: str):
         :return:
         """
         assert name is not None
-        self.graph_model.remove_network_link(node_id=self.__get_link_by_name(name=name).node_id)
+        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:
@@ -241,7 +269,7 @@ def remove_network_service(self, name: str):
         :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)
+        self.graph_model.remove_ns_with_cps_and_links(node_id=self._get_ns_by_name(name=name).node_id)
 
     def validate(self):
         """
@@ -257,12 +285,17 @@ def validate(self):
             # owning nodes for each interface so we search for proper interfaces
             for si in service_interfaces:
                 if si.type == InterfaceType.ServicePort:
-                    node_interfaces.append(si.get_peer())
+                    # there should only ever be one peer for a service port
+                    peers = si.get_peers()
+                    if peers is None or len(peers) != 1:
+                        raise TopologyException(f'Interface {si} of Network Service {s} has unexpected '
+                                                f'number of peer interfaces')
+                    node_interfaces.append(si.get_peers()[0])
                 else:
                     node_interfaces.append(si)
             s.validate_service_constraints(s.type, node_interfaces)
 
-    def __get_node_by_name(self, name: str) -> Node:
+    def _get_node_by_name(self, name: str) -> Node:
         """
         Find node by its name, return Node object
         :param name:
@@ -273,7 +306,7 @@ def __get_node_by_name(self, name: str) -> Node:
                                                      label=ABCPropertyGraph.CLASS_NetworkNode)
         return Node(name=name, node_id=node_id, topo=self)
 
-    def __get_link_by_name(self, name: str) -> Link:
+    def _get_link_by_name(self, name: str) -> Link:
         """
         Find link by its name, return Link object
         :param name:
@@ -284,7 +317,7 @@ 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:
+    def _get_ns_by_name(self, name: str) -> NetworkService:
         """
         Find a network service by its name, return NetworkService object
         :param name:
@@ -295,7 +328,7 @@ def __get_ns_by_name(self, name: str) -> NetworkService:
                                                      label=ABCPropertyGraph.CLASS_NetworkService)
         return NetworkService(name=name, node_id=node_id, topo=self)
 
-    def __get_node_by_id(self, node_id: str) -> Node:
+    def _get_node_by_id(self, node_id: str) -> Node:
         """
         Get node by its node_id, return Node object
         :param node_id:
@@ -306,7 +339,7 @@ def __get_node_by_id(self, node_id: str) -> Node:
         assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
         return Node(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id, topo=self)
 
-    def __get_link_by_id(self, node_id: str) -> Link:
+    def _get_link_by_id(self, node_id: str) -> Link:
         """
         Get link by its node_id, return Link object
         :param node_id:
@@ -317,7 +350,7 @@ 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:
+    def _get_ns_by_id(self, node_id: str) -> NetworkService:
         """
         Get network service by its node id, return NetworkService object
         :param node_id:
@@ -328,7 +361,7 @@ def __get_ns_by_id(self, node_id: str) -> NetworkService:
         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:
+    def _list_nodes(self) -> ViewOnlyDict:
         """
         List all NetworkNodes in the topology as a dictionary
         organized by node name. Modifying the dictionary will not affect
@@ -338,11 +371,11 @@ def __list_nodes(self) -> ViewOnlyDict:
         node_id_list = self.graph_model.get_all_network_nodes()
         ret = dict()
         for nid in node_id_list:
-            n = self.__get_node_by_id(nid)
+            n = self._get_node_by_id(nid)
             ret[n.name] = n
         return ViewOnlyDict(ret)
 
-    def __list_network_services(self) -> ViewOnlyDict:
+    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
@@ -352,11 +385,11 @@ def __list_network_services(self) -> ViewOnlyDict:
         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)
+            n = self._get_ns_by_id(nid)
             ret[n.name] = n
         return ViewOnlyDict(ret)
 
-    def __list_links(self) -> ViewOnlyDict:
+    def _list_links(self) -> ViewOnlyDict:
         """
         List all Links in the topology as a dictionary organized by Link name.
         :return:
@@ -364,13 +397,13 @@ def __list_links(self) -> ViewOnlyDict:
         link_id_list = self.graph_model.get_all_network_links()
         ret = dict()
         for nid in link_id_list:
-            n = self.__get_link_by_id(nid)
+            n = self._get_link_by_id(nid)
             ret[n.name] = n
         return ViewOnlyDict(ret)
 
-    def __list_of_interfaces(self) -> Tuple[Any]:
+    def _list_of_interfaces(self) -> Tuple[Any]:
         """
-        List all interfaces of the topology as a dictionary
+        List all interfaces of the topology as a dictionary.
         :return:
         """
         ret = list()
@@ -380,19 +413,19 @@ def __list_of_interfaces(self) -> Tuple[Any]:
 
     @property
     def nodes(self):
-        return self.__list_nodes()
+        return self._list_nodes()
 
     @property
     def links(self):
-        return self.__list_links()
+        return self._list_links()
 
     @property
     def network_services(self):
-        return self.__list_network_services()
+        return self._list_network_services()
 
     @property
     def interface_list(self):
-        return self.__list_of_interfaces()
+        return self._list_of_interfaces()
 
     def serialize(self, file_name: str = None, fmt: GraphFormat = GraphFormat.GRAPHML) -> str or None:
         """
@@ -418,7 +451,7 @@ def load(self, *, file_name: str = None, graph_string: str = None):
         :return:
         """
         if file_name is None and graph_string is None:
-            raise RuntimeError('Either file_name or graph_string parameters must be specified.')
+            raise TopologyException('Either file_name or graph_string parameters must be specified.')
         if file_name is not None:
             nx_pgraph = self.graph_model.importer.import_graph_from_file_direct(graph_file=file_name)
         else:
@@ -427,6 +460,66 @@ def load(self, *, file_name: str = None, graph_string: str = None):
                                        importer=nx_pgraph.importer,
                                        logger=nx_pgraph.log)
 
+    @staticmethod
+    def __print_caplabs__(caps) -> str:
+        if caps is None:
+            return '{ }'
+        else:
+            return str(caps)
+
+    def __str__(self):
+        """
+        Print topology in tabulated form - network nodes, their components, interfaces, then print links
+        and their interfaces
+        :return:
+        """
+        lines = list()
+        for n in self.nodes.values():
+            lines.append(n.name + "[" + str(n.type) + ", " +
+                         str(n.site) + "]:  " +
+                         self.__print_caplabs__(n.capacities))
+            for i in n.direct_interfaces.values():
+                lines.append("\t\t" + i.name + ": " + str(i.type) + " " +
+                             self.__print_caplabs__(i.capacities) + " " +
+                             self.__print_caplabs__(i.labels))
+            for c in n.components.values():
+                lines.append("\t" + c.name + ": " + " " + str(c.type) + " " +
+                             c.model)
+                for i in c.interfaces.values():
+                    lines.append("\t\t" + i.name + ": " + str(i.type) + " " +
+                                 self.__print_caplabs__(i.capacities))
+        lines.append("Links:")
+        for l in self.links.values():
+            interface_names = [iff.name for iff in l.interface_list]
+            lines.append("\t" + l.name + "[" + str(l.type) + "]: " +
+                         str(interface_names))
+
+        return "\n".join(lines)
+
+
+class ExperimentTopology(Topology):
+    """
+    Define an user topology model, inheriting behavior from Topology class.
+    In addition to publicly visible methods the following calls can be made:
+    topology.nodes - a read-only dictionary of nodes in the topology
+    topology.links - a read-only dictionary of links in the topology
+    topology.interface_list - a read-only list of all interfaces of all nodes
+    If you want to operate on top of a Neo4j graph, use the cast() method.
+    """
+    def __init__(self, graph_file: str = None, graph_string: str = None, logger=None):
+        super().__init__(graph_file=graph_file, graph_string=graph_string, logger=logger)
+
+    def cast(self, *, asm_graph: ABCASMPropertyGraph):
+        """
+        'Cast' an existing instance of ASM graph into a topology. This can create Neo4j or NetworkX-based
+        topology classes
+        :param asm_graph:
+        :return:
+        """
+        assert asm_graph is not None
+        assert isinstance(asm_graph, ABCASMPropertyGraph)
+        self.graph_model = asm_graph
+
     def draw(self, *, file_name: str = None, interactive: bool = False,
              topo_detail: TopologyDetail = TopologyDetail.Derived,
              layout=nx.spring_layout):
@@ -482,16 +575,17 @@ def draw(self, *, file_name: str = None, interactive: bool = False,
             # 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:
+                    peer_int_ids = self.graph_model.find_peer_connection_points(node_id=nint.node_id)
+                    if peer_int_ids is None:
                         continue
-                    derived_graph.add_edge(ns.name, peer_int_parent.name)
+                    for peer_int_id in peer_int_ids:
+                        _, 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)
@@ -503,67 +597,7 @@ def draw(self, *, file_name: str = None, interactive: bool = False,
             if file_name is not None:
                 plt.savefig(file_name)
         else:
-            raise RuntimeError("This level of detail not yet implemented")
-
-    @staticmethod
-    def __print_caplabs__(caps) -> str:
-        if caps is None:
-            return '{ }'
-        else:
-            return str(caps)
-
-    def __str__(self):
-        """
-        Print topology in tabulated form - network nodes, their components, interfaces, then print links
-        and their interfaces
-        :return:
-        """
-        lines = list()
-        for n in self.nodes.values():
-            lines.append(n.name + "[" + str(n.type) + ", " +
-                         str(n.site) + "]:  " +
-                         self.__print_caplabs__(n.capacities))
-            for i in n.direct_interfaces.values():
-                lines.append("\t\t" + i.name + ": " + str(i.type) + " " +
-                             self.__print_caplabs__(i.capacities) + " " +
-                             self.__print_caplabs__(i.labels))
-            for c in n.components.values():
-                lines.append("\t" + c.name + ": " + " " + str(c.type) + " " +
-                             c.model)
-                for i in c.interfaces.values():
-                    lines.append("\t\t" + i.name + ": " + str(i.type) + " " +
-                                 self.__print_caplabs__(i.capacities))
-        lines.append("Links:")
-        for l in self.links.values():
-            interface_names = [iff.name for iff in l.interface_list]
-            lines.append("\t" + l.name + "[" + str(l.type) + "]: " +
-                         str(interface_names))
-
-        return "\n".join(lines)
-
-
-class ExperimentTopology(Topology):
-    """
-    Define an user topology model, inheriting behavior from Topology class.
-    In addition to publicly visible methods the following calls can be made:
-    topology.nodes - a read-only dictionary of nodes in the topology
-    topology.links - a read-only dictionary of links in the topology
-    topology.interface_list - a read-only list of all interfaces of all nodes
-    If you want to operate on top of a Neo4j graph, use the cast() method.
-    """
-    def __init__(self, graph_file: str = None, graph_string: str = None, logger=None):
-        super().__init__(graph_file=graph_file, graph_string=graph_string, logger=logger)
-
-    def cast(self, *, asm_graph: ABCASMPropertyGraph):
-        """
-        'Cast' an existing instance of ASM graph into a topology. This can create Neo4j or NetworkX-based
-        topology classes
-        :param asm_graph:
-        :return:
-        """
-        assert asm_graph is not None
-        assert isinstance(asm_graph, ABCASMPropertyGraph)
-        self.graph_model = asm_graph
+            raise TopologyException("This level of detail not yet implemented")
 
 
 class SubstrateTopology(Topology):
@@ -651,7 +685,7 @@ 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
-                # network services (for eg switches)
+                # network services (for eg switches and facilities)
                 for sf in n.network_services.values():
                     delegations = self.__copy_to_delegations(e=sf, atype=t,
                                                              delegation_id=delegation_id)
@@ -672,8 +706,10 @@ class AdvertizedTopology(Topology):
     Topology object to operate on BQM (Query) models returned from e.g.
     listResources etc.
     In addition to publicly visible methods the following calls can be made:
-    topology.sites - a read-only dictionary of nodes in the topology
+    topology.sites - a read-only dictionary of sites in the topology
+    topology.nodes - a read-only dictionary of nodes
     topology.links - a read-only dictionary of links in the topology
+    topology.facilities - a read-only dictionary of facilities
     """
 
     def __init__(self, graph_file: str = None, graph_string: str = None, logger=None):
@@ -686,12 +722,12 @@ def __init__(self, graph_file: str = None, graph_string: str = None, logger=None
 
     def add_node(self, *, name: str, node_id: str = None, site: str, ntype: NodeType = NodeType.VM,
                  **kwargs) -> Node:
-        raise RuntimeError('Cannot add node to advertisement')
+        raise TopologyException('Cannot add node to advertisement')
 
     def add_link(self, *, name: str, node_id: str = None, ltype: LinkType = None,
                  interfaces: List[Interface], layer: NSLayer = None, technology: str = None,
                  **kwargs) -> Link:
-        raise RuntimeError('Cannot add link to advertisement')
+        raise TopologyException('Cannot add link to advertisement')
 
     def load(self, *, file_name: str = None, graph_string: str = None):
         """
@@ -701,25 +737,28 @@ def load(self, *, file_name: str = None, graph_string: str = None):
         :return:
         """
         if file_name is None and graph_string is None:
-            raise RuntimeError('Either file_name or graph_string must be specified.')
+            raise TopologyException('Either file_name or graph_string must be specified.')
         if file_name is not None:
             nx_pgraph = self.graph_model.importer.import_graph_from_file_direct(graph_file=file_name)
         else:
             nx_pgraph = self.graph_model.importer.import_graph_from_string_direct(graph_string=graph_string)
         self.graph_model = NetworkXABQMFactory.create(nx_pgraph)
 
-    def __get_node_by_id(self, node_id: str) -> Node:
+    def _get_node_by_id(self, node_id: str) -> Node:
         """
-        Get node by its node_id, return Node object
+        Get node by its node_id, return Node or CompositeNode object
         :param node_id:
         :return:
         """
         assert node_id is not None
-        _, node_props = self.graph_model.get_node_properties(node_id=node_id)
+        claz, node_props = self.graph_model.get_node_properties(node_id=node_id)
         assert node_props.get(ABCPropertyGraph.PROP_NAME, None) is not None
-        return CompositeNode(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id, topo=self)
+        if claz[0] == ABCPropertyGraph.CLASS_CompositeNode:
+            return CompositeNode(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id, topo=self)
+        elif claz[0] == ABCPropertyGraph.CLASS_NetworkNode:
+            return Node(name=node_props[ABCPropertyGraph.PROP_NAME], node_id=node_id, topo=self)
 
-    def __get_link_by_id(self, node_id: str) -> Link:
+    def _get_link_by_id(self, node_id: str) -> Link:
         """
         Get link by its node_id, return Link object
         :param node_id:
@@ -738,15 +777,15 @@ def __list_sites(self) -> ViewOnlyDict:
         node_id_list = self.graph_model.get_all_composite_nodes()
         ret = dict()
         for nid in node_id_list:
-            n = self.__get_node_by_id(nid)
+            n = self._get_node_by_id(nid)
             ret[n.name] = n
         return ViewOnlyDict(ret)
 
-    def __list_links(self) -> ViewOnlyDict:
+    def _list_links(self) -> ViewOnlyDict:
         link_id_list = self.graph_model.get_all_network_links()
         ret = dict()
         for nid in link_id_list:
-            n = self.__get_link_by_id(nid)
+            n = self._get_link_by_id(nid)
             ret[n.name] = n
         return ViewOnlyDict(ret)
 
@@ -764,7 +803,24 @@ def nodes(self):
 
     @property
     def links(self):
-        return self.__list_links()
+        return self._list_links()
+
+    @property
+    def facilities(self) -> ViewOnlyDict or None:
+        """
+        Return facilities connected in this topology. Facilities should NOT be composite nodes in the ad.
+        :return:
+        """
+        fac_ids = self.graph_model.get_all_nodes_by_class_and_type(label=ABCPropertyGraph.CLASS_NetworkNode,
+                                                                   ntype=str(NodeType.Facility))
+        if fac_ids is None:
+            return None
+        ret = dict()
+        for nid in fac_ids:
+            n = self._get_node_by_id(nid)
+            if n.type == NodeType.Facility:
+                ret[n.name] = n
+        return ViewOnlyDict(ret)
 
     def draw(self, *, file_name: str = None, interactive: bool = False,
              topo_detail: TopologyDetail = TopologyDetail.Derived,
@@ -790,7 +846,6 @@ def draw(self, *, file_name: str = None, interactive: bool = False,
 
             # collect all network nodes and links, draw edges between them
             network_sites = self.graph_model.get_all_composite_nodes()
-            links = self.graph_model.get_all_network_links()
 
             derived_graph = nx.Graph()
             for n in network_sites:
@@ -800,14 +855,15 @@ def draw(self, *, file_name: str = None, interactive: bool = False,
             # build two-element lists of which nodes should be connected by edges
             # indexed by link name
             graph_edges = defaultdict(list)
-            for n in self.sites.values():
+            all_node_like = list(self.sites.values())
+            all_node_like.extend(self.facilities.values())
+            for n in all_node_like:
                 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:
                             graph_edges[l.name].append(n.name)
-                            #derived_graph.add_edge(n.name, l.name)
 
             edge_labels = dict()
             for k, v in graph_edges.items():
@@ -852,32 +908,47 @@ def __str__(self):
         :return:
         """
         lines = list()
-        for n in self.sites.values():
-            tot_cap = n.capacities
-            alloc_cap = n.get_property('capacity_allocations')
-            if alloc_cap is None:
-                # if nothing is allocated, just zero out
-                alloc_cap = Capacities()
-            if tot_cap is not None:
-                ncp = CapacityTuple(total=tot_cap, allocated=alloc_cap)
-                lines.append(n.name + ": " + str(ncp))
-            else:
-                lines.append(n.name)
-            lines.append("\tComponents:")
-            for c in n.components.values():
-                ccp = CapacityTuple(total=c.capacities,
-                                    allocated=c.get_property("capacity_allocations"))
-                lines.append("\t\t" + c.name + ": " + " " + str(c.get_property("type")) + " " +
-                             c.model + " " + str(ccp))
-            lines.append("\tSite Interfaces:")
-            for i in n.interfaces.values():
-                icp = CapacityTuple(total=i.capacities,
-                                    allocated=i.get_property("capacity_allocations"))
-                lines.append("\t\t" + i.name + ": " + str(i.get_property("type")) + " " +
-                             str(icp))
-        lines.append("Links:")
-        for l in self.links.values():
-            interface_names = [iff.name for iff in l.interface_list]
-            lines.append("\t" + l.name + "[" + str(l.type) + "]: " +
-                         str(interface_names))
+        if self.sites:
+            for n in self.sites.values():
+                tot_cap = n.capacities
+                alloc_cap = n.get_property('capacity_allocations')
+                if alloc_cap is None:
+                    # if nothing is allocated, just zero out
+                    alloc_cap = Capacities()
+                if tot_cap is not None:
+                    ncp = CapacityTuple(total=tot_cap, allocated=alloc_cap)
+                    lines.append(n.name + " [Site] : " + str(ncp))
+                else:
+                    lines.append(n.name + " [Site]")
+                lines.append("\tComponents:")
+                for c in n.components.values():
+                    ccp = CapacityTuple(total=c.capacities,
+                                        allocated=c.get_property("capacity_allocations"))
+                    lines.append("\t\t" + c.name + ": " + " " + str(c.get_property("type")) + " " +
+                                 c.model + " " + str(ccp))
+                lines.append("\tSite Interfaces:")
+                for i in n.interfaces.values():
+                    if i.capacities is not None:
+                        icp = CapacityTuple(total=i.capacities,
+                                            allocated=i.get_property("capacity_allocations"))
+                        lines.append("\t\t" + i.name + ": " + str(i.get_property("type")) + " " +
+                                    str(icp))
+        if self.facilities:
+            for fp in self.facilities.values():
+                lines.append(fp.name + " [Facility]")
+                lines.append("\tFacility Interfaces:")
+                for i in fp.interfaces.values():
+                    if i.capacities is not None:
+                        icp = CapacityTuple(total=i.capacities,
+                                            allocated=i.get_property("capacity_allocations"))
+                        lines.append("\t\t" + i.name + ": " + str(i.get_property("type")) + " " +
+                                    str(icp) + " " + self.__print_caplabs__(i.labels))
+
+        if self.links:
+            lines.append("Links:")
+            for l in self.links.values():
+                interface_names = [iff.name for iff in l.interface_list]
+                lines.append("\t" + l.name + "[" + str(l.type) + "]: " +
+                             str(interface_names))
+
         return "\n".join(lines)
diff --git a/requirements.txt b/requirements.txt
index 87eaf7a..035f3ef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,11 +16,11 @@ lazy-object-proxy==1.4.3
 matplotlib==3.3.4
 mccabe==0.6.1
 neo4j==4.1.0
-networkx==2.5
+networkx==2.6.2
 networkx-query==1.0.1
 numpy==1.20.0
 packaging==20.8
-Pillow==8.2.0
+Pillow==8.3.2
 pipdeptree==1.0.0
 pkginfo==1.6.1
 pluggy==0.13.1
diff --git a/util/fim_util.py b/util/fim_util.py
index 76cf16f..02db8d9 100644
--- a/util/fim_util.py
+++ b/util/fim_util.py
@@ -75,14 +75,14 @@ def load_file(*, filename, graph_id, neo4j_config):
                                               pswd=neo4j_config["pass"],
                                               import_host_dir=neo4j_config["import_host_dir"],
                                               import_dir=neo4j_config["import_dir"])
-    print(f"Loading graph into Neo4j")
+    logging.info(f"Loading graph into Neo4j")
     _graph = neo4j_graph_importer.import_graph_from_file(graph_file=filename, graph_id=graph_id)
-    print(f"Created graph {_graph}")
+    logging.info(f"Created graph {_graph}")
     try:
         _graph.validate_graph()
         return _graph.graph_id
     except PropertyGraphImportException as pe:
-        print(f"Unable to load graph due to error {pe.msg}, deleting")
+        logging.info(f"Unable to load graph due to error {pe.msg}, deleting")
         _graph.delete_graph()
         return None
 
@@ -146,36 +146,39 @@ def merge_ads(*, neo4j_config, file_names, delete_arms, delete_adms):
                                               import_host_dir=neo4j_config["import_host_dir"],
                                               import_dir=neo4j_config["import_dir"])
     cbm = Neo4jCBMGraph(importer=neo4j_graph_importer)
-    print(f'Created blank CBM {cbm.graph_id}')
+    logging.info(f'Created blank CBM {cbm.graph_id}')
 
     arms = dict()
     for file_name in file_names:
         _graph = neo4j_graph_importer.import_graph_from_file(graph_file=file_name)
-        print(f'Loaded {file_name} as graph ID {_graph.graph_id}')
+        logging.info(f'Loaded {file_name} as graph ID {_graph.graph_id}')
         _graph.validate_graph()
+        if arms.get(file_name, None) is not None:
+            logging.error(f'Encountered model {file_name} more than once, please revise parameters.')
+            sys.exit(-1)
         arms[file_name] = Neo4jARMGraph(graph=Neo4jPropertyGraph(graph_id=_graph.graph_id,
                                                                  importer=neo4j_graph_importer))
         # generate a dict of ADMs from site graph ARM
         site_adms = arms[file_name].generate_adms()
-        print(f'ADMS for {file_name}:')
+        logging.info(f'ADMS for {file_name}:')
         for adm_id in site_adms.keys():
-            print(f'  ADM id {adm_id}: {site_adms[adm_id].graph_id}')
+            logging.info(f'  ADM id {adm_id}: {site_adms[adm_id].graph_id}')
 
         # desired ADM is under 'primary'
         site_adm = site_adms['primary']
 
-        print(f'Merging into CBM')
+        logging.info(f'Merging into CBM')
         cbm.merge_adm(adm=site_adm)
         cbm.validate_graph()
 
         if delete_arms:
-            print(f'Deleting ARM {file_name}')
+            logging.info(f'Deleting ARM {file_name}')
             arms[file_name].delete_graph()
         if delete_adms:
-            print(f'Deleting ADMs for {file_name}')
+            logging.info(f'Deleting ADMs for {file_name}')
             for adm in site_adms.values():
                 adm.delete_graph()
-    print('Completed')
+    logging.info('Completed')
 
 
 def save_graph(*, outfile, graph_id, neo4j_config):
@@ -237,80 +240,80 @@ def save_graph(*, outfile, graph_id, neo4j_config):
         with open(args.config, 'r') as config_file:
             yaml_config = yaml.safe_load(config_file)
     except IOError:
-        print(f"Unable to open config file {args.config}, exiting", file=sys.stderr)
+        logging.error(f"Unable to open config file {args.config}, exiting", file=sys.stderr)
         sys.exit(-1)
 
     if args.load:
         if args.file is None or len(args.file) == 0:
-            print("Must specify at least one -f option", file=sys.stderr)
+            logging.error("Must specify at least one -f option", file=sys.stderr)
             sys.exit(-1)
         if not args.direct and (args.graph is None or len(args.graph) != len(args.file)):
-            print("Must specify the same number of -g options as -f")
+            logging.error("Must specify the same number of -g options as -f")
             sys.exit(-1)
         # load a file with a given id (or generated id)
-        print(args.file)
+        logging.info(f'Loading {args.file}')
         try:
             if not args.direct:
                 for file, graph in zip(args.file, args.graph):
                     gid = load_file(filename=file, neo4j_config=yaml_config["neo4j"],
                                     graph_id=args.graph)
                     if gid is not None:
-                        print(f"Graph {file} successfully loaded with id {gid}")
+                        logging.info(f"Graph {file} successfully loaded with id {gid}")
                     else:
                         sys.exit(-1)
             else:
                 for file in args.file:
                     gid = load_file_direct(filename=file, neo4j_config=yaml_config["neo4j"])
                     if gid is not None:
-                        print(f"Graph {file} successfully loaded with id {gid}")
+                        logging.info(f"Graph {file} successfully loaded with id {gid}")
                     else:
                         sys.exit(-1)
         except Exception as e:
-            print(f"Unable to load file {args.file} to Neo4j due to {e.args}", file=sys.stderr)
+            logging.error(f"Unable to load file {args.file} to Neo4j due to {e.args}", file=sys.stderr)
             traceback.print_exc(file=sys.stderr)
             sys.exit(-1)
     elif args.merge:
         if args.file is None or len(args.file) == 0:
-            print('Must specify at least one -f option', file=sys.stderr)
+            logging.error('Must specify at least one -f option', file=sys.stderr)
             sys.exit(-1)
         # merge specified files into a CBM
         try:
             merge_ads(neo4j_config=yaml_config["neo4j"], file_names=args.file,
                       delete_arms=True, delete_adms=True)
         except Exception as e:
-            print(f"Unable to merge files {args.file} to Neo4j due to {e.args}", file=sys.stderr)
+            logging.error(f"Unable to merge files {args.file} to Neo4j due to {e.args}", file=sys.stderr)
             traceback.print_exc(file=sys.stderr)
             sys.exit(-1)
     elif args.wipe:
         # wipe one graph or whole database
         delete_graphs(neo4j_config=yaml_config["neo4j"], graph_id=args.graph)
         if args.graph is not None:
-            print(f"Deleted graph {args.graph} from the database")
+            logging.info(f"Deleted graph {args.graph} from the database")
         else:
-            print("Deleted all graphs from the database")
+            logging.info("Deleted all graphs from the database")
     elif args.enumerate:
         if args.file is None or len(args.file) == 0:
-            print('Must specify at least one -f option', file=sys.stderr)
+            logging.error('Must specify at least one -f option', file=sys.stderr)
             sys.exit(-1)
         if args.outfile is None or len(args.outfile) != len(args.file):
-            print('Must specify same number of -o options as -f options', file=sys.stderr)
+            logging.error('Must specify same number of -o options as -f options', file=sys.stderr)
             sys.exit(-1)
         # enumerate a file
         for infile, outfile in zip(args.file, args.outfile):
-            print(f"Enumerating nodes in {infile} and saving as {outfile}")
+            logging.info(f"Enumerating nodes in {infile} and saving as {outfile}")
             enumerate_nodes(filename=infile, new_filename=outfile, neo4j_config=yaml_config["neo4j"])
     elif args.save:
         if args.outfile is None or len(args.outfile) != 1:
-            print('Must specify one -o option', file=sys.stderr)
+            logging.error('Must specify one -o option', file=sys.stderr)
             sys.exit(-1)
         # save graph
         outfile = args.outfile[0]
         graph = args.graph
         if graph is None:
-            print("Must specify graph id (-g)", file=sys.stderr)
+            logging.error("Must specify graph id (-g)", file=sys.stderr)
             sys.exit(-1)
 
-        print(f"Saving graph {graph} into file {outfile}")
+        logging.info(f"Saving graph {graph} into file {outfile}")
         save_graph(outfile=outfile, graph_id=graph, neo4j_config=yaml_config["neo4j"])
     else:
-        print("Please specify one of -h, -l, -e or -w", file=sys.stderr)
+        logging.error("Please specify one of -h, -l, -e or -w", file=sys.stderr)


Test patch:
diff --git a/test/catalog_test.py b/test/catalog_test.py
index 6fbc398..e206367 100644
--- a/test/catalog_test.py
+++ b/test/catalog_test.py
@@ -31,7 +31,7 @@ def testCatalog(self):
 
     def testInstanceCatalog(self):
         cata = InstanceCatalog()
-        cap = Capacities().set_fields(ram=20, cpu=1, core=9, disk=110)
+        cap = Capacities(ram=20, cpu=1, core=9, disk=110)
         c = cata.map_capacities_to_instance(cap=cap)
         cap1 = cata.get_instance_capacities(instance_type=c)
         self.assertTrue(cap1.core > cap.core and cap1.ram > cap.ram and cap1.disk > cap.disk)
diff --git a/test/delegation_label_test.py b/test/delegation_label_test.py
index dd28864..6edb24d 100644
--- a/test/delegation_label_test.py
+++ b/test/delegation_label_test.py
@@ -8,9 +8,7 @@
 class DelegationTests(unittest.TestCase):
 
     def testCapacityAssignment(self):
-        c = Capacities()
-
-        c.set_fields(cpu=1, core=2)
+        c = Capacities(cpu=1, core=2)
 
         self.assertEqual(c.cpu, 1)
         self.assertEqual(c.core, 2)
@@ -24,10 +22,8 @@ def testCapacityAssignment(self):
         self.assertEqual(c1.core, 32)
 
     def testLabelAssignment(self):
-        c = Labels()
-
-        c.set_fields(mac=['0C:42:A1:BE:8F:D5', '0C:42:A1:BE:8F:E9'],
-                     bdf=['0000:41:00.0', '0000:41:00.1'])
+        c = Labels(mac=['0C:42:A1:BE:8F:D5', '0C:42:A1:BE:8F:E9'],
+                       bdf=['0000:41:00.0', '0000:41:00.1'])
 
         self.assertEqual(c.bdf, ['0000:41:00.0', '0000:41:00.1'])
         s = '{"bdf": ["0000:41:00.0", "0000:41:00.1"], ' \
@@ -47,13 +43,13 @@ def testDelegationsSerDes(self):
         d3 = Delegation(atype=DelegationType.LABEL, aformat=DelegationFormat.PoolReference,
                         delegation_id='del3', pool_id='pool1')
         ds = Delegations(atype=DelegationType.LABEL)
-        d1.set_details(Labels().set_fields(vlan_range='1-100'))
-        d2.set_details(Labels().set_fields(vlan_range='101-200'))
+        d1.set_details(Labels(vlan_range='1-100'))
+        d2.set_details(Labels(vlan_range='101-200'))
         with self.assertRaises(DelegationException) as de:
-            d2.set_details(Capacities().set_fields(unit=1))
+            d2.set_details(Capacities(unit=1))
 
         with self.assertRaises(DelegationException) as de:
-            d3.set_details(Labels().set_fields(vlan_range='something'))
+            d3.set_details(Labels(vlan_range='100-200'))
 
         ds.add_delegations(d1)
         ds.add_delegations(d2, d3)
@@ -76,10 +72,10 @@ def testDelegationsSerDes(self):
     def testPools(self):
         p1 = Pool(atype=DelegationType.LABEL, pool_id='pool1', delegation_id='del1',
                   defined_on='node1', defined_for=['node2', 'node3'])
-        p1.set_pool_details(Labels().set_fields(vlan_range='1-100'))
+        p1.set_pool_details(Labels(vlan_range='1-100'))
         p2 = Pool(atype=DelegationType.LABEL, pool_id='pool2', delegation_id='del2',
                   defined_on='node2', defined_for=['node1', 'node4', 'node5'])
-        p2.set_pool_details(Labels().set_fields(vlan_range='101-200'))
+        p2.set_pool_details(Labels(vlan_range='101-200'))
         ps = Pools(atype=DelegationType.CAPACITY)
         with self.assertRaises(PoolException) as pe:
             ps.add_pool(pool=p1)
@@ -91,7 +87,7 @@ def testPools(self):
 
         delegs = ps.generate_delegations_by_node_id()
         d1 = Delegation(atype=DelegationType.LABEL, delegation_id='del3', aformat=DelegationFormat.SinglePool)
-        d1.set_details(Labels().set_fields(ipv4='192.168.1.1-192.168.1.10'))
+        d1.set_details(Labels(ipv4_range='192.168.1.1-192.168.1.10'))
         delegs['node1'].add_delegations(d1)
 
         print(delegs)
diff --git a/test/slice_topology_test.py b/test/slice_topology_test.py
index f338f71..261d216 100644
--- a/test/slice_topology_test.py
+++ b/test/slice_topology_test.py
@@ -1,9 +1,14 @@
 import unittest
+import json
 
 import fim.user as f
 
 from fim.graph.slices.neo4j_asm import Neo4jASM, Neo4jASMFactory
 from fim.graph.neo4j_property_graph import Neo4jGraphImporter
+from fim.slivers.gateway import Gateway
+from fim.slivers.capacities_labels import Labels
+from fim.user.model_element import TopologyException
+from fim.slivers.measurement_data import MeasurementDataError
 
 
 class SliceTest(unittest.TestCase):
@@ -100,19 +105,14 @@ def testNodesAndComponents(self):
 
         self.assertTrue(len(gpu1.interfaces) == 0)
         self.assertTrue(len(nic1.interfaces) == 1)
-        cap = f.Capacities()
-        cap.set_fields(bw=50, unit=1)
-        #nic1.set_properties(capacities=cap)
-        nic1.capacities = cap
-        lab = f.Labels()
-        lab.set_fields(ipv4="192.168.1.12")
-        #nic1.set_properties(labels=lab)
-        nic1.labels = lab
-        self.assertEqual(nic1.get_property('capacities').bw, 50)
-        self.assertEqual(nic1.get_property('capacities').unit, 1)
-        self.assertEqual(nic1.get_property('capacities').disk, 0)
-        self.assertEqual(n1.components['nic1'].get_property('labels').ipv4, "192.168.1.12")
-        self.assertEqual(n1.components['nic1'].labels.ipv4, "192.168.1.12")
+        nic1.interface_list[0].update_capacities(bw=50, unit=1)
+        nic1.interface_list[0].update_labels(ipv4="192.168.1.12")
+        self.assertEqual(nic1.interface_list[0].get_property('capacities').bw, 50)
+        self.assertEqual(nic1.interface_list[0].capacities.unit, 1)
+        self.assertEqual(nic1.interface_list[0].capacities.disk, 0)
+        self.assertEqual(nic1.interface_list[0].labels.local_name, 'p1')
+        self.assertEqual(n1.components['nic1'].interface_list[0].get_property('labels').ipv4, "192.168.1.12")
+        self.assertEqual(n1.components['nic1'].interface_list[0].labels.ipv4, "192.168.1.12")
 
         # comparisons
         nic11 = n1.components['nic1']
@@ -122,7 +122,7 @@ def testNodesAndComponents(self):
         self.assertEqual(len(self.topo.interface_list), 4)
 
         # no peers yet
-        service_port = self.topo.interface_list[0].get_peer()
+        service_port = self.topo.interface_list[0].get_peers()
         self.assertEqual(service_port, None)
 
         self.topo.add_network_service(name='s1', nstype=f.ServiceType.L2Bridge, interfaces=self.topo.interface_list)
@@ -130,20 +130,13 @@ def testNodesAndComponents(self):
         assert(self.topo.network_services['s1'].get_property('site') == 'RENC')
 
         # test peer code
-        service_port = self.topo.interface_list[0].get_peer()
+        service_port = self.topo.interface_list[0].get_peers()[0]
         print(f'This is a service port {service_port}')
         self.assertEqual(service_port.get_property('type'), f.InterfaceType.ServicePort)
         # back to self
-        self_port = service_port.get_peer()
+        self_port = service_port.get_peers()[0]
         self.assertEqual(self_port, self.topo.interface_list[0])
 
-        # disabled - replaced with RuntimeError
-        #with self.assertRaises(AssertionError) as e:
-        #    self.topo.add_network_service(name='s1', nstype=f.ServiceType.L2Bridge,
-        #                                  interfaces=self.topo.interface_list[0:2])
-
-        #print(self.topo)
-
         # nodemap and unset
         n1.set_properties(node_map=('dead-beef-graph', 'dead-beef-node'))
         assert n1.get_property('node_map') == ('dead-beef-graph', 'dead-beef-node')
@@ -196,6 +189,73 @@ def testNetworkServices(self):
         n2.add_component(ctype=f.ComponentType.SmartNIC, model='ConnectX-6', name='nic2')
         n3.add_component(ctype=f.ComponentType.SharedNIC, model='ConnectX-6', name='nic3')
 
+        #tags on model elements (nodes, links, components, interfaces, network services)
+        n1.tags = f.Tags('blue', 'heavy')
+        self.assertTrue('blue' in n1.tags)
+        # unset the tags
+        n1.tags = None
+        self.assertEqual(n1.tags, None)
+
+        #boot script on nodes only
+        n1.boot_script = """
+        #!/bin/bash
+        
+        echo *
+        """
+        self.assertTrue("bash" in n1.boot_script)
+        n1.boot_script = None
+        self.assertIsNone(n1.boot_script)
+
+        # measurement data on model elements (nodes, links, components, interfaces, network services)
+        # can be set simply as json string (string length not to exceed 1M)
+        n1.mf_data = json.dumps({'k1': ['some', 'measurement', 'configuration']})
+        # you are guaranteed that whatever is on mf_data is JSON parsable and can be reconstituted into
+        # an object
+        mf_object1 = n1.mf_data
+        self.assertTrue(mf_object1['k1'] == ['some', 'measurement', 'configuration'])
+        # when nothing is set, it is None
+        self.assertEqual(n2.mf_data, None)
+
+        # you can also set it as MeasurementData object
+        my_meas_data_object = {'key1': {'key2': ['v1', 2]}}
+        n1.mf_data = f.MeasurementData(json.dumps(my_meas_data_object))
+
+        # you can also just pass a JSON serializable object to MeasurementData constructor:
+        n1.mf_data = f.MeasurementData(my_meas_data_object)
+        # or even an serializable object itself. Either way the limit of 1M on the JSON string
+        # length is enforced
+        n1.mf_data = my_meas_data_object
+
+        # for most uses, just set the object
+        my_meas_data_object = {'key1': {'key2': ['some', 'config', 'info']}}
+        n1.mf_data = my_meas_data_object
+
+        # you get back your object (in this case a dict)
+        self.assertTrue(isinstance(n1.mf_data, dict))
+        mf_object2 = n1.mf_data
+        self.assertTrue(mf_object2['key1'] == {'key2': ['some', 'config', 'info']})
+
+        class MyClass:
+            def __init__(self, val):
+                self.val = val
+
+        # this is not a valid object - json.dumps() will fail on it
+        bad_meas_data_object = {'key1': MyClass(3)}
+        with self.assertRaises(MeasurementDataError):
+            n1.mf_data = bad_meas_data_object
+
+        # also cannot use bad strings
+        with self.assertRaises(MeasurementDataError):
+            # you cannot assign non-json string to measurement data either as MeasurementData object
+            n1.mf_data = f.MeasurementData("not parsable json")
+        with self.assertRaises(MeasurementDataError):
+            # or directly as string
+            n1.mf_data = 'random string'
+
+        # most settable properties can be unset by setting them to None (there are exceptions, like e.g. name)
+        n1.mf_data = None
+        self.assertIsNone(n1.mf_data)
+
         gpu1 = n1.components['gpu1']
         nic1 = n1.components['nic1']
         nic2 = n2.components['nic2']
@@ -203,15 +263,11 @@ def testNetworkServices(self):
         p1 = nic2.interfaces['nic2-p1']
         p2 = nic2.interfaces['nic2-p2']
 
-        cap = f.Capacities()
-        cap.set_fields(bw=50, unit=1)
-        nic1.capacities=cap
-        lab = f.Labels()
-        lab.set_fields(ipv4="192.168.1.12")
+        cap = f.Capacities(bw=50, unit=1)
+        nic1.capacities = cap
+        lab = f.Labels(ipv4="192.168.1.12")
         nic1.labels = lab
-        caphints = f.CapacityHints()
-        caphints.set_fields(instance_type='blah')
-        #n1.set_properties(capacity_hints=caphints)
+        caphints = f.CapacityHints(instance_type='blah')
         n1.capacity_hints = caphints
 
         # check capacities, hints labels on the graph
@@ -223,12 +279,27 @@ def testNetworkServices(self):
 
         #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)
+        s1 = self.topo.add_network_service(name='s1', nstype=f.ServiceType.L2STS, interfaces=[n1.interface_list[0],
+                                                                                              n2.interface_list[0],
+                                                                                              n3.interface_list[0]])
+
+        # facilities
+        fac1 = self.topo.add_facility(name='RENCI-DTN', site='RENC', capacities=f.Capacities(bw=10))
+        sfac = self.topo.add_network_service(name='s-fac', nstype=f.ServiceType.L2STS,
+                                             interfaces=[fac1.interface_list[0],
+                                             n1.interface_list[1]])
 
         self.assertEqual(s1.layer, f.Layer.L2)
+        self.assertEqual(sfac.layer, f.Layer.L2)
+
+        # this is typically done by orchestrator
+        s1.gateway = Gateway(Labels(ipv4_subnet="192.168.1.0/24", ipv4="192.168.1.1", mac="00:11:22:33:44:55"))
+
+        self.assertEqual(s1.gateway.gateway, "192.168.1.1")
+        self.assertEqual(s1.gateway.subnet, "192.168.1.0/24")
 
         print(f'S1 has these interfaces: {s1.interface_list}')
-        self.assertEqual(len(s1.interface_list), 6)
+        self.assertEqual(len(s1.interface_list), 3)
         self.topo.validate()
 
         s1p = self.topo.network_services['s1']
@@ -238,25 +309,33 @@ def testNetworkServices(self):
         s1.disconnect_interface(interface=p1)
 
         print(f'S1 has these interfaces: {s1.interface_list}')
-        self.assertEqual(len(s1.interface_list), 5)
+        self.assertEqual(len(s1.interface_list), 2)
+
+        # validate the topology
         self.topo.validate()
 
         self.topo.remove_network_service('s1')
 
+        with self.assertRaises(TopologyException):
+            # connection conflict because we have an interface connecting to s-fac
+            s2 = self.topo.add_network_service(name='s2', nstype=f.ServiceType.L2PTP,
+                                               interfaces=self.topo.interface_list)
+
         s2 = self.topo.add_network_service(name='s2', nstype=f.ServiceType.L2PTP,
-                                           interfaces=self.topo.interface_list[2:4])
+                                           interfaces=[n1.components['nic4'].interfaces['nic4-p1'],
+                                                       n2.components['nic2'].interfaces['nic2-p1']])
         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), 5)
+        print(f'There are {self.topo.links}  links left in topology')
+        self.assertEqual(len(self.topo.links), 4)
+        print(f'There are {self.topo.network_services} Network services  in topology')
+        self.assertEqual(len(self.topo.network_services), 7)
         self.topo.remove_network_service('s2')
         self.topo.validate()
-        self.assertEqual(len(self.topo.network_services), 4)
+        self.assertEqual(len(self.topo.network_services), 6)
         n1.remove_component('nic1')
-        self.assertEqual(len(self.topo.network_services), 3)
+        self.assertEqual(len(self.topo.network_services), 5)
 
         #self.topo.add_link(name='l3', ltype=f.LinkType.L2Bridge, interfaces=self.topo.interface_list)
 
@@ -375,3 +454,26 @@ def testBasicTwoSiteSlice(self):
         self.topo.serialize(file_name='two-site.graphml')
         self.topo.serialize(file_name='two-site.json', fmt=f.GraphFormat.JSON_NODELINK)
         self.topo.serialize(file_name='two-site.cyt.json', fmt=f.GraphFormat.CYTOSCAPE)
+
+    def testL3Service(self):
+        self.topo.add_node(name='n1', site='RENC', ntype=f.NodeType.VM)
+        self.topo.add_node(name='n2', site='RENC')
+        self.topo.add_node(name='n3', site='UKY')
+        self.topo.nodes['n1'].add_component(model_type=f.ComponentModelType.SharedNIC_ConnectX_6, name='nic1')
+        self.topo.nodes['n2'].add_component(model_type=f.ComponentModelType.SmartNIC_ConnectX_6, name='nic1')
+        self.topo.nodes['n3'].add_component(model_type=f.ComponentModelType.SmartNIC_ConnectX_5, name='nic1')
+
+        # one L3 service per site
+        s1 = self.topo.add_network_service(name='v4UKY', nstype=f.ServiceType.FABNetv4,
+                                           interfaces=self.topo.nodes['n3'].interface_list)
+        s2 = self.topo.add_network_service(name='v4RENC', nstype=f.ServiceType.FABNetv4,
+                                           interfaces=[self.topo.nodes['n1'].interface_list[0],
+                                                  self.topo.nodes['n2'].interface_list[0]])
+        # adding interfaces belonging to nodes from diffeerent sites is a no no
+        with self.assertRaises(TopologyException):
+            self.topo.add_network_service(name='bad_service', nstype=f.ServiceType.FABNetv4,
+                                          interfaces=self.topo.interface_list)
+        # site property is set automagically
+        self.assertEqual(s1.site, 'UKY')
+        self.assertEqual(s2.site, 'RENC')
+
diff --git a/test/sliver_test.py b/test/sliver_test.py
index 0b08f2a..a7bee72 100644
--- a/test/sliver_test.py
+++ b/test/sliver_test.py
@@ -5,7 +5,8 @@
 from fim.slivers.network_link import NetworkLinkSliver
 from fim.slivers.network_service import NetworkServiceSliver, NetworkServiceInfo
 from fim.slivers.path_info import ERO, PathInfo, Path
-from fim.slivers.capacities_labels import Capacities, Labels, CapacityHints, Location
+from fim.slivers.capacities_labels import Capacities, Labels, CapacityHints, Location, LabelException
+from fim.slivers.gateway import Gateway, GatewayException
 
 
 class TestSlivers(unittest.TestCase):
@@ -63,13 +64,25 @@ def testNetworkServiceSliver(self):
 
     def testCapacitiesLabels(self):
         ns = NodeSliver()
-        cap_hint = CapacityHints().set_fields(instance_type='blah')
-        lab = Labels().set_fields(vlan_range='1-4096')
+        cap_hint = CapacityHints(instance_type='blah')
+        lab = Labels(vlan_range='1-4096')
         ns.set_properties(capacities=Capacities(unit=1, core=2), labels=lab, capacity_hints=cap_hint)
         assert(ns.get_capacity_hints().instance_type == 'blah')
         assert(ns.get_labels().vlan_range == '1-4096')
         assert(ns.get_capacities().core == 2)
 
+        with self.assertRaises(LabelException):
+            Labels(vlan_range='1-8000')
+
+        with self.assertRaises(LabelException):
+            Labels(asn='600000')
+
+        with self.assertRaises(LabelException):
+            Labels(vlan='4098')
+
+        with self.assertRaises(LabelException):
+            Labels(inner_vlan='6000')
+
     def testLocation(self):
         ns = NodeSliver()
         loc = Location(postal='100 Europa Dr., Chapel Hill, NC 27517')
@@ -81,3 +94,18 @@ def testLocation(self):
         self.assertLess(lon, -79.00)
         #print(f'{lat=} {lon=}')
 
+    def testGateway(self):
+
+        gw = Gateway(Labels(ipv4="192.168.1.1", ipv4_subnet="192.168.1.0/24", mac="00:11:22:33:44:55"))
+
+        self.assertEqual(gw.gateway, "192.168.1.1")
+        self.assertEqual(gw.subnet, "192.168.1.0/24")
+        self.assertEqual(gw.mac, "00:11:22:33:44:55")
+
+        with self.assertRaises(GatewayException):
+            Gateway(Labels(ipv4="192.168.1.1"))
+
+
+
+
+
diff --git a/test/substrate_topology_test.py b/test/substrate_topology_test.py
index 8ba2020..2e08238 100644
--- a/test/substrate_topology_test.py
+++ b/test/substrate_topology_test.py
@@ -20,14 +20,9 @@ def testRENCSiteAd(self):
         loc = f.Location(postal='100 Europa Dr., Chapel Hill, NC 27517')
         head_model = 'R7515'
         worker_model = 'R7525'
-        hn_cap = f.Capacities()
-        hn_cap.set_fields(core=32, cpu=1, unit=1, ram=128, disk=100000)
-        network_worker_cap = f.Capacities()
-        # only internal storage
-        network_worker_cap.set_fields(core=32, cpu=2, unit=1, ram=512, disk=4800)
-        gpu_worker_cap = f.Capacities()
-        # has access to SAS NAS
-        gpu_worker_cap.set_fields(core=32, cpu=2, unit=1, ram=512, disk=100000)
+        hn_cap = f.Capacities(core=32, cpu=1, unit=1, ram=128, disk=100000)
+        network_worker_cap = f.Capacities(core=32, cpu=2, unit=1, ram=512, disk=4800)
+        gpu_worker_cap = f.Capacities(core=32, cpu=2, unit=1, ram=512, disk=100000)
         #hn = self.topo.add_node(name='headnode', model=head_model, site=site,
         #                        node_id='702C4409-6635-4051-91A0-9C5A45CA28EC',
         #                        ntype=f.NodeType.Server, capacities=hn_cap)
@@ -57,96 +52,96 @@ def testRENCSiteAd(self):
         # Disks
         #
 
-        nvme_cap = f.Capacities().set_fields(unit=1, disk=1000)
+        nvme_cap = f.Capacities(unit=1, disk=1000)
         gpu_nvme1 = gpuw.add_component(name=gpuw.name + '-nvme1', model='P4510',
                                        node_id='PHLJ016004CC1P0FGN',
                                        ctype=f.ComponentType.NVME,
                                        capacities=nvme_cap,
-                                       labels=f.Labels().set_fields(bdf='0000:21:00.0'))
+                                       labels=f.Labels(bdf='0000:21:00.0'))
         gpu_nvme2 = gpuw.add_component(name=gpuw.name + '-nvme2', model='P4510',
                                        node_id='PHLJ0160047K1P0FGN',
                                        ctype=f.ComponentType.NVME,
                                        capacities=nvme_cap,
-                                       labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                       labels=f.Labels(bdf='0000:22:00.0'))
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme1', model='P4510',
                                      node_id='PHLJ015301TU1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='000:21:00.0'))
+                                     labels=f.Labels(bdf='000:21:00.0'))
         fn_nvme2 = fnw.add_component(name=fnw.name + '-nvme2', model='P4510',
                                      node_id='PHLJ016004CK1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                     labels=f.Labels(bdf='0000:22:00.0'))
         fn_nvme3 = fnw.add_component(name=fnw.name + '-nvme3', model='P4510',
                                      node_id='PHLJ016002P61P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:23:00.0'))
+                                     labels=f.Labels(bdf='0000:23:00.0'))
         fn_nvme4 = fnw.add_component(name=fnw.name + '-nvme4', model='P4510',
                                      node_id='PHLJ016004CL1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:24:00.0'))
+                                     labels=f.Labels(bdf='0000:24:00.0'))
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme1', model='P4510',
                                      node_id='PHLJ015301V81P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:21:00.0'))
+                                     labels=f.Labels(bdf='0000:21:00.0'))
         sn_nvme2 = snw.add_component(name=snw.name + '-nvme2', model='P4510',
                                      node_id='PHLJ0160047L1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                     labels=f.Labels(bdf='0000:22:00.0'))
         sn_nvme3 = snw.add_component(name=snw.name + '-nvme3', model='P4510',
                                      node_id='PHLJ016004CJ1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:23:00.0'))
+                                     labels=f.Labels(bdf='0000:23:00.0'))
         sn_nvme4 = snw.add_component(name=snw.name + '-nvme4', model='P4510',
                                      node_id='PHLJ016004C91P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:24:00.0'))
+                                     labels=f.Labels(bdf='0000:24:00.0'))
 
         #
         # GPUs (serial #s not available so made up of node serial # + uniq but
         # consistent string
         #
 
-        unit_cap = f.Capacities().set_fields(unit=1)
+        unit_cap = f.Capacities(unit=1)
         gpu_gpu1 = gpuw.add_component(name=gpuw.name + '-gpu1', model='RTX6000',
                                       node_id=gpuw.node_id + '-gpu1',
                                       ctype=f.ComponentType.GPU,
                                       capacities=unit_cap,
-                                      labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                      labels=f.Labels(bdf='0000:25:00.0'))
         gpu_gpu2 = gpuw.add_component(name=gpuw.name + '-gpu2', model='RTX6000',
                                       node_id=gpuw.node_id + '-gpu2',
                                       ctype=f.ComponentType.GPU,
                                       capacities=unit_cap,
-                                      labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                      labels=f.Labels(bdf='0000:81:00.0'))
 
         fn_gpu1 = fnw.add_component(name=fnw.name + '-gpu1', model='Tesla T4',
                                     node_id=fnw.node_id + '-gpu1',
                                     ctype=f.ComponentType.GPU,
                                     capacities=unit_cap,
-                                    labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                    labels=f.Labels(bdf='0000:25:00.0'))
         fn_gpu2 = fnw.add_component(name=fnw.name + '-gpu2', model='Tesla T4',
                                     node_id=fnw.node_id + '-gpu2',
                                     ctype=f.ComponentType.GPU,
                                     capacities=unit_cap,
-                                    labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                    labels=f.Labels(bdf='0000:81:00.0'))
 
         sn_gpu1 = snw.add_component(name=snw.name + '-gpu1', model='Tesla T4',
                                     node_id=snw.node_id + '-gpu1',
                                     ctype=f.ComponentType.GPU,
                                     capacities=unit_cap,
-                                    labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                    labels=f.Labels(bdf='0000:25:00.0'))
         sn_gpu2 = snw.add_component(name=snw.name + '-gpu2', model='Tesla T4',
                                     node_id=snw.node_id + '-gpu2',
                                     ctype=f.ComponentType.GPU,
                                     capacities=unit_cap,
-                                    labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                    labels=f.Labels(bdf='0000:81:00.0'))
 
         #
         # NICs. Interface node ids are MAC addresses of the ports,
@@ -158,17 +153,17 @@ def testRENCSiteAd(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:14:ED', '04:3F:72:B7:14:EE', '04:3F:72:B7:14:EF', '04:3F:72:B7:14:F0']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         # 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',
                                         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,
-                                                                                vlan=vf_vlans)],
-                                        capacities=f.Capacities().set_fields(unit=4),
-                                        labels=f.Labels().set_fields(bdf=vf_ids),
+                                        interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                  mac=vf_macs,
+                                                                                  vlan=vf_vlans)],
+                                        capacities=f.Capacities(unit=4),
+                                        labels=f.Labels(bdf=vf_ids),
                                         ctype=f.ComponentType.SharedNIC,
                                         details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -182,16 +177,16 @@ def testRENCSiteAd(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:18:B5', '04:3F:72:B7:18:B6', '04:3F:72:B7:18:B7', '04:3F:72:B7:18:B8']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         fnw_shnic = fnw.add_component(name=fnw.name + '-shnic', model='ConnectX-6',
                                       node_id=fnw.node_id + '-shnic',
                                       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,
-                                                                              vlan=vf_vlans)],
-                                      capacities=f.Capacities().set_fields(unit=4),
-                                      labels=f.Labels().set_fields(bdf=vf_ids),
+                                      interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                mac=vf_macs,
+                                                                                vlan=vf_vlans)],
+                                      capacities=f.Capacities(unit=4),
+                                      labels=f.Labels(bdf=vf_ids),
                                       ctype=f.ComponentType.SharedNIC,
                                       details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -200,16 +195,16 @@ def testRENCSiteAd(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:16:15', '04:3F:72:B7:16:16', '04:3F:72:B7:16:17', '04:3F:72:B7:16:18']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         snw_shnic = snw.add_component(name=snw.name + '-shnic', model='ConnectX-6',
                                       node_id=snw.node_id + '-shnic',
                                       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,
-                                                                              vlan=vf_vlans)],
-                                      capacities=f.Capacities().set_fields(unit=4),
-                                      labels=f.Labels().set_fields(bdf=vf_ids),
+                                      interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                mac=vf_macs,
+                                                                                vlan=vf_vlans)],
+                                      capacities=f.Capacities(unit=4),
+                                      labels=f.Labels(bdf=vf_ids),
                                       ctype=f.ComponentType.SharedNIC,
                                       details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -222,14 +217,14 @@ def testRENCSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='04:3F:72:B7:15:75',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='04:3F:72:B7:15:74',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='04:3F:72:B7:15:75',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:41:00.0', '0000:41:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:41:00.0', '0000:41:00.1']),
                                      details='Mellanox Technologies MT28908 Family [ConnectX-6]')
 
         fnw_shnic_lab = fnw.components[fnw.name + '-nic1'].interfaces[fnw.name + '-nic1' + '-p1'].get_property('labels')
@@ -242,14 +237,14 @@ def testRENCSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='04:3F:72:B7:19:5D',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='04:3F:72:B7:19:5C',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='04:3F:72:B7:19:5D',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:a1:00.0', '0000:a1:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:a1:00.0', '0000:a1:00.1']),
                                      details='Mellanox Technologies MT28908 Family [ConnectX-6]')
 
         snw_nic1 = snw.add_component(name=snw.name + '-nic1', model='ConnectX-5',
@@ -257,14 +252,14 @@ def testRENCSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:BE:8F:D5',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:BE:8F:D4',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:BE:8F:D5',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:41:00.0', '0000:41:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:41:00.0', '0000:41:00.1']),
                                      details='Mellanox Technologies MT27800 Family [ConnectX-5]')
 
         snw_nic2 = snw.add_component(name=snw.name + '-nic2', model='ConnectX-5',
@@ -272,21 +267,21 @@ def testRENCSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:BE:8F:E9',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:BE:8F:E8',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:BE:8F:E9',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:a1:00.0', '0000:a1:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:a1:00.0', '0000:a1:00.1']),
                                      details='Mellanox Technologies MT27800 Family [ConnectX-5]')
 
         # NAS
         nas_model = 'ME4084'
         nas = self.topo.add_node(name=site.lower() + '-nas', model=nas_model, site=site, ntype=f.NodeType.NAS,
                                  node_id='BDXTQ53',
-                                 capacities=f.Capacities().set_fields(unit=1, disk=100000))
+                                 capacities=f.Capacities(unit=1, disk=100000))
         # DP switch
         switch_model = 'NCS 55A1-36H' # new switches have  NCS-57B1-6D24-SYS
         switch_ip = "192.168.11.3"
@@ -340,14 +335,9 @@ def testUKYSiteAd(self):
         loc = f.Location(postal='301 Hilltop Ave Lexington, KY 40506')
         head_model = 'R7515'
         worker_model = 'R7525'
-        hn_cap = f.Capacities()
-        hn_cap.set_fields(core=32, cpu=1, unit=1, ram=128, disk=100000)
-        network_worker_cap = f.Capacities()
-        # only internal storage
-        network_worker_cap.set_fields(core=32, cpu=2, unit=1, ram=512, disk=4800)
-        gpu_worker_cap = f.Capacities()
-        # has access to SAS NAS
-        gpu_worker_cap.set_fields(core=32, cpu=2, unit=1, ram=512, disk=100000)
+        hn_cap = f.Capacities(core=32, cpu=1, unit=1, ram=128, disk=100000)
+        network_worker_cap = f.Capacities(core=32, cpu=2, unit=1, ram=512, disk=4800)
+        gpu_worker_cap = f.Capacities(core=32, cpu=2, unit=1, ram=512, disk=100000)
         #hn = self.topo.add_node(name='headnode', model=head_model, site=site,
         #                        node_id='702C4409-6635-4051-91A0-9C5A45CA28EC',
         #                        ntype=f.NodeType.Server, capacities=hn_cap)
@@ -376,55 +366,55 @@ def testUKYSiteAd(self):
         gpu_nvme1 = gpuw.add_component(name=gpuw.name + '-nvme1', model='P4510',
                                        node_id='PHLJ0160046A1P0FGN',
                                        ctype=f.ComponentType.NVME,
-                                       capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                       labels=f.Labels().set_fields(bdf='0000:21:00.0'))
+                                       capacities=f.Capacities(unit=1, disk=1000),
+                                       labels=f.Labels(bdf='0000:21:00.0'))
         gpu_nvme2 = gpuw.add_component(name=gpuw.name + '-nvme2', model='P4510',
                                        node_id='PHLJ016003SU1P0FGN',
                                        ctype=f.ComponentType.NVME,
-                                       capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                       labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                       capacities=f.Capacities(unit=1, disk=1000),
+                                       labels=f.Labels(bdf='0000:22:00.0'))
 
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme1', model='P4510',
                                      node_id='PHLJ0160047A1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='000:21:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='000:21:00.0'))
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme2', model='P4510',
                                      node_id='PHLJ0160041X1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:22:00.0'))
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme3', model='P4510',
                                      node_id='PHLJ016100981P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:23:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:23:00.0'))
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme4', model='P4510',
                                      node_id='PHLJ0161008L1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:24:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:24:00.0'))
 
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme1', model='P4510',
                                      node_id='PHLJ016100951P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:21:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:21:00.0'))
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme2', model='P4510',
                                      node_id='PHLJ0160046F1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:22:00.0'))
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme3', model='P4510',
                                      node_id='PHLJ0161000L1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:23:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:23:00.0'))
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme4', model='P4510',
                                      node_id='PHLJ015301VG1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:24:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:24:00.0'))
 
         #
         # GPUs (serial #s not available so made up of node serial # + uniq but
@@ -434,35 +424,35 @@ def testUKYSiteAd(self):
         gpu_gpu1 = gpuw.add_component(name=gpuw.name + '-gpu1', model='RTX6000',
                                       node_id=gpuw.node_id + '-gpu1',
                                       ctype=f.ComponentType.GPU,
-                                      capacities=f.Capacities().set_fields(unit=1),
-                                      labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                      capacities=f.Capacities(unit=1),
+                                      labels=f.Labels(bdf='0000:25:00.0'))
         gpu_gpu2 = gpuw.add_component(name=gpuw.name + '-gpu2', model='RTX6000',
                                       node_id=gpuw.node_id + '-gpu2',
                                       ctype=f.ComponentType.GPU,
-                                      capacities=f.Capacities().set_fields(unit=1),
-                                      labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                      capacities=f.Capacities(unit=1),
+                                      labels=f.Labels(bdf='0000:81:00.0'))
 
         fn_gpu1 = fnw.add_component(name=fnw.name + '-gpu1', model='Tesla T4',
                                     node_id=fnw.node_id + '-gpu1',
                                     ctype=f.ComponentType.GPU,
-                                    capacities=f.Capacities().set_fields(unit=1),
-                                    labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                    capacities=f.Capacities(unit=1),
+                                    labels=f.Labels(bdf='0000:25:00.0'))
         fn_gpu2 = fnw.add_component(name=fnw.name + '-gpu2', model='Tesla T4',
                                     node_id=fnw.node_id + '-gpu2',
                                     ctype=f.ComponentType.GPU,
-                                    capacities=f.Capacities().set_fields(unit=1),
-                                    labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                    capacities=f.Capacities(unit=1),
+                                    labels=f.Labels(bdf='0000:81:00.0'))
 
         sn_gpu1 = snw.add_component(name=snw.name + '-gpu1', model='Tesla T4',
                                     node_id=snw.node_id + '-gpu1',
                                     ctype=f.ComponentType.GPU,
-                                    capacities=f.Capacities().set_fields(unit=1),
-                                    labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                    capacities=f.Capacities(unit=1),
+                                    labels=f.Labels(bdf='0000:25:00.0'))
         sn_gpu2 = snw.add_component(name=snw.name + '-gpu2', model='Tesla T4',
                                     node_id=snw.node_id + '-gpu2',
                                     ctype=f.ComponentType.GPU,
-                                    capacities=f.Capacities().set_fields(unit=1),
-                                    labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                    capacities=f.Capacities(unit=1),
+                                    labels=f.Labels(bdf='0000:81:00.0'))
 
         #
         # NICs. Interface node ids are MAC addresses of the ports,
@@ -474,23 +464,23 @@ def testUKYSiteAd(self):
         vf_ids = ['0000:a1:00.2', '0000:a1:00.3', '0000:a1:00.4', '0000:a1:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['0C:42:A1:EA:C7:61', '0C:42:A1:EA:C7:62', '0C:42:A1:EA:C7:63', '0C:42:A1:EA:C7:64']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         # 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',
                                         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,
-                                                                                mac=vf_macs,
-                                                                                vlan=vf_vlans)],
-                                        capacities=f.Capacities().set_fields(unit=4),
-                                        labels=f.Labels().set_fields(bdf=vf_ids),
+                                        interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                  mac=vf_macs,
+                                                                                  vlan=vf_vlans)],
+                                        capacities=f.Capacities(unit=4),
+                                        labels=f.Labels(bdf=vf_ids),
                                         ctype=f.ComponentType.SharedNIC,
                                         details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
         # test that labels propagated to the port
         ilabs = gpuw_shnic.interface_list[0].get_property('labels')
-        assert(ilabs.vlan == [1001, 1002, 1003, 1004])
+        assert(ilabs.vlan == ['1001', '1002', '1003', '1004'])
         assert(ilabs.mac == ['0C:42:A1:EA:C7:61', '0C:42:A1:EA:C7:62', '0C:42:A1:EA:C7:63', '0C:42:A1:EA:C7:64'])
 
         pf_id = '0000:e2:00.0' # we don't seem to need it, but I'd collect it anyway
@@ -498,17 +488,17 @@ def testUKYSiteAd(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['0C:42:A1:EA:C7:E9', '0C:42:A1:EA:C7:EA', '0C:42:A1:EA:C7:EB', '0C:42:A1:EA:C7:EC']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         fnw_shnic = fnw.add_component(name=fnw.name + '-shnic', model='ConnectX-6',
                                       node_id=fnw.node_id + '-shnic',
                                       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,
-                                                                              mac=vf_macs,
-                                                                              vlan=vf_vlans)],
-                                      capacities=f.Capacities().set_fields(unit=4),
-                                      labels=f.Labels().set_fields(bdf=vf_ids),
+                                      interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                mac=vf_macs,
+                                                                                vlan=vf_vlans)],
+                                      capacities=f.Capacities(unit=4),
+                                      labels=f.Labels(bdf=vf_ids),
                                       ctype=f.ComponentType.SharedNIC,
                                       details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -517,17 +507,17 @@ def testUKYSiteAd(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['0C:42:A1:78:F8:1D', '0C:42:A1:78:F8:1E', '0C:42:A1:78:F8:1F', '0C:42:A1:78:F8:20']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         snw_shnic = snw.add_component(name=snw.name + '-shnic', model='ConnectX-6',
                                       node_id=snw.node_id + '-shnic',
                                       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,
-                                                                              mac=vf_macs,
-                                                                              vlan=vf_vlans)],
-                                      capacities=f.Capacities().set_fields(unit=4),
-                                      labels=f.Labels().set_fields(bdf=vf_ids),
+                                      interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                mac=vf_macs,
+                                                                                vlan=vf_vlans)],
+                                      capacities=f.Capacities(unit=4),
+                                      labels=f.Labels(bdf=vf_ids),
                                       ctype=f.ComponentType.SharedNIC,
                                       details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -540,14 +530,14 @@ def testUKYSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:EA:C7:51',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:EA:C7:50',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:EA:C7:51',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:41:00.0', '0000:41:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:41:00.0', '0000:41:00.1']),
                                      details='Mellanox Technologies MT28908 Family [ConnectX-6]')
 
         ilabs = fnw_nic1.interface_list[0].get_property('labels')
@@ -562,14 +552,14 @@ def testUKYSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:78:F8:05',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:78:F8:04',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:78:F8:05',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:a1:00.0', '0000:a1:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:a1:00.0', '0000:a1:00.1']),
                                      details='Mellanox Technologies MT28908 Family [ConnectX-6]')
 
         snw_nic1 = snw.add_component(name=snw.name + '-nic1', model='ConnectX-5',
@@ -577,14 +567,14 @@ def testUKYSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:BE:8F:F9',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:BE:8F:F8',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:BE:8F:F9',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:41:00.0', '0000:41:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:41:00.0', '0000:41:00.1']),
                                      details='Mellanox Technologies MT27800 Family [ConnectX-5]')
 
         snw_nic2 = snw.add_component(name=snw.name + '-nic2', model='ConnectX-5',
@@ -592,21 +582,21 @@ def testUKYSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:BE:8F:DD',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:BE:8F:DC',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:BE:8F:DD',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:a1:00.0', '0000:a1:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:a1:00.0', '0000:a1:00.1']),
                                      details='Mellanox Technologies MT27800 Family [ConnectX-5]')
 
         # NAS
         nas_model = 'ME4084'
         nas = self.topo.add_node(name='nas', model=nas_model, site=site, ntype=f.NodeType.NAS,
                                  node_id='3DB3R53',
-                                 capacities=f.Capacities().set_fields(unit=1, disk=100000))
+                                 capacities=f.Capacities(unit=1, disk=100000))
 
         # DP switch
         switch_model = 'NCS 55A1-36H' # new switches have  NCS-57B1-6D24-SYS
@@ -659,14 +649,9 @@ def testLBNLSiteAd(self):
         loc = f.Location(postal='1 Cyclotron Rd, Berkeley, CA 94720')
         head_model = 'R7515'
         worker_model = 'R7525'
-        hn_cap = f.Capacities()
-        hn_cap.set_fields(core=32, cpu=1, unit=1, ram=128, disk=100000)
-        network_worker_cap = f.Capacities()
-        # only internal storage
-        network_worker_cap.set_fields(core=32, cpu=2, unit=1, ram=512, disk=4800)
-        gpu_worker_cap = f.Capacities()
-        # has access to SAS NAS
-        gpu_worker_cap.set_fields(core=32, cpu=2, unit=1, ram=512, disk=100000)
+        hn_cap = f.Capacities(core=32, cpu=1, unit=1, ram=128, disk=100000)
+        network_worker_cap = f.Capacities(core=32, cpu=2, unit=1, ram=512, disk=4800)
+        gpu_worker_cap = f.Capacities(core=32, cpu=2, unit=1, ram=512, disk=100000)
         #hn = self.topo.add_node(name='headnode', model=head_model, site=site,
         #                        node_id='702C4409-6635-4051-91A0-9C5A45CA28EC',
         #                        ntype=f.NodeType.Server, capacities=hn_cap)
@@ -695,55 +680,55 @@ def testLBNLSiteAd(self):
         gpu_nvme1 = gpuw.add_component(name=gpuw.name + '-nvme1', model='P4510',
                                        node_id='PHLJ015301XE1P0FGN',
                                        ctype=f.ComponentType.NVME,
-                                       capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                       labels=f.Labels().set_fields(bdf='0000:21:00.0'))
+                                       capacities=f.Capacities(unit=1, disk=1000),
+                                       labels=f.Labels(bdf='0000:21:00.0'))
         gpu_nvme2 = gpuw.add_component(name=gpuw.name + '-nvme2', model='P4510',
                                        node_id='PHLJ015301K31P0FGN',
                                        ctype=f.ComponentType.NVME,
-                                       capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                       labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                       capacities=f.Capacities(unit=1, disk=1000),
+                                       labels=f.Labels(bdf='0000:22:00.0'))
 
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme1', model='P4510',
                                      node_id='PHLJ015301LQ1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='000:21:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='000:21:00.0'))
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme2', model='P4510',
                                      node_id='PHLJ015301LE1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:22:00.0'))
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme3', model='P4510',
                                      node_id='PHLJ015301M31P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:23:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:23:00.0'))
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme4', model='P4510',
                                      node_id='PHLJ015301LS1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:24:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:24:00.0'))
 
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme1', model='P4510',
                                      node_id='PHLJ015301KV1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:21:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:21:00.0'))
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme2', model='P4510',
                                      node_id='PHLJ015301RL1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:22:00.0'))
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme3', model='P4510',
                                      node_id='PHLJ015301L91P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:23:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:23:00.0'))
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme4', model='P4510',
                                      node_id='PHLJ015301SK1P0FGN',
                                      ctype=f.ComponentType.NVME,
-                                     capacities=f.Capacities().set_fields(unit=1, disk=1000),
-                                     labels=f.Labels().set_fields(bdf='0000:24:00.0'))
+                                     capacities=f.Capacities(unit=1, disk=1000),
+                                     labels=f.Labels(bdf='0000:24:00.0'))
 
         #
         # GPUs (serial #s not available so made up of node serial # + uniq but
@@ -753,35 +738,35 @@ def testLBNLSiteAd(self):
         gpu_gpu1 = gpuw.add_component(name=gpuw.name + '-gpu1', model='RTX6000',
                                       node_id=gpuw.node_id + '-gpu1',
                                       ctype=f.ComponentType.GPU,
-                                      capacities=f.Capacities().set_fields(unit=1),
-                                      labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                      capacities=f.Capacities(unit=1),
+                                      labels=f.Labels(bdf='0000:25:00.0'))
         gpu_gpu2 = gpuw.add_component(name=gpuw.name + '-gpu2', model='RTX6000',
                                       node_id=gpuw.node_id + '-gpu2',
                                       ctype=f.ComponentType.GPU,
-                                      capacities=f.Capacities().set_fields(unit=1),
-                                      labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                      capacities=f.Capacities(unit=1),
+                                      labels=f.Labels(bdf='0000:81:00.0'))
 
         fn_gpu1 = fnw.add_component(name=fnw.name + '-gpu1', model='Tesla T4',
                                     node_id=fnw.node_id + '-gpu1',
                                     ctype=f.ComponentType.GPU,
-                                    capacities=f.Capacities().set_fields(unit=1),
-                                    labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                    capacities=f.Capacities(unit=1),
+                                    labels=f.Labels(bdf='0000:25:00.0'))
         fn_gpu2 = fnw.add_component(name=fnw.name + '-gpu2', model='Tesla T4',
                                     node_id=fnw.node_id + '-gpu2',
                                     ctype=f.ComponentType.GPU,
-                                    capacities=f.Capacities().set_fields(unit=1),
-                                    labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                    capacities=f.Capacities(unit=1),
+                                    labels=f.Labels(bdf='0000:81:00.0'))
 
         sn_gpu1 = snw.add_component(name=snw.name + '-gpu1', model='Tesla T4',
                                     node_id=snw.node_id + '-gpu1',
                                     ctype=f.ComponentType.GPU,
-                                    capacities=f.Capacities().set_fields(unit=1),
-                                    labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                    capacities=f.Capacities(unit=1),
+                                    labels=f.Labels(bdf='0000:25:00.0'))
         sn_gpu2 = snw.add_component(name=snw.name + '-gpu2', model='Tesla T4',
                                     node_id=snw.node_id + '-gpu2',
                                     ctype=f.ComponentType.GPU,
-                                    capacities=f.Capacities().set_fields(unit=1),
-                                    labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                    capacities=f.Capacities(unit=1),
+                                    labels=f.Labels(bdf='0000:81:00.0'))
 
         #
         # NICs. Interface node ids are MAC addresses of the ports,
@@ -794,17 +779,17 @@ def testLBNLSiteAd(self):
         vf_ids = ['0000:a1:00.2', '0000:a1:00.3', '0000:a1:00.4', '0000:a1:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:19:8D', '04:3F:72:B7:19:8E', '04:3F:72:B7:19:8F', '04:3F:72:B7:19:8A']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         gpuw_shnic = gpuw.add_component(name=gpuw.name + '-shnic', model='ConnectX-6',
                                         node_id=gpuw.node_id + '-shnic',
                                         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,
-                                                                                mac=vf_macs,
-                                                                                vlan=vf_vlans)],
-                                        capacities=f.Capacities().set_fields(unit=4),
-                                        labels=f.Labels().set_fields(bdf=vf_ids),
+                                        interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                  mac=vf_macs,
+                                                                                  vlan=vf_vlans)],
+                                        capacities=f.Capacities(unit=4),
+                                        labels=f.Labels(bdf=vf_ids),
                                         ctype=f.ComponentType.SharedNIC,
                                         details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -813,17 +798,17 @@ def testLBNLSiteAd(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:15:8D', '04:3F:72:B7:15:8E', '04:3F:72:B7:15:8F', '04:3F:72:B7:15:8A']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         fnw_shnic = fnw.add_component(name=fnw.name + '-shnic', model='ConnectX-6',
                                       node_id=fnw.node_id + '-shnic',
                                       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,
-                                                                              mac=vf_macs,
-                                                                              vlan=vf_vlans)],
-                                      capacities=f.Capacities().set_fields(unit=4),
-                                      labels=f.Labels().set_fields(bdf=vf_ids),
+                                      interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                mac=vf_macs,
+                                                                                vlan=vf_vlans)],
+                                      capacities=f.Capacities(unit=4),
+                                      labels=f.Labels(bdf=vf_ids),
                                       ctype=f.ComponentType.SharedNIC,
                                       details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -832,17 +817,17 @@ def testLBNLSiteAd(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:15:7D', '04:3F:72:B7:15:7E', '04:3F:72:B7:15:7F', '04:3F:72:B7:15:7A']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         snw_shnic = snw.add_component(name=snw.name + '-shnic', model='ConnectX-6',
                                       node_id=snw.node_id + '-shnic',
                                       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,
-                                                                              mac=vf_macs,
-                                                                              vlan=vf_vlans)],
-                                      capacities=f.Capacities().set_fields(unit=4),
-                                      labels=f.Labels().set_fields(bdf=vf_ids),
+                                      interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                mac=vf_macs,
+                                                                                vlan=vf_vlans)],
+                                      capacities=f.Capacities(unit=4),
+                                      labels=f.Labels(bdf=vf_ids),
                                       ctype=f.ComponentType.SharedNIC,
                                       details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -855,14 +840,14 @@ def testLBNLSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='04:3F:72:B7:15:6D',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='04:3F:72:B7:15:6C',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='04:3F:72:B7:15:6D',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:41:00.0', '0000:41:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:41:00.0', '0000:41:00.1']),
                                      details='Mellanox Technologies MT28908 Family [ConnectX-6]')
 
         fnw_nic2 = fnw.add_component(name=fnw.name + '-nic2', model='ConnectX-6',
@@ -870,14 +855,14 @@ def testLBNLSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='04:3F:72:B7:18:AD',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='04:3F:72:B7:18:AC',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='04:3F:72:B7:18:AD',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:a1:00.0', '0000:a1:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:a1:00.0', '0000:a1:00.1']),
                                      details='Mellanox Technologies MT28908 Family [ConnectX-6]')
 
         snw_nic1 = snw.add_component(name=snw.name + '-nic1', model='ConnectX-5',
@@ -885,14 +870,14 @@ def testLBNLSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:91:75:13',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:91:75:12',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:91:75:13',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:41:00.0', '0000:41:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:41:00.0', '0000:41:00.1']),
                                      details='Mellanox Technologies MT27800 Family [ConnectX-5]')
 
         snw_nic2 = snw.add_component(name=snw.name + '-nic2', model='ConnectX-5',
@@ -900,21 +885,21 @@ def testLBNLSiteAd(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:91:75:0F',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:91:75:0E',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:91:75:0F',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:a1:00.0', '0000:a1:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:a1:00.0', '0000:a1:00.1']),
                                      details='Mellanox Technologies MT27800 Family [ConnectX-5]')
 
         # NAS
         nas_model = 'ME4084'
         nas = self.topo.add_node(name='nas', model=nas_model, site=site, ntype=f.NodeType.NAS,
                                  node_id='3DB2R53',
-                                 capacities=f.Capacities().set_fields(unit=1, disk=100000))
+                                 capacities=f.Capacities(unit=1, disk=100000))
         # DP switch
         switch_model = 'NCS 55A1-36H' # new switches have  NCS-57B1-6D24-SYS
         switch_ip = "192.168.13.3"
@@ -970,10 +955,18 @@ def testNetworkAd(self):
         switch_ip = "192.168.11.3"
         switch = self.topo.add_node(name=dp_switch_name_id(site, switch_ip)[0], model=switch_model,
                                     node_id=dp_switch_name_id(site, switch_ip)[1], site=site,
-                                    ntype=f.NodeType.Switch, stitch_node=True)
+                                    ntype=f.NodeType.Switch)
+        # stitch node is true here because MPLS service shares definition with site
         dp_ns = switch.add_network_service(name=switch.name+'-ns',
                                            node_id=switch.node_id + '-ns',
-                                           nstype=f.ServiceType.MPLS, stitch_node=True)
+                                           nstype=f.ServiceType.MPLS,
+                                           labels=f.Labels(vlan_range='1-100'))
+        # stitch node is false here because L3 service is not shared with site definition of switch
+        dp_l3ns = switch.add_network_service(name=switch.name + '-l3ns',
+                                             node_id=switch.node_id + '-l3ns',
+                                             nstype=f.ServiceType.FABNetv4,
+                                             labels=f.Labels(ipv4_range='192.168.1.1-192.168.1.255',
+                                                             vlan_range='100-200'))
 
         # add ports
         port_caps = f.Capacities(bw=100)
@@ -990,6 +983,11 @@ def testNetworkAd(self):
                                      node_id=dp_port_id(switch.name, dp),
                                      labels=port_labs, capacities=port_caps)
 
+        facility_port_facing_port = dp_ns.add_interface(name='HundredDigE0/0/0/99', itype=f.InterfaceType.TrunkPort,
+                                                        node_id=dp_port_id(switch.name, 'HundredDigE0/0/0/99'),
+                                                        labels=port_labs, capacities=port_caps)
+
+
         for dp in dp_ports_25g:
             sp = dp_ns.add_interface(name=dp, itype=f.InterfaceType.TrunkPort,
                                      node_id=dp_port_id(switch.name, dp),
@@ -1010,10 +1008,16 @@ def testNetworkAd(self):
         switch_ip = "192.168.12.3"
         switch = self.topo.add_node(name=dp_switch_name_id(site, switch_ip)[0], model=switch_model,
                                     node_id=dp_switch_name_id(site, switch_ip)[1], site=site,
-                                    ntype=f.NodeType.Switch, stitch_node=True)
+                                    ntype=f.NodeType.Switch)
         dp_ns = switch.add_network_service(name=switch.name+'-ns',
                                            node_id=switch.node_id + '-ns',
-                                           nstype=f.ServiceType.MPLS, stitch_node=True)
+                                           nstype=f.ServiceType.MPLS,
+                                           labels=f.Labels(vlan_range='1-100'))
+        dp_l3ns = switch.add_network_service(name=switch.name + '-l3ns',
+                                             node_id=switch.node_id + '-l3ns',
+                                             nstype=f.ServiceType.FABNetv4,
+                                             labels=f.Labels(ipv4_range='192.168.2.1-192.168.2.255',
+                                                             vlan_range='100-200'))
 
         # add ports
         port_caps = f.Capacities(bw=100)
@@ -1051,10 +1055,16 @@ def testNetworkAd(self):
         switch_ip = "192.168.13.3"
         switch = self.topo.add_node(name=dp_switch_name_id(site, switch_ip)[0], model=switch_model,
                                     node_id=dp_switch_name_id(site, switch_ip)[1], site=site,
-                                    ntype=f.NodeType.Switch, stitch_node=True)
+                                    ntype=f.NodeType.Switch)
         dp_ns = switch.add_network_service(name=switch.name+'-ns',
                                            node_id=switch.node_id + '-ns',
-                                           nstype=f.ServiceType.MPLS, stitch_node=True)
+                                           nstype=f.ServiceType.MPLS,
+                                           labels=f.Labels(vlan_range='1-100'))
+        dp_l3ns = switch.add_network_service(name=switch.name + '-l3ns',
+                                             node_id=switch.node_id + '-l3ns',
+                                             nstype=f.ServiceType.FABNetv4,
+                                             labels=f.Labels(ipv4_range='192.168.2.1-192.168.2.255',
+                                                             vlan_range='100-200'))
 
         # add ports
         port_caps = f.Capacities(bw=100)
@@ -1079,14 +1089,14 @@ def testNetworkAd(self):
         # FIXME: fake ports towards UKY and RENC
         lbnl_renc = dp_ns.add_interface(name='HundredGigE0/0/0/26', itype=f.InterfaceType.TrunkPort,
                                         node_id=dp_port_id(switch.name, 'HundredGigE0/0/0/26'),
-                                        labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/26',
-                                                                     mac='20:00:00:00:00:10'),
+                                        labels=f.Labels(local_name='HundredGigE 0/0/0/26',
+                                                                       mac='20:00:00:00:00:10'),
                                         capacities=port_caps)
 
         lbnl_uky = dp_ns.add_interface(name='HundredGigE 0/0/0/27', itype=f.InterfaceType.TrunkPort,
                                        node_id=dp_port_id(switch.name, 'HundredGigE0/0/0/27'),
-                                       labels=f.Labels().set_fields(local_name='HundredGigE 0/0/0/27',
-                                                                    mac='20:00:00:00:00:11'),
+                                       labels=f.Labels(local_name='HundredGigE 0/0/0/27',
+                                                                      mac='20:00:00:00:00:11'),
                                        capacities=port_caps)
 
         # add 3 links
@@ -1102,7 +1112,26 @@ def testNetworkAd(self):
                                 interfaces=[renc_lbnl, lbnl_renc],
                                 node_id=renc_lbnl.node_id + '-Wave')
 
+        # add two separate facility nodes (and associated network services and interfaces)
+        fac1 = self.topo.add_facility(name='RENCI-DTN', node_id='RENCI-DTN-id', site='RENC',
+                                      capacities=f.Capacities(mtu=1500, bw=10))
+        fac2 = self.topo.add_facility(name='RENCI-BEN', node_id='RENCI-BEN-id', site='RENC',
+                                      labels=f.Labels(ipv4_range='192.168.1.1-192.168.1.10',
+                                                      vlan_range='1-100'),
+                                      capacities=f.Capacities(mtu=9000))
+
+        # connect them to links along with the port facing the facility
+        fac1_port_link = self.topo.add_link(name='RENCI-DC-link1', node_id='RENCI-DC-link1-id',
+                                            ltype=f.LinkType.L2Path,
+                                            interfaces=[fac1.interface_list[0], # only one interface available
+                                                        facility_port_facing_port])
+        fac2_port_link = self.topo.add_link(name='RENCI-DC-link2', node_id='RENCI-DC-link2-id',
+                                            ltype=f.LinkType.L2Path,
+                                            interfaces=[fac2.interface_list[0], # only one interface available
+                                                        facility_port_facing_port])
+
         delegation1 = 'primary'
+        print('RUNNING NETWORK AD DELEGATIONS')
         self.topo.single_delegation(delegation_id=delegation1,
                                     label_pools=f.Pools(atype=f.DelegationType.LABEL),
                                     capacity_pools=f.Pools(atype=f.DelegationType.CAPACITY))
@@ -1118,14 +1147,9 @@ def testRENCI_non_save(self):
         site = 'RENC'
         head_model = 'R7515'
         worker_model = 'R7525'
-        hn_cap = f.Capacities()
-        hn_cap.set_fields(core=32, cpu=1, unit=1, ram=128, disk=100000)
-        network_worker_cap = f.Capacities()
-        # only internal storage
-        network_worker_cap.set_fields(core=32, cpu=2, unit=1, ram=512, disk=4800)
-        gpu_worker_cap = f.Capacities()
-        # has access to SAS NAS
-        gpu_worker_cap.set_fields(core=32, cpu=2, unit=1, ram=512, disk=100000)
+        hn_cap = f.Capacities(core=32, cpu=1, unit=1, ram=128, disk=100000)
+        network_worker_cap = f.Capacities(core=32, cpu=2, unit=1, ram=512, disk=4800)
+        gpu_worker_cap = f.Capacities(core=32, cpu=2, unit=1, ram=512, disk=100000)
         #hn = self.topo.add_node(name='headnode', model=head_model, site=site,
         #                        node_id='702C4409-6635-4051-91A0-9C5A45CA28EC',
         #                        ntype=f.NodeType.Server, capacities=hn_cap)
@@ -1155,96 +1179,96 @@ def testRENCI_non_save(self):
         # Disks
         #
 
-        nvme_cap = f.Capacities().set_fields(unit=1, disk=1000)
+        nvme_cap = f.Capacities(unit=1, disk=1000)
         gpu_nvme1 = gpuw.add_component(name=gpuw.name + '-nvme1', model='P4510',
                                        node_id='PHLJ016004CC1P0FGN',
                                        ctype=f.ComponentType.NVME,
                                        capacities=nvme_cap,
-                                       labels=f.Labels().set_fields(bdf='0000:21:00.0'))
+                                       labels=f.Labels(bdf='0000:21:00.0'))
         gpu_nvme2 = gpuw.add_component(name=gpuw.name + '-nvme2', model='P4510',
                                        node_id='PHLJ0160047K1P0FGN',
                                        ctype=f.ComponentType.NVME,
                                        capacities=nvme_cap,
-                                       labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                       labels=f.Labels(bdf='0000:22:00.0'))
         fn_nvme1 = fnw.add_component(name=fnw.name + '-nvme1', model='P4510',
                                      node_id='PHLJ015301TU1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='000:21:00.0'))
+                                     labels=f.Labels(bdf='000:21:00.0'))
         fn_nvme2 = fnw.add_component(name=fnw.name + '-nvme2', model='P4510',
                                      node_id='PHLJ016004CK1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                     labels=f.Labels(bdf='0000:22:00.0'))
         fn_nvme3 = fnw.add_component(name=fnw.name + '-nvme3', model='P4510',
                                      node_id='PHLJ016002P61P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:23:00.0'))
+                                     labels=f.Labels(bdf='0000:23:00.0'))
         fn_nvme4 = fnw.add_component(name=fnw.name + '-nvme4', model='P4510',
                                      node_id='PHLJ016004CL1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:24:00.0'))
+                                     labels=f.Labels(bdf='0000:24:00.0'))
         sn_nvme1 = snw.add_component(name=snw.name + '-nvme1', model='P4510',
                                      node_id='PHLJ015301V81P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:21:00.0'))
+                                     labels=f.Labels(bdf='0000:21:00.0'))
         sn_nvme2 = snw.add_component(name=snw.name + '-nvme2', model='P4510',
                                      node_id='PHLJ0160047L1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:22:00.0'))
+                                     labels=f.Labels(bdf='0000:22:00.0'))
         sn_nvme3 = snw.add_component(name=snw.name + '-nvme3', model='P4510',
                                      node_id='PHLJ016004CJ1P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:23:00.0'))
+                                     labels=f.Labels(bdf='0000:23:00.0'))
         sn_nvme4 = snw.add_component(name=snw.name + '-nvme4', model='P4510',
                                      node_id='PHLJ016004C91P0FGN',
                                      ctype=f.ComponentType.NVME,
                                      capacities=nvme_cap,
-                                     labels=f.Labels().set_fields(bdf='0000:24:00.0'))
+                                     labels=f.Labels(bdf='0000:24:00.0'))
 
         #
         # GPUs (serial #s not available so made up of node serial # + uniq but
         # consistent string
         #
 
-        unit_cap = f.Capacities().set_fields(unit=1)
+        unit_cap = f.Capacities(unit=1)
         gpu_gpu1 = gpuw.add_component(name=gpuw.name + '-gpu1', model='RTX6000',
                                       node_id=gpuw.node_id + '-gpu1',
                                       ctype=f.ComponentType.GPU,
                                       capacities=unit_cap,
-                                      labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                      labels=f.Labels(bdf='0000:25:00.0'))
         gpu_gpu2 = gpuw.add_component(name=gpuw.name + '-gpu2', model='RTX6000',
                                       node_id=gpuw.node_id + '-gpu2',
                                       ctype=f.ComponentType.GPU,
                                       capacities=unit_cap,
-                                      labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                      labels=f.Labels(bdf='0000:81:00.0'))
 
         fn_gpu1 = fnw.add_component(name=fnw.name + '-gpu1', model='Tesla T4',
                                     node_id=fnw.node_id + '-gpu1',
                                     ctype=f.ComponentType.GPU,
                                     capacities=unit_cap,
-                                    labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                    labels=f.Labels(bdf='0000:25:00.0'))
         fn_gpu2 = fnw.add_component(name=fnw.name + '-gpu2', model='Tesla T4',
                                     node_id=fnw.node_id + '-gpu2',
                                     ctype=f.ComponentType.GPU,
                                     capacities=unit_cap,
-                                    labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                    labels=f.Labels(bdf='0000:81:00.0'))
 
         sn_gpu1 = snw.add_component(name=snw.name + '-gpu1', model='Tesla T4',
                                     node_id=snw.node_id + '-gpu1',
                                     ctype=f.ComponentType.GPU,
                                     capacities=unit_cap,
-                                    labels=f.Labels().set_fields(bdf='0000:25:00.0'))
+                                    labels=f.Labels(bdf='0000:25:00.0'))
         sn_gpu2 = snw.add_component(name=snw.name + '-gpu2', model='Tesla T4',
                                     node_id=snw.node_id + '-gpu2',
                                     ctype=f.ComponentType.GPU,
                                     capacities=unit_cap,
-                                    labels=f.Labels().set_fields(bdf='0000:81:00.0'))
+                                    labels=f.Labels(bdf='0000:81:00.0'))
 
         #
         # NICs. Interface node ids are MAC addresses of the ports,
@@ -1256,17 +1280,17 @@ def testRENCI_non_save(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:14:ED', '04:3F:72:B7:14:EE', '04:3F:72:B7:14:EF', '04:3F:72:B7:14:F0']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         # 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',
                                         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,
-                                                                                vlan=vf_vlans)],
-                                        capacities=f.Capacities().set_fields(unit=4),
-                                        labels=f.Labels().set_fields(bdf=vf_ids),
+                                        interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                  mac=vf_macs,
+                                                                                  vlan=vf_vlans)],
+                                        capacities=f.Capacities(unit=4),
+                                        labels=f.Labels(bdf=vf_ids),
                                         ctype=f.ComponentType.SharedNIC,
                                         details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -1280,16 +1304,16 @@ def testRENCI_non_save(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:18:B5', '04:3F:72:B7:18:B6', '04:3F:72:B7:18:B7', '04:3F:72:B7:18:B8']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         fnw_shnic = fnw.add_component(name=fnw.name + '-shnic', model='ConnectX-6',
                                       node_id=fnw.node_id + '-shnic',
                                       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,
-                                                                              vlan=vf_vlans)],
-                                      capacities=f.Capacities().set_fields(unit=4),
-                                      labels=f.Labels().set_fields(bdf=vf_ids),
+                                      interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                mac=vf_macs,
+                                                                                vlan=vf_vlans)],
+                                      capacities=f.Capacities(unit=4),
+                                      labels=f.Labels(bdf=vf_ids),
                                       ctype=f.ComponentType.SharedNIC,
                                       details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -1298,16 +1322,16 @@ def testRENCI_non_save(self):
         vf_ids = ['0000:e2:00.2', '0000:e2:00.3', '0000:e2:00.4', '0000:e2:00.5']
         # i don't know how VF macs are allocated, this is made-up /ib
         vf_macs = ['04:3F:72:B7:16:15', '04:3F:72:B7:16:16', '04:3F:72:B7:16:17', '04:3F:72:B7:16:18']
-        vf_vlans = [1001, 1002, 1003, 1004]
+        vf_vlans = ['1001', '1002', '1003', '1004']
         snw_shnic = snw.add_component(name=snw.name + '-shnic', model='ConnectX-6',
                                       node_id=snw.node_id + '-shnic',
                                       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,
-                                                                              vlan=vf_vlans)],
-                                      capacities=f.Capacities().set_fields(unit=4),
-                                      labels=f.Labels().set_fields(bdf=vf_ids),
+                                      interface_labels=[f.Labels(bdf=vf_ids,
+                                                                                mac=vf_macs,
+                                                                                vlan=vf_vlans)],
+                                      capacities=f.Capacities(unit=4),
+                                      labels=f.Labels(bdf=vf_ids),
                                       ctype=f.ComponentType.SharedNIC,
                                       details='Shared NIC: Mellanox Technologies MT28908 Family [ConnectX-6]')
 
@@ -1320,14 +1344,14 @@ def testRENCI_non_save(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='04:3F:72:B7:15:75',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='04:3F:72:B7:15:74',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='04:3F:72:B7:15:75',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:41:00.0', '0000:41:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:41:00.0', '0000:41:00.1']),
                                      details='Mellanox Technologies MT28908 Family [ConnectX-6]')
 
         fnw_shnic_lab = fnw.components[fnw.name + '-nic1'].interfaces[fnw.name + '-nic1' + '-p1'].get_property('labels')
@@ -1340,14 +1364,14 @@ def testRENCI_non_save(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='04:3F:72:B7:19:5D',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='04:3F:72:B7:19:5C',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='04:3F:72:B7:19:5D',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:a1:00.0', '0000:a1:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:a1:00.0', '0000:a1:00.1']),
                                      details='Mellanox Technologies MT28908 Family [ConnectX-6]')
 
         snw_nic1 = snw.add_component(name=snw.name + '-nic1', model='ConnectX-5',
@@ -1355,14 +1379,14 @@ def testRENCI_non_save(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:BE:8F:D5',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:BE:8F:D4',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:BE:8F:D5',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:41:00.0', '0000:41:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:41:00.0', '0000:41:00.1']),
                                      details='Mellanox Technologies MT27800 Family [ConnectX-5]')
 
         snw_nic2 = snw.add_component(name=snw.name + '-nic2', model='ConnectX-5',
@@ -1370,21 +1394,21 @@ def testRENCI_non_save(self):
                                      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',
-                                                                             vlan_range='1-4096'),
-                                                       f.Labels().set_fields(mac='0C:42:A1:BE:8F:E9',
-                                                                             vlan_range='1-4096')
+                                     interface_labels=[f.Labels(mac='0C:42:A1:BE:8F:E8',
+                                                                               vlan_range='1-4096'),
+                                                       f.Labels(mac='0C:42:A1:BE:8F:E9',
+                                                                               vlan_range='1-4096')
                                                        ],
                                      ctype=f.ComponentType.SmartNIC,
-                                     capacities=f.Capacities().set_fields(unit=1),
-                                     labels=f.Labels().set_fields(bdf=['0000:a1:00.0', '0000:a1:00.1']),
+                                     capacities=f.Capacities(unit=1),
+                                     labels=f.Labels(bdf=['0000:a1:00.0', '0000:a1:00.1']),
                                      details='Mellanox Technologies MT27800 Family [ConnectX-5]')
 
         # NAS
         nas_model = 'ME4084'
         nas = self.topo.add_node(name=site.lower() + '-nas', model=nas_model, site=site, ntype=f.NodeType.NAS,
                                  node_id='BDXTQ53',
-                                 capacities=f.Capacities().set_fields(unit=1, disk=100000))
+                                 capacities=f.Capacities(unit=1, disk=100000))
         # DP switch
         switch_model = 'NCS 55A1-36H' # new switches have  NCS-57B1-6D24-SYS
         switch_ip = "192.168.11.3"
@@ -1429,8 +1453,8 @@ def testRENCI_non_save(self):
                              defined_for=[switch.interfaces['HundredGigE0/0/0/5'].node_id,
                                           switch.interfaces['HundredGigE0/0/0/13'].node_id,
                                           switch.interfaces['HundredGigE0/0/0/21'].node_id])
-        shared_pool.set_pool_details(f.Labels().set_fields(vlan_range='100-200',
-                                                           ipv4_range='192.168.1.1-192.168.10.255'))
+        shared_pool.set_pool_details(f.Labels(vlan_range='100-200',
+                                                             ipv4_range='192.168.1.1-192.168.10.255'))
         pools.add_pool(pool=shared_pool)
 
         datanic_pool = f.Pool(atype=f.DelegationType.LABEL, pool_id='datanic_pool', delegation_id=delegation1,
@@ -1445,7 +1469,7 @@ def testRENCI_non_save(self):
                                            switch.interfaces['HundredGigE0/0/0/25.4'].node_id
                                            ]
                               )
-        datanic_pool.set_pool_details(f.Labels().set_fields(vlan_range='1500-2000'))
+        datanic_pool.set_pool_details(f.Labels(vlan_range='1500-2000'))
         pools.add_pool(pool=datanic_pool)
         # have to reindex pools by delegation
         pools.build_index_by_delegation_id()
diff --git a/test/test_load.py b/test/test_load.py
new file mode 100644
index 0000000..5659fcd
--- /dev/null
+++ b/test/test_load.py
@@ -0,0 +1,24 @@
+import unittest
+
+import fim.user as fu
+
+
+def load(file_name: str):
+
+    t = fu.ExperimentTopology()
+    t.load(file_name=file_name)
+    return t
+
+
+class LoadTests(unittest.TestCase):
+
+    def testNetworkXLoad(self):
+        """
+        Test for a stupid case where loading silently failed on duplicate graph IDs
+        """
+        print('')
+        t1 = load(file_name="test/before.graphml")
+        print(f"{t1.nodes['n2'].management_ip=}")
+        t2 = load(file_name="test/after.graphml")
+        print(f"{t2.nodes['n2'].management_ip=}")
+        self.assertEqual(str(t2.nodes['n2'].management_ip), '128.163.179.51', "No management IP address found")
\ No newline at end of file
diff --git a/test/tuple_test.py b/test/tuple_test.py
index ab86bc7..f156488 100644
--- a/test/tuple_test.py
+++ b/test/tuple_test.py
@@ -53,7 +53,7 @@ def testPools(self):
         p.set_defined_on(node_id="node1")
         p.set_defined_for(node_id_list=["node4"])
         p.add_defined_for(node_ids=["node2", "node3", "node1"])
-        p.set_pool_details(caporlab=Capacities().set_fields(cpu=2, ram=1024))
+        p.set_pool_details(caporlab=Capacities(cpu=2, ram=1024))
 
         pp = Pools(atype=DelegationType.LABEL)
         pp.add_pool(pool=p)
diff --git a/test/zz_neo4j_pg_test.py b/test/zz_neo4j_pg_test.py
index f4454e1..765999c 100644
--- a/test/zz_neo4j_pg_test.py
+++ b/test/zz_neo4j_pg_test.py
@@ -92,8 +92,7 @@ def test_asm_transfer(self):
         """
         t = fu.ExperimentTopology()
         n1 = t.add_node(name='n1', site='RENC')
-        cap = fu.Capacities()
-        cap.set_fields(core=4, ram=64, disk=500)
+        cap = fu.Capacities(core=4, ram=64, disk=500)
         n1.set_properties(capacities=cap, image_type='qcow2', image_ref='default_centos_8')
         n1.add_component(ctype=fu.ComponentType.SmartNIC, model='ConnectX-6', name='nic1')
         n2 = t.add_node(name='n2', site='RENC')
@@ -127,12 +126,12 @@ def test_asm_transfer(self):
 
         # set allocated capacities or labels
         # in orchestrator
-        alloc_labels = fu.Labels().set_fields(instance_parent="worker_node-1")
+        alloc_labels = fu.Labels(instance_parent="worker_node-1")
         neo4j_topo.nodes['n1'].set_properties(label_allocations=alloc_labels)
         # in AM
-        provisioned_labels = fu.Labels().set_fields(instance="open-stack-instance-id-123")
+        provisioned_labels = fu.Labels(instance="open-stack-instance-id-123")
         neo4j_topo.nodes['n1'].set_properties(labels=provisioned_labels)
-        ri = fu.ReservationInfo().set_fields(reservation_id="01234", reservation_state='READY')
+        ri = fu.ReservationInfo(reservation_id="01234", reservation_state='READY')
         neo4j_topo.nodes['n1'].set_properties(reservation_info=ri)
 
         self.n4j_imp.delete_all_graphs()