Do not provision the OLT until the Tech-profile has been pushed into ETCD

Change-Id: I78671e63acfaab1ac8d865b22f12a4ca35c11fc1
diff --git a/xos/synchronizer/migrations/0011_auto_20190626_2027.py b/xos/synchronizer/migrations/0011_auto_20190626_2027.py
new file mode 100644
index 0000000..0e2c028
--- /dev/null
+++ b/xos/synchronizer/migrations/0011_auto_20190626_2027.py
@@ -0,0 +1,34 @@
+# 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.
+
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.21 on 2019-06-27 00:27
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('volt', '0010_auto_20190620_1906'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='technologyprofile_decl',
+            name='technology',
+            field=models.CharField(choices=[(b'gpon', b'gpon'), (b'xgspon', b'xgspon')], db_index=True, help_text=b'The technology being utilized by the adaptor', max_length=16),
+        ),
+    ]
diff --git a/xos/synchronizer/models/models.py b/xos/synchronizer/models/models.py
index a71817e..5198db2 100644
--- a/xos/synchronizer/models/models.py
+++ b/xos/synchronizer/models/models.py
@@ -148,5 +148,9 @@
             except ValueError as e:
                 raise XOSValidationError('Technology Profile value not in valid JSON format')
 
+        # TODO validate the tech profile (in the model), see errors like:
+        # num_gem_ports=tech_profile[TechProfile.NUM_GEM_PORTS],\nKeyError: \'num_gem_ports\''
+        # in File "/voltha/common/tech_profile/tech_profile.py", line 403, in _get_tech_profile
+
         super(TechnologyProfile, self).save(*args, **kwargs)
 
diff --git a/xos/synchronizer/models/volt.xproto b/xos/synchronizer/models/volt.xproto
index 3843c6d..8461301 100644
--- a/xos/synchronizer/models/volt.xproto
+++ b/xos/synchronizer/models/volt.xproto
@@ -236,6 +236,7 @@
         help_text = "The technology being utilized by the adaptor",
         db_index = True,
         tosca_key = True,
+        choices = "(('gpon', 'gpon'), ('xgspon', 'xgspon'))",
         max_length = 16];
     required int32 profile_id = 2 [
         help_text = "The numeric id of the profile",
diff --git a/xos/synchronizer/steps/sync_olt_device.py b/xos/synchronizer/steps/sync_olt_device.py
index 686afcf..469e879 100644
--- a/xos/synchronizer/steps/sync_olt_device.py
+++ b/xos/synchronizer/steps/sync_olt_device.py
@@ -18,7 +18,7 @@
 from multistructlog import create_logger
 from requests.auth import HTTPBasicAuth
 from xossynchronizer.steps.syncstep import SyncStep, DeferredException
-from xossynchronizer.modelaccessor import OLTDevice, model_accessor
+from xossynchronizer.modelaccessor import OLTDevice, TechnologyProfile, model_accessor
 from xosconfig import Config
 
 import os, sys
@@ -166,9 +166,25 @@
             except Exception:
                 print request.text
 
+    def wait_for_tp(self, technology):
+        """
+        Check if a technology profile for this technology has been already pushed to ETCD,
+        if not defer the OLT Provisioning.
+        :param technology: string - the technology to check for a tech profile
+        :return: True (or raises DeferredException)
+        """
+        try:
+            tps = TechnologyProfile.objects.get(technology=technology, backend_code=1)
+        except IndexError:
+            raise DeferredException("Waiting for a TechnologyProfile (technology=%s) to be synchronized" % technology)
+
+        return True
+
     def sync_record(self, model):
         log.info("Synching device", object=str(model), **model.tologdict())
 
+        self.wait_for_tp(model.technology)
+
         if model.admin_state not in ["ENABLED", "DISABLED"]:
             raise Exception("OLT Device %s admin_state has invalid value %s" % (model.id, model.admin_state))
 
diff --git a/xos/synchronizer/steps/sync_tech_profile.py b/xos/synchronizer/steps/sync_tech_profile.py
index 3c36f36..dcd5f52 100644
--- a/xos/synchronizer/steps/sync_tech_profile.py
+++ b/xos/synchronizer/steps/sync_tech_profile.py
@@ -39,7 +39,7 @@
     observes = TechnologyProfile
 
     def update_etcd(self, operation, key, value):
-        log.info('Update Etcd store: ', operation=operation, key=key, value=value)
+        log.info('Update Etcd store: ', operation=operation, key=PREFIX + key, value=value)
 
         etcd = etcd3.client(host=ETCD_HOST_URL, port=ETCD_PORT)
         if operation == 'PUT':
@@ -55,6 +55,7 @@
            log.warning('Invalid or unsupported Etcd operation: %s' % operation)
 
     def sync_record(self, model):
+
         log.info('Synching TechnologyProfile', object=str(model), **model.tologdict())
 
         log.info('TechnologyProfile: %s : %s' % (model.technology, model.profile_id))
diff --git a/xos/synchronizer/steps/test_sync_olt_device.py b/xos/synchronizer/steps/test_sync_olt_device.py
index 307cae1..57330d2 100644
--- a/xos/synchronizer/steps/test_sync_olt_device.py
+++ b/xos/synchronizer/steps/test_sync_olt_device.py
@@ -50,6 +50,10 @@
         from xossynchronizer.modelaccessor import model_accessor
         self.model_accessor = model_accessor
 
+        # import all class names to globals
+        for (k, v) in model_accessor.all_model_classes.items():
+            globals()[k] = v
+
         from sync_olt_device import SyncOLTDevice, DeferredException
         self.sync_step = SyncOLTDevice
 
@@ -97,6 +101,12 @@
 
         self.voltha_devices_response = {"id": "123", "serial_number": "foobar"}
 
+        self.tp = TechnologyProfile(
+            technology="xgspon",
+            profile_id=64,
+            profile_value="{}"
+        )
+
     def tearDown(self):
         self.o = None
         sys.path = self.sys_path_save
@@ -127,8 +137,12 @@
         """
         m.post("http://voltha_url:1234/api/v1/devices", status_code=500, text="MockError")
 
-        with self.assertRaises(Exception) as e:
+        with self.assertRaises(Exception) as e, \
+            patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
+
             self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+
         self.assertEqual(e.exception.message, "Failed to add OLT device: MockError")
 
     @requests_mock.Mocker()
@@ -138,8 +152,12 @@
         """
         m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json={"id": ""})
 
