[SEBA-179] Fabric-crossconnect service should react to changes in BNGPortMapping

Change-Id: Ie7174ac31af300dabb50e0d850fff0c474e5366a
diff --git a/VERSION b/VERSION
index 38f77a6..e9307ca 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.0.1
+2.0.2
diff --git a/xos/synchronizer/models/fabric-crossconnect.xproto b/xos/synchronizer/models/fabric-crossconnect.xproto
index 5844106..a7cbe35 100644
--- a/xos/synchronizer/models/fabric-crossconnect.xproto
+++ b/xos/synchronizer/models/fabric-crossconnect.xproto
@@ -25,6 +25,8 @@
 }
 
 message BNGPortMapping (XOSBase) {
+    option sync_implemented = "True";
+
     required string s_tag = 1 [
         help_text = "Single s-tag, range of s-tags, or 'ANY'",
         max_length = 1024,
diff --git a/xos/synchronizer/steps/helpers.py b/xos/synchronizer/steps/helpers.py
new file mode 100644
index 0000000..159dda8
--- /dev/null
+++ b/xos/synchronizer/steps/helpers.py
@@ -0,0 +1,57 @@
+# 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 __future__ import absolute_import
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class Helpers():
+
+    @staticmethod
+    def format_url(url):
+        if 'http' in url:
+            return url
+        else:
+            return 'http://%s' % url
+
+    @staticmethod
+    def get_fabric_onos_info(model_accessor, service):
+        fabric_onos = [s.leaf_model for s in service.provider_services if "onos" in s.name.lower()]
+        if len(fabric_onos) == 0:
+            raise Exception('Cannot find ONOS service in provider_services of Fabric-Crossconnect')
+
+        fabric_onos = fabric_onos[0]
+        return {
+            'url': Helpers.format_url(
+                "%s:%s" %
+                (fabric_onos.rest_hostname,
+                fabric_onos.rest_port)),
+                'user': fabric_onos.rest_username,
+                'pass': fabric_onos.rest_password}
+
+    @staticmethod
+    def range_matches(value, pattern):
+        value = int(value)
+        for this_range in pattern.split(","):
+            this_range = this_range.strip()
+            if "-" in this_range:
+                (first, last) = this_range.split("-")
+                first = int(first.strip())
+                last = int(last.strip())
+                if (value >= first) and (value <= last):
+                    return True
+            elif this_range.lower() == "any":
+                return False
diff --git a/xos/synchronizer/steps/sync_bng_port_mapping.py b/xos/synchronizer/steps/sync_bng_port_mapping.py
new file mode 100644
index 0000000..c704e6d
--- /dev/null
+++ b/xos/synchronizer/steps/sync_bng_port_mapping.py
@@ -0,0 +1,125 @@
+# 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.
+
+import os, sys
+import json
+from xossynchronizer.steps.syncstep import SyncStep, DeferredException
+from xossynchronizer.modelaccessor import model_accessor, FabricCrossconnectServiceInstance, ServiceInstance, BNGPortMapping
+
+import requests
+from requests import ConnectionError
+from requests.auth import HTTPBasicAuth
+from requests.models import InvalidURL
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+from helpers import Helpers
+log = create_logger(Config().get('logging'))
+
+
+class SyncBNGPortMapping(SyncStep):
+    provides = [BNGPortMapping]
+    observes = BNGPortMapping
+
+    def remove_crossconnect(self, fcsis):
+        for fcsi in fcsis:
+            onos = Helpers.get_fabric_onos_info(self.model_accessor, fcsi.owner)
+
+            url = onos['url'] + '/onos/segmentrouting/xconnect'
+            data = {"deviceId": fcsi.switch_datapath_id,
+                    "vlanId": fcsi.s_tag}
+            log.info("Sending request to ONOS", url=url)
+            r = requests.delete(url, json=data, auth=HTTPBasicAuth(onos['user'], onos['pass']))
+            if r.status_code != 204:
+                raise Exception("Failed to remove fabric crossconnect in ONOS: %s" % r.text)
+            fcsi.save(always_update_timestamp = True)
+
+    def find_crossconnect(self, bng_s_tag):
+        if(bng_s_tag.isnumeric()):
+            xconnect_si = self.model_accessor.FabricCrossconnectServiceInstance.objects.filter(s_tag=int(bng_s_tag))
+            if xconnect_si:
+                log.info("Crossconnects belonging having s-tag equal to s-tags: %s" % xconnect_si)
+                return xconnect_si
+        else:
+            [fcsis_range, fcsis_any] = [[], []]
+            for fcsi in self.model_accessor.FabricCrossconnectServiceInstance.objects.all():
+                if Helpers.range_matches(fcsi.s_tag, bng_s_tag):
+                    fcsis_range.append(fcsi)
+                else:
+                    fcsis_any.append(fcsi)
+            if fcsis_range:
+                log.info("Crossconnects belonging to bng range s-tags: %s" % fcsis_range)
+                return fcsis_range
+            else:
+                log.info("Crossconnects belonging to bng any s-tags: %s" % fcsis_any)
+                return fcsis_any
+
+    def check_switch_port_change(self, model):
+        fcsis = self.find_crossconnect(model.s_tag)
+        isChanged = False
+        remove_xconnect = []
+        if fcsis:
+            for fcsi in fcsis:
+                onos = Helpers.get_fabric_onos_info(self.model_accessor, fcsi.owner)
+                log.info("ONOS belonging to fabric crossconnect instance: %s" % onos)
+
+                url = onos['url'] + '/onos/segmentrouting/xconnect'
+                log.info("Sending request to ONOS", url=url)
+                r = requests.get(url, auth=HTTPBasicAuth(onos['user'], onos['pass']))
+                if r.status_code != 200:
+                    log.error(r.text)
+                    raise Exception("Failed to get onos devices")
+                else:
+                    try:
+                        log.info("Get devices response", json=r.json())
+                    except Exception:
+                        log.info("Get devices exception response", text=r.text) 
+                xconnects = r.json()["xconnects"]
+                for xconn in xconnects:
+                    val = xconn['deviceId']
+                    if(str(fcsi.switch_datapath_id) == str(val)):
+                        if model.switch_port not in xconn['endpoints']:
+                            remove_xconnect.append(fcsi)
+            self.remove_crossconnect(remove_xconnect)
+            isChanged = True
+        else:
+            log.info("No Fabric-xconnect-si found & saving bng instance.")
+        return isChanged
+
+    def sync_record(self, model):
+        log.info("Sync started for BNGPortMapping instance: %s" % model.id)
+        log.info('Syncing BNGPortMapping instance', object=str(model), **model.tologdict()) 
+        if model.old_s_tag:
+            if (model.old_s_tag != model.s_tag):
+                fcsis = self.find_crossconnect(model.old_s_tag)
+                if fcsis:
+                    log.info("Xconnect-instance linked to bng : %s" % fcsis)
+                    self.remove_crossconnect(fcsis)
+                else:
+                    log.info("No crossconnect is found for current bng instance")
+            else:
+                self.check_switch_port_change(model)
+        else:
+            if self.check_switch_port_change(model):
+                log.info("Changed bng switch port is repushed to ONOS")
+        log.info("Completing Synchronization for BNGPortMapping instance: %s" % model.id)
+
+    def delete_record(self,model):
+        log.info('Deleting BNGPortMapping instance', object=str(model), **model.tologdict()) 
+        fcsis = self.find_crossconnect(model.s_tag)
+        if fcsis:
+            log.info("Xconnect-instance linked to bng : %s" % fcsis)
+            self.remove_crossconnect(fcsis)
+        log.info("Completing deletion of bng instance")
diff --git a/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py b/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
index c03adab..4d8a355 100644
--- a/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
+++ b/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
@@ -22,6 +22,7 @@
 from multistructlog import create_logger
 import requests
 from requests.auth import HTTPBasicAuth
+from helpers import Helpers
 
 
 class SyncFabricCrossconnectServiceInstance(SyncStep):
@@ -30,35 +31,6 @@
 
     observes = FabricCrossconnectServiceInstance
 
-    @staticmethod
-    def format_url(url):
-        if 'http' in url:
-            return url
-        else:
-            return 'http://%s' % url
-
-    @staticmethod
-    def get_fabric_onos_info(si):
-
-        # get the fabric-crossconnect service
-        fabric_crossconnect = si.owner
-
-        # get the onos_fabric service
-        fabric_onos = [s.leaf_model for s in fabric_crossconnect.provider_services if "onos" in s.name.lower()]
-
-        if len(fabric_onos) == 0:
-            raise Exception('Cannot find ONOS service in provider_services of Fabric-Crossconnect')
-
-        fabric_onos = fabric_onos[0]
-
-        return {
-            'url': SyncFabricCrossconnectServiceInstance.format_url(
-                "%s:%s" %
-                (fabric_onos.rest_hostname,
-                 fabric_onos.rest_port)),
-            'user': fabric_onos.rest_username,
-            'pass': fabric_onos.rest_password}
-
     def make_handle(self, s_tag, switch_datapath_id):
         # 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
@@ -109,7 +81,7 @@
         if (o.policed is None) or (o.policed < o.updated):
             raise DeferredException("Waiting for model_policy to run on fcsi %s" % o.id)
 
-        onos = self.get_fabric_onos_info(o)
+        onos = Helpers.get_fabric_onos_info(self.model_accessor, o.owner)
 
         ServiceInstance.objects.get(id=o.id)
 
@@ -156,7 +128,7 @@
         self.log.info("Deleting Fabric Crossconnect Service Instance", service_instance=o)
 
         if o.backend_handle:
-            onos = self.get_fabric_onos_info(o)
+            onos = Helpers.get_fabric_onos_info(self.model_accessor, o.owner)
 
             # 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 f758d5c..edfd1c8 100644
--- a/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py
+++ b/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py
@@ -63,6 +63,9 @@
         from sync_fabric_crossconnect_service_instance import SyncFabricCrossconnectServiceInstance, model_accessor, \
             DeferredException
 
+        from helpers import Helpers
+        self.helpers = Helpers
+
         # import all class names to globals
         for (k, v) in model_accessor.all_model_classes.items():
             globals()[k] = v
@@ -91,10 +94,10 @@
         return si
 
     def test_format_url(self):
-        url = self.sync_step(model_accessor=self.model_accessor).format_url("foo.com/bar")
+        url = self.helpers.format_url("foo.com/bar")
         self.assertEqual(url, "http://foo.com/bar")
 
-        url = self.sync_step(model_accessor=self.model_accessor).format_url("http://foo.com/bar")
+        url = self.helpers.format_url("http://foo.com/bar")
         self.assertEqual(url, "http://foo.com/bar")
 
     def test_make_handle_extract_handle(self):
@@ -107,7 +110,7 @@
     def test_get_fabric_onos_init(self):
         fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service)
 
-        d = self.sync_step(model_accessor=self.model_accessor).get_fabric_onos_info(fsi)
+        d = self.helpers.get_fabric_onos_info(self.model_accessor, fsi.owner)
 
         self.assertEqual(d["url"], "http://onos-fabric:8181")
         self.assertEqual(d["user"], "onos")