SEBA-353 Pull step should be resilient to missing PonPorts;
Delete OLTDevice if Voltha unreachable

Change-Id: I806eaa9f3bdd281ef3184ec5d6e76177199d5cad
diff --git a/VERSION b/VERSION
index 63a1a1c..8dbb0f2 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.1.9
+2.1.10
diff --git a/xos/synchronizer/pull_steps/pull_onus.py b/xos/synchronizer/pull_steps/pull_onus.py
index d4c2d10..18f047c 100644
--- a/xos/synchronizer/pull_steps/pull_onus.py
+++ b/xos/synchronizer/pull_steps/pull_onus.py
@@ -82,7 +82,7 @@
                     log.debug("Skipping pull on ONUDevice %s as enacted < updated" % model.serial_number, serial_number=model.serial_number, id=model.id, enacted=model.enacted, updated=model.updated)
                     # if we are not updating the device we still need to pull ports
                     self.fetch_onu_ports(model)
-                    return
+                    continue
 
             except IndexError:
                 model = ONUDevice()
@@ -90,6 +90,18 @@
 
                 log.debug("ONUDevice is new, creating it", serial_number=onu["serial_number"])
 
+            try:
+                olt = OLTDevice.objects.get(device_id=onu["parent_id"])
+            except IndexError:
+                log.warning("Unable to find olt for ONUDevice", serial_number=onu["serial_number"], olt_device_id=onu["parent_id"])
+                continue
+
+            try:
+                pon_port = PONPort.objects.get(port_no=onu["parent_port_no"], olt_device_id=olt.id)
+            except IndexError:
+                log.warning("Unable to find pon_port for ONUDevice", serial_number=onu["serial_number"], olt_device_id=onu["parent_id"], port_no=onu["parent_port_no"])
+                continue
+
             # Adding feedback state to the device
             model.vendor = onu["vendor"]
             model.device_type = onu["type"]
@@ -100,9 +112,6 @@
             model.connect_status = onu["connect_status"]
             model.xos_managed = False
 
-            olt = OLTDevice.objects.get(device_id=onu["parent_id"])
-            pon_port = PONPort.objects.get(port_no=onu["parent_port_no"], olt_device_id=olt.id)
-
             model.pon_port = pon_port
             model.pon_port_id = pon_port.id
 
diff --git a/xos/synchronizer/pull_steps/test_pull_onus.py b/xos/synchronizer/pull_steps/test_pull_onus.py
index a1fb763..6fc5816 100644
--- a/xos/synchronizer/pull_steps/test_pull_onus.py
+++ b/xos/synchronizer/pull_steps/test_pull_onus.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import functools
 import unittest
 from mock import patch, call, Mock, PropertyMock
 import requests_mock
@@ -85,10 +86,18 @@
         self.olt = Mock()
         self.olt.id = 1
 
+        # second mock OLTDevice
+        self.olt2 = Mock()
+        self.olt2.id = 2
+
         # mock pon port
         self.pon_port = Mock()
         self.pon_port.id = 1
 
+        # mock pon port
+        self.pon_port2 = Mock()
+        self.pon_port2.id = 2
+
         # mock voltha responses
         self.devices = {
             "items": [
@@ -109,6 +118,39 @@
             ]
         }
 