-        with self.assertRaises(Exception) as e:
+        with self.assertRaises(Exception) as e, \
+            patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
+
             self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+
         self.assertEqual(e.exception.message, "VOLTHA Device Id is empty. This probably means that the OLT device is already provisioned in VOLTHA")
 
     @requests_mock.Mocker()
@@ -150,7 +168,10 @@
         m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.voltha_devices_response)
         m.post("http://voltha_url:1234/api/v1/devices/123/enable", status_code=500, text="EnableError")
 
-        with self.assertRaises(Exception) as e:
+        with self.assertRaises(Exception) as e, \
+            patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
+
             self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
 
         self.assertEqual(e.exception.message, "Failed to enable OLT device: EnableError")
@@ -189,16 +210,19 @@
         m.post("http://onos:4321/onos/v1/network/configuration/", status_code=200, json=onos_expected_conf,
                additional_matcher=functools.partial(match_json, onos_expected_conf))
 
-        self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
-        self.assertEqual(self.o.admin_state, "ENABLED")
-        self.assertEqual(self.o.oper_status, "ACTIVE")
-        self.assertEqual(self.o.serial_number, "foobar")
-        self.assertEqual(self.o.of_id, "0001000ce2314000")
+        with patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
 
-        # One save during preprovision
-        # One save during activation to set backend_status to "Waiting for device to activate"
-        # One save after activation has succeeded
-        self.assertEqual(self.o.save_changed_fields.call_count, 3)
+            self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+            self.assertEqual(self.o.admin_state, "ENABLED")
+            self.assertEqual(self.o.oper_status, "ACTIVE")
+            self.assertEqual(self.o.serial_number, "foobar")
+            self.assertEqual(self.o.of_id, "0001000ce2314000")
+
+            # One save during preprovision
+            # One save during activation to set backend_status to "Waiting for device to activate"
+            # One save after activation has succeeded
+            self.assertEqual(self.o.save_changed_fields.call_count, 3)
 
     @requests_mock.Mocker()
     def test_sync_record_success_mac_address(self, m):
