SEBA-108 acquire_service_instance and validate_links methods
Change-Id: Ie63de0fd7beb0aa50d4f482f7c1c9000ebb2cb34
diff --git a/xos/synchronizer/model_policies/model_policy_fabriccrossconnectserviceinstance.py b/xos/synchronizer/model_policies/model_policy_fabriccrossconnectserviceinstance.py
index db439a3..5a61506 100644
--- a/xos/synchronizer/model_policies/model_policy_fabriccrossconnectserviceinstance.py
+++ b/xos/synchronizer/model_policies/model_policy_fabriccrossconnectserviceinstance.py
@@ -39,48 +39,6 @@
service_instance.delete()
return
- # If there is a westbound link, then make sure the SerivceInstance is consistent with the
- # westbound fields.
-
- if service_instance.provided_links.exists():
- updated_fields = []
-
- si = ServiceInstance.objects.get(id=service_instance.id)
- s_tag = si.get_westbound_service_instance_properties("s_tag")
- switch_datapath_id = si.get_westbound_service_instance_properties("switch_datapath_id")
- source_port = si.get_westbound_service_instance_properties("switch_port")
-
- if (s_tag is None):
- raise Exception("Westbound ServiceInstance s-tag is None on fcsi %s" % service_instance.id)
-
- if (not switch_datapath_id):
- raise Exception("Westbound ServiceInstance switch_datapath_id is unset on fcsi %s" % service_instance.id)
-
- if (source_port is None):
- raise Exception("Westbound ServiceInstance switch_port is None on fcsi %s" % service_instance.id)
-
- s_tag = int(s_tag)
- source_port = int(source_port)
-
- if (s_tag != service_instance.s_tag):
- if service_instance.s_tag is not None:
- raise Exception("Westbound ServiceInstance changing s-tag is not currently permitted")
- service_instance.s_tag = s_tag
- updated_fields.append("s_tag")
- if (switch_datapath_id != service_instance.switch_datapath_id):
- if service_instance.switch_datapath_id:
- raise Exception("Westbound ServiceInstance changing switch_datapath_id is not currently permitted")
- service_instance.switch_datapath_id = switch_datapath_id
- updated_fields.append("switch_datapath_id")
- if (source_port != service_instance.source_port):
- if service_instance.source_port is not None:
- raise Exception("Westbound ServiceInstance changing source_port is not currently permitted")
- service_instance.source_port = source_port
- updated_fields.append("source_port")
-
- if updated_fields:
- service_instance.save(update_fields = updated_fields)
-
def handle_delete(self, service_instance):
log.info("Handle_delete Fabric-Crossconnect Service Instance", service_instance=service_instance)
diff --git a/xos/synchronizer/model_policies/test_model_policy_fabriccrossconnectserviceinstance.py b/xos/synchronizer/model_policies/test_model_policy_fabriccrossconnectserviceinstance.py
index 0099881..bccdfdc 100644
--- a/xos/synchronizer/model_policies/test_model_policy_fabriccrossconnectserviceinstance.py
+++ b/xos/synchronizer/model_policies/test_model_policy_fabriccrossconnectserviceinstance.py
@@ -123,130 +123,6 @@
self.policy_step().handle_update(fsi)
- self.assertEqual(fsi.s_tag, 111)
- self.assertEqual(fsi.switch_datapath_id, "of:0000000000000201")
- self.assertEqual(fsi.source_port, 3)
-
- fcsi_save.assert_called_with(update_fields=["s_tag", "switch_datapath_id", "source_port"])
-
- def test_handle_update_west_si_bad_s_tag(self):
- with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
- patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
-
- fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=None, source_port=None,
- switch_datapath_id=None)
-
- serviceinstance_objects.return_value = [fsi]
-
- si = self.mock_westbound(fsi, s_tag=None, switch_datapath_id = "of:0000000000000201", switch_port = 3)
- serviceinstance_objects.return_value = [si]
-
- with self.assertRaises(Exception) as e:
- self.policy_step().handle_update(fsi)
-
- self.assertEqual(e.exception.message, "Westbound ServiceInstance s-tag is None on fcsi 7777")
-
- def test_handle_update_west_si_bad_port(self):
- with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
- patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
-
- fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=None, source_port=None,
- switch_datapath_id=None)
-
- serviceinstance_objects.return_value = [fsi]
-
- si = self.mock_westbound(fsi, s_tag=111, switch_datapath_id = "of:0000000000000201", switch_port = None)
- serviceinstance_objects.return_value = [si]
-
- with self.assertRaises(Exception) as e:
- self.policy_step().handle_update(fsi)
-
- self.assertEqual(e.exception.message, "Westbound ServiceInstance switch_port is None on fcsi 7777")
-
- def test_handle_update_west_si_bad_switch_datapath_id(self):
- with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
- patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
-
- fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=None, source_port=None,
- switch_datapath_id=None)
-
- serviceinstance_objects.return_value = [fsi]
-
- si = self.mock_westbound(fsi, s_tag=111, switch_datapath_id = None, switch_port = 3)
- serviceinstance_objects.return_value = [si]
-
- with self.assertRaises(Exception) as e:
- self.policy_step().handle_update(fsi)
-
- self.assertEqual(e.exception.message, "Westbound ServiceInstance switch_datapath_id is unset on fcsi 7777")
-
- def test_handle_update_no_links(self):
- with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
- patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
-
- fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=None, source_port=None,
- switch_datapath_id=None)
-
- fsi.provided_links = Mock(exists=Mock(return_value=False))
-
- serviceinstance_objects.return_value = [fsi]
-
- self.policy_step().handle_update(fsi)
-
- fcsi_save.assert_not_called()
-
- def test_handle_update_change_s_tag(self):
- with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
- patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
-
- fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=100, source_port=None,
- switch_datapath_id=None)
-
- serviceinstance_objects.return_value = [fsi]
-
- si = self.mock_westbound(fsi, s_tag=111, switch_datapath_id = "of:0000000000000201", switch_port = 3)
- serviceinstance_objects.return_value = [si]
-
- with self.assertRaises(Exception) as e:
- self.policy_step().handle_update(fsi)
-
- self.assertEqual(e.exception.message, "Westbound ServiceInstance changing s-tag is not currently permitted")
-
- def test_handle_update_change_switch_datapath_id(self):
- with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
- patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
-
- fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=None, source_port=None,
- switch_datapath_id="foo")
-
- serviceinstance_objects.return_value = [fsi]
-
- si = self.mock_westbound(fsi, s_tag=111, switch_datapath_id = "of:0000000000000201", switch_port = 3)
- serviceinstance_objects.return_value = [si]
-
- with self.assertRaises(Exception) as e:
- self.policy_step().handle_update(fsi)
-
- self.assertEqual(e.exception.message, "Westbound ServiceInstance changing switch_datapath_id is not currently permitted")
-
- def test_handle_update_change_source_port(self):
- with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
- patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
-
- fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=None, source_port=5,
- switch_datapath_id=None)
-
- serviceinstance_objects.return_value = [fsi]
-
- si = self.mock_westbound(fsi, s_tag=111, switch_datapath_id = "of:0000000000000201", switch_port = 3)
- serviceinstance_objects.return_value = [si]
-
- with self.assertRaises(Exception) as e:
- self.policy_step().handle_update(fsi)
-
- self.assertEqual(e.exception.message, "Westbound ServiceInstance changing source_port is not currently permitted")
-
-
def tearDown(self):
self.o = None
sys.path = self.sys_path_save
diff --git a/xos/synchronizer/models/convenience/fabric_crossconnect_service.py b/xos/synchronizer/models/convenience/fabric_crossconnect_service.py
new file mode 100644
index 0000000..c2bb8bf
--- /dev/null
+++ b/xos/synchronizer/models/convenience/fabric_crossconnect_service.py
@@ -0,0 +1,111 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from xosapi.orm import ORMWrapper, register_convenience_wrapper
+from xosapi.convenience.service import ORMWrapperService
+
+class ORMWrapperFabricCrossconnectService(ORMWrapperService):
+
+ """ Calling convention. Assume the subscribing service does approximately (needs some checks to see
+ if the methods exist before calling them) the following in its model_policy:
+
+ if not eastbound_service.validate_links(self):
+ eastbound_service.acquire_service_instance(self)
+ """
+
+ def acquire_service_instance(self, subscriber_service_instance):
+ """ Given a subscriber_service_instance:
+ 1) If there is an eligible provider_service_instance that can be used, then link to it
+ 2) Otherwise, create a new provider_service_instance and link to it.
+ """
+ (s_tag, switch_datapath_id, source_port) = self._get_west_fields(subscriber_service_instance)
+
+ FabricCrossconnectServiceInstance = self.stub.FabricCrossconnectServiceInstance
+ ServiceInstanceLink = self.stub.ServiceInstanceLink
+
+ candidates = FabricCrossconnectServiceInstance.objects.filter(owner_id=self.id,
+ s_tag=s_tag,
+ switch_datapath_id=switch_datapath_id,
+ source_port=source_port)
+
+ if candidates:
+ provider_service_instance = candidates[0]
+ else:
+ provider_service_instance = FabricCrossconnectServiceInstance(owner=self,
+ s_tag=s_tag,
+ switch_datapath_id=switch_datapath_id,
+ source_port=source_port)
+ provider_service_instance.save()
+
+ # NOTE: Lack-of-atomicity vulnerability -- provider_service_instance could be deleted before we created the
+ # link.
+
+ link = ServiceInstanceLink(provider_service_instance=provider_service_instance,
+ subscriber_service_instance=subscriber_service_instance)
+ link.save()
+
+ return provider_service_instance
+
+ def validate_links(self, subscriber_service_instance):
+ """ Validate existing links between the provider and subscriber service instances. If a valid link exists,
+ then return it. Return [] otherwise.
+
+ As a side-effect, delete any invalid links.
+ """
+
+ # Short-cut -- if there are no subscriber links then we can skip getting all the properties.
+ if not subscriber_service_instance.subscribed_links.exists():
+ return None
+
+ (s_tag, switch_datapath_id, source_port) = self._get_west_fields(subscriber_service_instance)
+
+ matched = []
+ for link in subscriber_service_instance.subscribed_links.all():
+ if link.provider_service_instance.owner.id == self.id:
+ fcsi = link.provider_service_instance.leaf_model
+ if (fcsi.s_tag == s_tag) and (fcsi.switch_datapath_id == switch_datapath_id) and \
+ (fcsi.source_port == source_port):
+ matched.append(fcsi)
+ else:
+ link.delete()
+ return matched
+
+ def _get_west_fields(self, subscriber_si):
+ """ _get_west_fields()
+
+ Helper function to inspect westbound service instance for fields that will be used inside of
+ FabricCrossconnectServiceInstance.
+ """
+
+ s_tag = subscriber_si.get_westbound_service_instance_properties("s_tag", include_self=True)
+ switch_datapath_id = subscriber_si.get_westbound_service_instance_properties("switch_datapath_id", include_self=True)
+ source_port = subscriber_si.get_westbound_service_instance_properties("switch_port", include_self=True)
+
+ if (s_tag is None):
+ raise Exception("Subscriber ServiceInstance %s s-tag is None" % subscriber_si.id)
+
+ if (not switch_datapath_id):
+ raise Exception("Subscriber ServiceInstance %s switch_datapath_id is unset" % subscriber_si.id)
+
+ if (source_port is None):
+ raise Exception("Subscriber ServiceInstance %s switch_port is None" % subscriber_si.id)
+
+ s_tag = int(s_tag)
+ source_port = int(source_port)
+
+ return (s_tag, switch_datapath_id, source_port)
+
+register_convenience_wrapper("FabricCrossconnectService", ORMWrapperFabricCrossconnectService)
diff --git a/xos/synchronizer/models/fabric-crossconnect.xproto b/xos/synchronizer/models/fabric-crossconnect.xproto
index d0d78e4..bbcfa66 100644
--- a/xos/synchronizer/models/fabric-crossconnect.xproto
+++ b/xos/synchronizer/models/fabric-crossconnect.xproto
@@ -11,9 +11,9 @@
option verbose_name = "Fabric Crossconnect Service Instance";
option owner_class_name="FabricCrossconnectService";
- optional int32 s_tag = 1 [help_text = "s-tag"];
- optional string switch_datapath_id = 2 [help_text = "switch datapath id"];
- optional int32 source_port = 3 [help_text = "source port of fabric crossconnect"];
+ required int32 s_tag = 1 [help_text = "s-tag"];
+ required string switch_datapath_id = 2 [help_text = "switch datapath id"];
+ required int32 source_port = 3 [help_text = "source port of fabric crossconnect"];
}
message BNGPortMapping (XOSBase) {
diff --git a/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py b/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
index 165eda9..5299fc1 100644
--- a/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
+++ b/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
@@ -152,13 +152,6 @@
if o.backend_handle:
onos = self.get_fabric_onos_info(o)
- # If some other subscriber is using the same entry, then we shouldn't delete it
- other_subscribers = FabricCrossconnectServiceInstance.objects.filter(backend_handle=o.backend_handle)
- other_subscribers = [x for x in other_subscribers if x.id != o.id]
- if other_subscribers:
- self.log.info("Other subscribers exist using same fabric crossconnect entry. Not deleting.")
- return
-
# backend_handle has everything we need in it to delete this entry.
(s_tag, switch_datapath_id) = self.extract_handle(o.backend_handle)
diff --git a/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py b/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py
index 8e4c5a8..9f07687 100644
--- a/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py
+++ b/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py
@@ -292,31 +292,6 @@
self.sync_step().delete_record(fsi)
self.assertTrue(m.called)
- @requests_mock.Mocker()
- def test_delete_in_use(self, m):
- with patch.object(FabricCrossconnectServiceInstance.objects, "get_items") as fcsi_objects:
- # The subscriber we want to delete
- fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service,
- backend_handle="111/of:0000000000000201",
- enacted=True)
-
- # Another subscriber using the same (s_tag, switch_datapath_id) pair
- fsi2 = FabricCrossconnectServiceInstance(id=7778, owner=self.service,
- backend_handle="111/of:0000000000000201",
- enacted=True)
-
- fcsi_objects.return_value=[fsi, fsi2]
-
- desired_data = {"deviceId": "of:0000000000000201",
- "vlanId": 111}
-
- m.delete("http://onos-fabric:8181/onos/segmentrouting/xconnect",
- status_code=204,
- additional_matcher=functools.partial(match_json, desired_data))
-
- self.sync_step().delete_record(fsi)
- self.assertFalse(m.called)
-
def tearDown(self):
self.o = None
sys.path = self.sys_path_save