SEBA-89 Finish sync steps, add unit tests
Change-Id: I39495e6167fceffa4c86ad5e6c4d238271ecbcc7
diff --git a/samples/bng_mapping.yaml b/samples/bng_mapping.yaml
new file mode 100644
index 0000000..f61bfdb
--- /dev/null
+++ b/samples/bng_mapping.yaml
@@ -0,0 +1,27 @@
+# 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.
+
+# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @pon_port.yaml http://192.168.99.100:30007/run
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+ - custom_types/bngportmapping.yaml
+description: Create a bng port mapping
+topology_template:
+ node_templates:
+ bngmapping:
+ type: tosca.nodes.BNGPortMapping
+ properties:
+ s_tag: 222
+ switch_port: 4
diff --git a/xos/synchronizer/models/fabric-crossconnect.xproto b/xos/synchronizer/models/fabric-crossconnect.xproto
index 29b90e1..4a7e8dc 100644
--- a/xos/synchronizer/models/fabric-crossconnect.xproto
+++ b/xos/synchronizer/models/fabric-crossconnect.xproto
@@ -6,8 +6,12 @@
option description="Fabric Crossconnect implementation";
}
-
message FabricCrossconnectServiceInstance (ServiceInstance){
option verbose_name = "Fabric Crossconnect Service Instance";
option owner_class_name="FabricCrossconnectService";
}
+
+message BNGPortMapping (XOSBase) {
+ required int32 s_tag = 1 [help_text = "S Tag", null = False, db_index = False, blank = False, unique=True, tosca_key=True];
+ required int32 switch_port = 2 [help_text = "Port Number", null = False, db_index = False, blank = False];
+}
diff --git a/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py b/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
index 8f03075..46a4694 100644
--- a/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
+++ b/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
@@ -13,7 +13,7 @@
# limitations under the License.
from synchronizers.new_base.syncstep import SyncStep, DeferredException
-from synchronizers.new_base.modelaccessor import model_accessor, FabricCrossconnectServiceInstance, ServiceInstance
+from synchronizers.new_base.modelaccessor import model_accessor, FabricCrossconnectServiceInstance, ServiceInstance, BNGPortMapping
from xosconfig import Config
from multistructlog import create_logger
@@ -55,9 +55,16 @@
'pass': fabric_onos.rest_password
}
- def get_bng_port(self, o):
- # TODO(smbaker): Get BNG port from somewhere
- return 2
+ def make_handle(self, s_tag, west_dpid):
+ # Generate a backend_handle that uniquely identifies the cross connect. ONOS doesn't provide us a handle, so
+ # we make up our own. This helps us to detect other FabricCrossconnectServiceInstance using the same
+ # entry, as well as to be able to extract the necessary information to delete the entry later.
+ return "%d/%s" % (s_tag, west_dpid)
+
+ def extract_handle(self, backend_handle):
+ (s_tag, dpid) = backend_handle.split("/",1)
+ s_tag = int(s_tag)
+ return (s_tag, dpid)
def sync_record(self, o):
self.log.info("Sync'ing Fabric Crossconnect Service Instance", service_instance=o)
@@ -67,15 +74,19 @@
si = ServiceInstance.objects.get(id=o.id)
s_tag = si.get_westbound_service_instance_properties("s_tag")
- west_dpid = si.get_westbound_service_instance_properties("switch_datapath_id")
+ dpid = si.get_westbound_service_instance_properties("switch_datapath_id")
west_port = si.get_westbound_service_instance_properties("switch_port")
- east_port = self.get_bng_port(o)
- data = { "deviceId": west_dpid,
+ bng_mappings = BNGPortMapping.objects.filter(s_tag = s_tag)
+ if not bng_mappings:
+ raise Exception("Unable to determine BNG port for s_tag %s" % s_tag)
+ east_port = bng_mappings[0].switch_port
+
+ data = { "deviceId": dpid,
"vlanId": s_tag,
"ports": [ west_port, east_port ] }
- url = onos['url'] + '/onos/segmentsrouting/xconnect'
+ url = onos['url'] + '/onos/segmentrouting/xconnect'
self.log.info("Sending request to ONOS", url=url, body=data)
@@ -84,25 +95,29 @@
if r.status_code != 200:
raise Exception("Failed to create fabric crossconnect in ONOS: %s" % r.text)
+ o.backend_handle = self.make_handle(s_tag, dpid)
+ o.save(update_fields=["backend_handle"])
+
self.log.info("ONOS response", res=r.text)
def delete_record(self, o):
self.log.info("Deleting Fabric Crossconnect Service Instance", service_instance=o)
- # TODO(smbaker): make sure it's not in use by some other subscriber
-
- if o.enacted:
+ if o.backend_handle:
onos = self.get_fabric_onos_info(o)
- si = ServiceInstance.objects.get(id=o.id)
+ # 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
- # TODO(smbaker): unable to get westbound service_instance while deleting?
+ # backend_handle has everything we need in it to delete this entry.
+ (s_tag, dpid) = self.extract_handle(o.backend_handle)
- s_tag = si.get_westbound_service_instance_properties("s_tag")
- west_dpid = si.get_westbound_service_instance_properties("switch_datapath_id")
-
- data = { "deviceId": west_dpid,
- "vlanID": s_tag }
+ data = { "deviceId": dpid,
+ "vlanId": s_tag }
url = onos['url'] + '/onos/segmentrouting/xconnect'
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 5140ea6..4455497 100644
--- a/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py
+++ b/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py
@@ -15,7 +15,7 @@
import unittest
import functools
-from mock import patch, call, Mock, PropertyMock
+from mock import patch, call, Mock, PropertyMock, MagicMock
import requests_mock
import multistructlog
from multistructlog import create_logger
@@ -44,10 +44,8 @@
raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name))
# END generate model from xproto
-def mock_get_westbound_service_instance_properties(prop):
- if prop == "status":
- return "enabled"
- return prop
+def mock_get_westbound_service_instance_properties(props, prop):
+ return props[prop]
def match_json(desired, req):
if desired!=req.json():
@@ -55,7 +53,7 @@
return False
return True
-class TestSyncOLTDevice(unittest.TestCase):
+class TestSyncFabricCrossconnectServiceInstance(unittest.TestCase):
def setUp(self):
global DeferredException
@@ -81,14 +79,139 @@
for (k, v) in model_accessor.all_model_classes.items():
globals()[k] = v
-
self.sync_step = SyncFabricCrossconnectServiceInstance
self.sync_step.log = Mock()
- # TODO: stuff
+ # mock onos-fabric
+ self.onos_fabric = Service(name = "onos-fabric",
+ rest_hostname = "onos-fabric",
+ rest_port = "8181",
+ rest_username = "onos",
+ rest_password = "rocks")
- def test_sync(self):
- pass
+ self.service = FabricCrossconnectService(name = "fcservice",
+ provider_services = [self.onos_fabric])
+
+ def mock_westbound(self, fsi, s_tag, switch_datapath_id, switch_port):
+ # Mock out a ServiceInstance so the syncstep can call get_westbound_service_instance_properties on it
+ si = ServiceInstance(id=fsi.id)
+ si.get_westbound_service_instance_properties = functools.partial(
+ mock_get_westbound_service_instance_properties,
+ {"s_tag": s_tag,
+ "switch_datapath_id": switch_datapath_id,
+ "switch_port": switch_port})
+ return si
+
+ def test_format_url(self):
+ url = self.sync_step().format_url("foo.com/bar")
+ self.assertEqual(url, "http://foo.com/bar")
+
+ url = self.sync_step().format_url("http://foo.com/bar")
+ self.assertEqual(url, "http://foo.com/bar")
+
+ def test_make_handle_extract_handle(self):
+ h = self.sync_step().make_handle(222, "of:0000000000000201")
+ (s_tag, dpid) = self.sync_step().extract_handle(h)
+
+ self.assertEqual(s_tag, 222)
+ self.assertEqual(dpid, "of:0000000000000201")
+
+ def test_get_fabric_onos_init(self):
+ fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service)
+
+ d = self.sync_step().get_fabric_onos_info(fsi)
+
+ self.assertEqual(d["url"], "http://onos-fabric:8181")
+ self.assertEqual(d["user"], "onos")
+ self.assertEqual(d["pass"], "rocks")
+
+
+ @requests_mock.Mocker()
+ def test_sync(self, m):
+ with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
+ patch.object(BNGPortMapping.objects, "get_items") as bng_objects, \
+ patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+
+ fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service)
+
+ si = self.mock_westbound(fsi, s_tag=111, switch_datapath_id = "of:0000000000000201", switch_port = 3)
+ serviceinstance_objects.return_value = [si]
+
+ bngmapping = BNGPortMapping(s_tag=111, switch_port=4)
+ bng_objects.return_value = [bngmapping]
+
+ desired_data = {"deviceId": "of:0000000000000201",
+ "vlanId": 111,
+ "ports": [3, 4]}
+
+ m.post("http://onos-fabric:8181/onos/segmentrouting/xconnect",
+ status_code=200,
+ additional_matcher=functools.partial(match_json, desired_data))
+
+ self.sync_step().sync_record(fsi)
+ self.assertTrue(m.called)
+
+ self.assertEqual(fsi.backend_handle, "111/of:0000000000000201")
+ fcsi_save.assert_called()
+
+ def test_sync_no_bng_mapping(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)
+
+ 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.sync_step().sync_record(fsi)
+
+ self.assertEqual(e.exception.message, "Unable to determine BNG port for s_tag 111")
+
+ @requests_mock.Mocker()
+ def test_delete(self, m):
+ with patch.object(FabricCrossconnectServiceInstance.objects, "get_items") as fcsi_objects, \
+ patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+ fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service,
+ backend_handle="111/of:0000000000000201",
+ enacted=True)
+
+ fcsi_objects.return_value=[fsi]
+
+ 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.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, dpid) 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