+        self.two_devices = {
+            "items": [
+                {
+                    "id": "0001130158f01b2d",
+                    "type": "broadcom_onu",
+                    "vendor": "Broadcom",
+                    "serial_number": "BRCM22222222",
+                    "vendor_id": "BRCM",
+                    "adapter": "broadcom_onu",
+                    "vlan": 0,
+                    "admin_state": "ENABLED",
+                    "oper_status": "ACTIVE",
+                    "connect_status": "REACHABLE",
+                    "parent_id": "00010fc93996afea",
+                    "parent_port_no": 1
+                },
+                {
+                    "id": "0001130158f01b2e",
+                    "type": "broadcom_onu",
+                    "vendor": "Broadcom",
+                    "serial_number": "BRCM22222223",
+                    "vendor_id": "BRCM",
+                    "adapter": "broadcom_onu",
+                    "vlan": 0,
+                    "admin_state": "ENABLED",
+                    "oper_status": "ACTIVE",
+                    "connect_status": "REACHABLE",
+                    "parent_id": "00010fc93996afeb",
+                    "parent_port_no": 1
+                }
+            ],
+        }
+
         # TODO add ports
         self.ports = {
             "items": []
@@ -148,6 +190,88 @@
 
             self.assertEqual(mock_save.call_count, 1)
 
+    @requests_mock.Mocker()
+    def test_pull_bad_pon(self, m):
+
+        def olt_side_effect(device_id):
+            # fail the first onu device
+            if device_id=="00010fc93996afea":
+                return self.olt
+            else:
+                return self.olt2
+
+        def pon_port_side_effect(mock_pon_port, port_no, olt_device_id):
+            # fail the first onu device
+            if olt_device_id==1:
+                raise IndexError()
+            return self.pon_port2
+
+        with patch.object(VOLTService.objects, "all") as olt_service_mock, \
+                patch.object(OLTDevice.objects, "get") as mock_olt_device, \
+                patch.object(PONPort.objects, "get") as mock_pon_port, \
+                patch.object(ONUDevice, "save", autospec=True) as mock_save:
+            olt_service_mock.return_value = [self.volt_service]
+            mock_pon_port.side_effect = functools.partial(pon_port_side_effect, self.pon_port)
+            mock_olt_device.side_effect = olt_side_effect
+
+            m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.two_devices)
+            m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2d/ports", status_code=200, json=self.ports)
+            m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2e/ports", status_code=200, json=self.ports)
+
+            self.sync_step().pull_records()
+
+            self.assertEqual(mock_save.call_count, 1)
+            saved_onu = mock_save.call_args[0][0]
+
+            # we should get the second onu in self.two_onus
+
+            self.assertEqual(saved_onu.admin_state, "ENABLED")
+            self.assertEqual(saved_onu.oper_status, "ACTIVE")
+            self.assertEqual(saved_onu.connect_status, "REACHABLE")
+            self.assertEqual(saved_onu.device_type, "broadcom_onu")
+            self.assertEqual(saved_onu.vendor, "Broadcom")
+            self.assertEqual(saved_onu.device_id, "0001130158f01b2e")
+
+            self.assertEqual(mock_save.call_count, 1)
+
+    @requests_mock.Mocker()
+    def test_pull_bad_olt(self, m):
+
+        def olt_side_effect(device_id):
+            # fail the first onu device
+            if device_id=="00010fc93996afea":
+                raise IndexError()
+            else:
+                return self.olt2
+
+        with patch.object(VOLTService.objects, "all") as olt_service_mock, \
+                patch.object(OLTDevice.objects, "get") as mock_olt_device, \
+                patch.object(PONPort.objects, "get") as mock_pon_port, \
+                patch.object(ONUDevice, "save", autospec=True) as mock_save:
+            olt_service_mock.return_value = [self.volt_service]
+            mock_pon_port.return_value = self.pon_port2
+            mock_olt_device.side_effect = olt_side_effect
+
+            m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.two_devices)
+            m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2d/ports", status_code=200, json=self.ports)
+            m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2e/ports", status_code=200, json=self.ports)
+
+            self.sync_step().pull_records()
+
+            self.assertEqual(mock_save.call_count, 1)
+            saved_onu = mock_save.call_args[0][0]
+
+            # we should get the second onu in self.two_onus
+
+            self.assertEqual(saved_onu.admin_state, "ENABLED")
+            self.assertEqual(saved_onu.oper_status, "ACTIVE")
+            self.assertEqual(saved_onu.connect_status, "REACHABLE")
+            self.assertEqual(saved_onu.device_type, "broadcom_onu")
+            self.assertEqual(saved_onu.vendor, "Broadcom")
+            self.assertEqual(saved_onu.device_id, "0001130158f01b2e")
+
+            self.assertEqual(mock_save.call_count, 1)
+
 
 if __name__ == "__main__":
     unittest.main()