@@ -239,15 +263,18 @@
         }
         m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=logical_devices)
 
-        self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
-        self.assertEqual(self.o.admin_state, "ENABLED")
-        self.assertEqual(self.o.oper_status, "ACTIVE")
-        self.assertEqual(self.o.of_id, "0001000ce2314000")
+        with patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
 
-        # One save during preprovision
-        # One save during activation to set backend_status to "Waiting for device to activate"
-        # One save after activation has succeeded
-        self.assertEqual(self.o.save_changed_fields.call_count, 3)
+            self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+            self.assertEqual(self.o.admin_state, "ENABLED")
+            self.assertEqual(self.o.oper_status, "ACTIVE")
+            self.assertEqual(self.o.of_id, "0001000ce2314000")
+
+            # One save during preprovision
+            # One save during activation to set backend_status to "Waiting for device to activate"
+            # One save after activation has succeeded
+            self.assertEqual(self.o.save_changed_fields.call_count, 3)
 
     @requests_mock.Mocker()
     def test_sync_record_enable_timeout(self, m):
@@ -279,7 +306,10 @@
         }
         m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=logical_devices)
 
-        with self.assertRaises(Exception) as e:
+        with self.assertRaises(Exception) as e, \
+            patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
+
             self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
 
         self.assertEqual(e.exception.message, "It was not possible to activate OLTDevice with id 1")
@@ -317,10 +347,12 @@
         m.post("http://onos:4321/onos/v1/network/configuration/", status_code=200, json=expected_conf,
                additional_matcher=functools.partial(match_json, expected_conf))
 
-        self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
-        self.o.save.assert_not_called()
-        self.o.save_changed_fields.assert_not_called()
+        with patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
 
+            self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+            self.o.save.assert_not_called()
+            self.o.save_changed_fields.assert_not_called()
 
     @requests_mock.Mocker()
     def test_sync_record_deactivate(self, m):
@@ -343,16 +375,19 @@
         m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.voltha_devices_response, additional_matcher=functools.partial(match_json, expected_conf))
         m.post("http://voltha_url:1234/api/v1/devices/123/disable", status_code=200)
 
-        self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+        with patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
 
-        # No saves as state has not changed (will eventually be saved by synchronizer framework to update backend_status)
-        self.assertEqual(self.o.save.call_count, 0)
-        self.assertEqual(self.o.save_changed_fields.call_count, 0)
+            self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
 
+            # No saves as state has not changed (will eventually be saved
+            # by the synchronizer framework to update backend_status)
+            self.assertEqual(self.o.save.call_count, 0)
+            self.assertEqual(self.o.save_changed_fields.call_count, 0)
 
-        # Make sure disable was called
-        urls = [x.url for x in m.request_history]
-        self.assertIn("http://voltha_url:1234/api/v1/devices/123/disable", urls)
+            # Make sure disable was called
+            urls = [x.url for x in m.request_history]
+            self.assertIn("http://voltha_url:1234/api/v1/devices/123/disable", urls)
 
     @requests_mock.Mocker()
     def test_sync_record_deactivate_already_inactive(self, m):
@@ -375,11 +410,23 @@
 
         m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.voltha_devices_response, additional_matcher=functools.partial(match_json, expected_conf))
 
-        self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+        with patch.object(TechnologyProfile.objects, "get") as tp_mock:
+            tp_mock.return_value = self.tp
 
-        # No saves as state has not changed (will eventually be saved by synchronizer framework to update backend_status)
-        self.assertEqual(self.o.save.call_count, 0)
-        self.assertEqual(self.o.save_changed_fields.call_count, 0)
+            self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+
+            # No saves as state has not changed (will eventually be saved by synchronizer framework
+            # to update backend_status)
+            self.assertEqual(self.o.save.call_count, 0)
+            self.assertEqual(self.o.save_changed_fields.call_count, 0)
+
+    def test_do_not_sync_without_tech_profile(self):
+        self.o.technology = "xgspon"
+        with self.assertRaises(DeferredException) as e:
+
+            self.sync_step(model_accessor=self.model_accessor).sync_record(self.o)
+
+        self.assertEqual(e.exception.message, "Waiting for a TechnologyProfile (technology=xgspon) to be synchronized")
 
     @requests_mock.Mocker()
     def test_delete_record(self, m):