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):