\ No newline at end of file
diff --git a/xos/synchronizer/steps/sync_olt_device.py b/xos/synchronizer/steps/sync_olt_device.py
index 4646dbd..40f884e 100644
--- a/xos/synchronizer/steps/sync_olt_device.py
+++ b/xos/synchronizer/steps/sync_olt_device.py
@@ -185,22 +185,25 @@
             log.warning("OLTDevice %s has no device_id, it was never saved in VOLTHA" % model.name)
             return
         else:
-            # Disable the OLT device
-            request = requests.post("%s:%d/api/v1/devices/%s/disable" % (voltha['url'], voltha['port'], model.device_id))
+            try:
+                # Disable the OLT device
+                request = requests.post("%s:%d/api/v1/devices/%s/disable" % (voltha['url'], voltha['port'], model.device_id))
 
-            if request.status_code != 200:
-                log.error("Failed to disable OLT device in VOLTHA: %s - %s" % (model.name, model.device_id), rest_response=request.text, rest_status_code=request.status_code)
-                raise Exception("Failed to disable OLT device in VOLTHA")
+                if request.status_code != 200:
+                    log.error("Failed to disable OLT device in VOLTHA: %s - %s" % (model.name, model.device_id), rest_response=request.text, rest_status_code=request.status_code)
+                    raise Exception("Failed to disable OLT device in VOLTHA")
 
-            # NOTE [teo] wait some time after the disable to let VOLTHA doing its things
-            i = 0
-            for i in list(reversed(range(10))):
-                sleep(1)
-                log.info("Deleting the OLT in %s seconds" % i)
+                # NOTE [teo] wait some time after the disable to let VOLTHA doing its things
+                i = 0
+                for i in list(reversed(range(10))):
+                    sleep(1)
+                    log.info("Deleting the OLT in %s seconds" % i)
 
-            # Delete the OLT device
-            request = requests.delete("%s:%d/api/v1/devices/%s/delete" % (voltha['url'], voltha['port'], model.device_id))
+                # Delete the OLT device
+                request = requests.delete("%s:%d/api/v1/devices/%s/delete" % (voltha['url'], voltha['port'], model.device_id))
 
-            if request.status_code != 200:
-                log.error("Failed to delete OLT device from VOLTHA: %s - %s" % (model.name, model.device_id), rest_response=request.text, rest_status_code=request.status_code)
-                raise Exception("Failed to delete OLT device from VOLTHA")
+                if request.status_code != 200:
+                    log.error("Failed to delete OLT device from VOLTHA: %s - %s" % (model.name, model.device_id), rest_response=request.text, rest_status_code=request.status_code)
+                    raise Exception("Failed to delete OLT device from VOLTHA")
+            except requests.ConnectionError:
+                log.warning("ConnectionError when contacting Voltha in OLT delete step", name=model.name, device_id=model.device_id)
diff --git a/xos/synchronizer/steps/test_sync_olt_device.py b/xos/synchronizer/steps/test_sync_olt_device.py
index 18ed99b..38da1a1 100644
--- a/xos/synchronizer/steps/test_sync_olt_device.py
+++ b/xos/synchronizer/steps/test_sync_olt_device.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from requests import ConnectionError
 import unittest
 import functools
 from mock import patch, call, Mock, PropertyMock
@@ -339,6 +340,18 @@
 
         self.assertEqual(m.call_count, 2)
 
+    @patch('requests.post')
+    def test_delete_record_connectionerror(self, m):
+        self.o.of_id = "0001000ce2314000"
+        self.o.device_id = "123"
+
+        m.side_effect = ConnectionError()
+
+        self.sync_step().delete_record(self.o)
+
+        # No exception thrown, as ConnectionError will be caught
+
+
     @requests_mock.Mocker()
     def test_delete_unsynced_record(self, m):