VOL-63: Image Download and Image Update support
- download image
- get image download status
- cancel image download
- list all image downloads
- activate image update
- revert image update
- itest added
Change-Id: I95a5f76071679c8787b2f775de24c96d4e7d462f
diff --git a/cli/device.py b/cli/device.py
index 6c7eea2..6b9f281 100644
--- a/cli/device.py
+++ b/cli/device.py
@@ -23,12 +23,13 @@
from simplejson import dumps
from cli.table import print_pb_as_table, print_pb_list_as_table
-from cli.utils import print_flows, pb2dict
+from cli.utils import print_flows, pb2dict, enum2name
from voltha.protos import third_party
_ = third_party
-from voltha.protos import voltha_pb2
+from voltha.protos import voltha_pb2, common_pb2
import sys
+import json
from voltha.protos.device_pb2 import PmConfigs, PmConfig, PmGroupConfig
from google.protobuf.json_format import MessageToDict
@@ -343,3 +344,233 @@
print_pb_list_as_table('Software Images:', device.images.image,
omit_fields, self.poutput, show_nulls=True)
+ @options([
+ make_option('-u', '--url', action='store', dest='url',
+ help="URL to get sw image"),
+ make_option('-n', '--name', action='store', dest='name',
+ help="Image name"),
+ make_option('-c', '--crc', action='store', dest='crc',
+ help="CRC code to verify with", default=0),
+ make_option('-v', '--version', action='store', dest='version',
+ help="Image version", default=0),
+ ])
+ def do_img_dnld_request(self, line, opts):
+ """
+ Request image download to a device
+ """
+ device = self.get_device(depth=-1)
+ self.poutput('device_id {}'.format(device.id))
+ self.poutput('name {}'.format(opts.name))
+ self.poutput('url {}'.format(opts.url))
+ self.poutput('crc {}'.format(opts.crc))
+ self.poutput('version {}'.format(opts.version))
+ try:
+ device_id = device.id
+ if device_id and opts.name and opts.url:
+ kw = dict(id=device_id)
+ kw['name'] = opts.name
+ kw['url'] = opts.url
+ else:
+ self.poutput('Device ID and URL are needed')
+ raise Exception('Device ID and URL are needed')
+ except Exception as e:
+ self.poutput('Error request img dnld {}. Error:{}'.format(device_id, e))
+ return
+ kw['crc'] = long(opts.crc)
+ kw['image_version'] = opts.version
+ response = None
+ try:
+ request = voltha_pb2.ImageDownload(**kw)
+ stub = self.get_stub()
+ response = stub.DownloadImage(request)
+ except Exception as e:
+ self.poutput('Error download image {}. Error:{}'.format(kw['id'], e))
+ return
+ name = enum2name(common_pb2.OperationResp,
+ 'OperationReturnCode', response.code)
+ self.poutput('response: {}'.format(name))
+ self.poutput('{}'.format(response))
+
+ @options([
+ make_option('-n', '--name', action='store', dest='name',
+ help="Image name"),
+ ])
+ def do_img_dnld_status(self, line, opts):
+ """
+ Get a image download status
+ """
+ device = self.get_device(depth=-1)
+ self.poutput('device_id {}'.format(device.id))
+ self.poutput('name {}'.format(opts.name))
+ try:
+ device_id = device.id
+ if device_id and opts.name:
+ kw = dict(id=device_id)
+ kw['name'] = opts.name
+ else:
+ self.poutput('Device ID, Image Name are needed')
+ raise Exception('Device ID, Image Name are needed')
+ except Exception as e:
+ self.poutput('Error get img dnld status {}. Error:{}'.format(device_id, e))
+ return
+ status = None
+ try:
+ img_dnld = voltha_pb2.ImageDownload(**kw)
+ stub = self.get_stub()
+ status = stub.GetImageDownloadStatus(img_dnld)
+ except Exception as e:
+ self.poutput('Error get img dnld status {}. Error:{}'.format(device_id, e))
+ return
+ fields_to_omit = {
+ 'crc',
+ 'local_dir',
+ }
+ try:
+ print_pb_as_table('ImageDownload Status:', status, fields_to_omit, self.poutput)
+ except Exception, e:
+ self.poutput('Error {}. Error:{}'.format(device_id, e))
+
+ def do_img_dnld_list(self, line):
+ """
+ List all image download records for a given device
+ """
+ device = self.get_device(depth=-1)
+ device_id = device.id
+ self.poutput('Get all img dnld records {}'.format(device_id))
+ try:
+ stub = self.get_stub()
+ img_dnlds = stub.ListImageDownloads(voltha_pb2.ID(id=device_id))
+ except Exception, e:
+ self.poutput('Error list img dnlds {}. Error:{}'.format(device_id, e))
+ return
+ fields_to_omit = {
+ 'crc',
+ 'local_dir',
+ }
+ try:
+ print_pb_list_as_table('ImageDownloads:', img_dnlds.items, fields_to_omit, self.poutput)
+ except Exception, e:
+ self.poutput('Error {}. Error:{}'.format(device_id, e))
+
+
+ @options([
+ make_option('-n', '--name', action='store', dest='name',
+ help="Image name"),
+ ])
+ def do_img_dnld_cancel(self, line, opts):
+ """
+ Cancel a requested image download
+ """
+ device = self.get_device(depth=-1)
+ self.poutput('device_id {}'.format(device.id))
+ self.poutput('name {}'.format(opts.name))
+ device_id = device.id
+ try:
+ if device_id and opts.name:
+ kw = dict(id=device_id)
+ kw['name'] = opts.name
+ else:
+ self.poutput('Device ID, Image Name are needed')
+ raise Exception('Device ID, Image Name are needed')
+ except Exception as e:
+ self.poutput('Error cancel sw dnld {}. Error:{}'.format(device_id, e))
+ return
+ response = None
+ try:
+ img_dnld = voltha_pb2.ImageDownload(**kw)
+ stub = self.get_stub()
+ img_dnld = stub.GetImageDownload(img_dnld)
+ response = stub.CancelImageDownload(img_dnld)
+ except Exception as e:
+ self.poutput('Error cancel sw dnld {}. Error:{}'.format(device_id, e))
+ return
+ name = enum2name(common_pb2.OperationResp,
+ 'OperationReturnCode', response.code)
+ self.poutput('response: {}'.format(name))
+ self.poutput('{}'.format(response))
+
+ @options([
+ make_option('-n', '--name', action='store', dest='name',
+ help="Image name"),
+ make_option('-s', '--save', action='store', dest='save_config',
+ help="Save Config", default="True"),
+ make_option('-d', '--dir', action='store', dest='local_dir',
+ help="Image on device location"),
+ ])
+ def do_img_activate(self, line, opts):
+ """
+ Activate an image update on device
+ """
+ device = self.get_device(depth=-1)
+ device_id = device.id
+ try:
+ if device_id and opts.name and opts.local_dir:
+ kw = dict(id=device_id)
+ kw['name'] = opts.name
+ kw['local_dir'] = opts.local_dir
+ else:
+ self.poutput('Device ID, Image Name, and Location are needed')
+ raise Exception('Device ID, Image Name, and Location are needed')
+ except Exception as e:
+ self.poutput('Error activate image {}. Error:{}'.format(device_id, e))
+ return
+ kw['save_config'] = json.loads(opts.save_config.lower())
+ self.poutput('activate image update {} {} {} {}'.format( \
+ kw['id'], kw['name'],
+ kw['local_dir'], kw['save_config']))
+ response = None
+ try:
+ img_dnld = voltha_pb2.ImageDownload(**kw)
+ stub = self.get_stub()
+ img_dnld = stub.GetImageDownload(img_dnld)
+ response = stub.ActivateImageUpdate(img_dnld)
+ except Exception as e:
+ self.poutput('Error activate image {}. Error:{}'.format(kw['id'], e))
+ return
+ name = enum2name(common_pb2.OperationResp,
+ 'OperationReturnCode', response.code)
+ self.poutput('response: {}'.format(name))
+ self.poutput('{}'.format(response))
+
+ @options([
+ make_option('-n', '--name', action='store', dest='name',
+ help="Image name"),
+ make_option('-s', '--save', action='store', dest='save_config',
+ help="Save Config", default="True"),
+ make_option('-d', '--dir', action='store', dest='local_dir',
+ help="Image on device location"),
+ ])
+ def do_img_revert(self, line, opts):
+ """
+ Revert an image update on device
+ """
+ device = self.get_device(depth=-1)
+ device_id = device.id
+ try:
+ if device_id and opts.name and opts.local_dir:
+ kw = dict(id=device_id)
+ kw['name'] = opts.name
+ kw['local_dir'] = opts.local_dir
+ else:
+ self.poutput('Device ID, Image Name, and Location are needed')
+ raise Exception('Device ID, Image Name, and Location are needed')
+ except Exception as e:
+ self.poutput('Error revert image {}. Error:{}'.format(device_id, e))
+ return
+ kw['save_config'] = json.loads(opts.save_config.lower())
+ self.poutput('revert image update {} {} {} {}'.format( \
+ kw['id'], kw['name'],
+ kw['local_dir'], kw['save_config']))
+ response = None
+ try:
+ img_dnld = voltha_pb2.ImageDownload(**kw)
+ stub = self.get_stub()
+ img_dnld = stub.GetImageDownload(img_dnld)
+ response = stub.RevertImageUpdate(img_dnld)
+ except Exception as e:
+ self.poutput('Error revert image {}. Error:{}'.format(kw['id'], e))
+ return
+ name = enum2name(common_pb2.OperationResp,
+ 'OperationReturnCode', response.code)
+ self.poutput('response: {}'.format(name))
+ self.poutput('{}'.format(response))
diff --git a/cli/utils.py b/cli/utils.py
index 5ca0da5..105931b 100644
--- a/cli/utils.py
+++ b/cli/utils.py
@@ -159,3 +159,8 @@
def dict2line(d):
assert isinstance(d, dict)
return ', '.join('{}: {}'.format(k, v) for k, v in sorted(d.items()))
+
+def enum2name(msg_obj, enum_type, enum_value):
+ descriptor = msg_obj.DESCRIPTOR.enum_types_by_name[enum_type]
+ name = descriptor.values_by_number[enum_value].name
+ return name
diff --git a/tests/itests/voltha/test_voltha_image_download_update.py b/tests/itests/voltha/test_voltha_image_download_update.py
new file mode 100644
index 0000000..64780ff
--- /dev/null
+++ b/tests/itests/voltha/test_voltha_image_download_update.py
@@ -0,0 +1,196 @@
+from unittest import main
+from time import time, sleep
+import simplejson, jsonschema
+from google.protobuf.json_format import MessageToDict, \
+ MessageToJson
+from tests.itests.voltha.rest_base import RestBase
+from common.utils.consulhelpers import get_endpoint_from_consul
+from voltha.protos.device_pb2 import Device, ImageDownload
+from voltha.protos.common_pb2 import AdminState
+from google.protobuf.empty_pb2 import Empty
+
+LOCAL_CONSUL = "localhost:8500"
+
+class VolthaImageDownloadUpdate(RestBase):
+ # Retrieve details on the REST entry point
+ rest_endpoint = get_endpoint_from_consul(LOCAL_CONSUL, 'chameleon-rest')
+
+ # Construct the base_url
+ base_url = 'https://' + rest_endpoint
+
+ def wait_till(self, msg, predicate, interval=0.1, timeout=5.0):
+ deadline = time() + timeout
+ while time() < deadline:
+ if predicate():
+ return
+ sleep(interval)
+ self.fail('Timed out while waiting for condition: {}'.format(msg))
+
+ def setUp(self):
+ # Make sure the Voltha REST interface is available
+ self.verify_rest()
+ # Create a new device
+ device = self.add_device()
+ # Activate the new device
+ self.activate_device(device['id'])
+ self.device_id = device['id']
+ print("self.device_id {}".format(self.device_id))
+ assert(self.device_id)
+
+ # wait untill device moves to ACTIVE state
+ self.wait_till(
+ 'admin state moves from ACTIVATING to ACTIVE',
+ lambda: self.get('/api/v1/devices/{}'.format(self.device_id))\
+ ['oper_status'] in ('ACTIVE'),
+ timeout=5.0)
+ # wait until ONUs are detected
+ sleep(2.0)
+
+ def tearDown(self):
+ # Disable device
+ #self.disable_device(self.device_id)
+ # Delete device
+ #self.delete_device(self.device_id)
+ pass
+
+ # test cases
+
+ def test_voltha_global_download_image(self):
+ name = 'image-1'
+ self.request_download_image(name)
+ self.verify_request_download_image(name)
+ self.cancel_download_image(name)
+ self.verify_list_download_images(0)
+
+ name = 'image-2'
+ self.request_download_image(name)
+ self.verify_request_download_image(name)
+ self.get_download_image_status(name)
+ self.verify_successful_download_image(name)
+ self.activate_image(name)
+ self.verify_activate_image(name)
+ self.revert_image(name)
+ self.verify_revert_image(name)
+
+ name = 'image-3'
+ self.request_download_image(name)
+ self.verify_request_download_image(name)
+ self.verify_list_download_images(2)
+
+ def verify_list_download_images(self, num_of_images):
+ path = '/api/v1/devices/{}/image_downloads' \
+ .format(self.device_id)
+ res = self.get(path)
+ print(res['items'])
+ self.assertEqual(len(res['items']), num_of_images)
+
+ def get_download_image(self, name):
+ path = '/api/v1/devices/{}/image_downloads/{}' \
+ .format(self.device_id, name)
+ response = self.get(path)
+ print(response)
+ return response
+
+ def request_download_image(self, name):
+ path = '/api/v1/devices/{}/image_downloads/{}' \
+ .format(self.device_id, name)
+ url='http://[user@](hostname)[:port]/(dir)/(filename)'
+ request = ImageDownload(id=self.device_id,
+ name=name,
+ image_version="1.1.2",
+ url=url)
+ self.post(path, MessageToDict(request),
+ expected_code=200)
+
+ def verify_request_download_image(self, name):
+ res = self.get_download_image(name)
+ self.assertEqual(res['state'], 'DOWNLOAD_REQUESTED')
+ self.assertEqual(res['image_state'], 'IMAGE_UNKNOWN')
+ path = '/api/v1/local/devices/{}'.format(self.device_id)
+ device = self.get(path)
+ self.assertEqual(device['admin_state'], 'DOWNLOADING_IMAGE')
+
+ def cancel_download_image(self, name):
+ path = '/api/v1/devices/{}/image_downloads/{}' \
+ .format(self.device_id, name)
+ self.delete(path, expected_code=200)
+
+ def get_download_image_status(self, name):
+ path = '/api/v1/devices/{}/image_downloads/{}/status' \
+ .format(self.device_id, name)
+ response = self.get(path)
+ while (response['state'] != 'DOWNLOAD_SUCCEEDED'):
+ response = self.get(path)
+
+ def verify_successful_download_image(self, name):
+ res = self.get_download_image(name)
+ self.assertEqual(res['state'], 'DOWNLOAD_SUCCEEDED')
+ self.assertEqual(res['image_state'], 'IMAGE_UNKNOWN')
+ path = '/api/v1/local/devices/{}'.format(self.device_id)
+ device = self.get(path)
+ self.assertEqual(device['admin_state'], 'ENABLED')
+
+ def activate_image(self, name):
+ path = '/api/v1/devices/{}/image_downloads/{}/image_update' \
+ .format(self.device_id, name)
+ request = ImageDownload(id=self.device_id,
+ name=name,
+ save_config=True,
+ local_dir='/local/images/v.1.1.run')
+ self.post(path, MessageToDict(request),
+ expected_code=200)
+
+ def verify_activate_image(self, name):
+ res = self.get_download_image(name)
+ self.assertEqual(res['image_state'], 'IMAGE_ACTIVE')
+
+ def revert_image(self, name):
+ path = '/api/v1/devices/{}/image_downloads/{}/image_revert' \
+ .format(self.device_id, name)
+ request = ImageDownload(id=self.device_id,
+ name=name,
+ save_config=True,
+ local_dir='/local/images/v.1.1.run')
+ self.post(path, MessageToDict(request),
+ expected_code=200)
+
+ def verify_revert_image(self, name):
+ res = self.get_download_image(name)
+ self.assertEqual(res['image_state'], 'IMAGE_INACTIVE')
+
+
+ # test helpers
+
+ def verify_rest(self):
+ self.get('/api/v1')
+
+ # Create a new simulated device
+ def add_device(self):
+ device = Device(
+ type='simulated_olt',
+ )
+ device = self.post('/api/v1/local/devices', MessageToDict(device),
+ expected_code=200)
+ return device
+
+ # Active the simulated device.
+ def activate_device(self, device_id):
+ path = '/api/v1/local/devices/{}'.format(device_id)
+ self.post(path + '/enable', expected_code=200)
+ device = self.get(path)
+ self.assertEqual(device['admin_state'], 'ENABLED')
+
+ # Disable the simulated device.
+ def disable_device(self, device_id):
+ path = '/api/v1/local/devices/{}'.format(device_id)
+ self.post(path + '/disable', expected_code=200)
+ device = self.get(path)
+ self.assertEqual(device['admin_state'], 'DISABLED')
+
+ # Delete the simulated device
+ def delete_device(self, device_id):
+ path = '/api/v1/local/devices/{}'.format(device_id)
+ self.delete(path + '/delete', expected_code=200)
+
+if __name__ == '__main__':
+ main()
diff --git a/voltha/adapters/adtran_olt/adtran_olt.py b/voltha/adapters/adtran_olt/adtran_olt.py
index 96989ed..67ac4f2 100644
--- a/voltha/adapters/adtran_olt/adtran_olt.py
+++ b/voltha/adapters/adtran_olt/adtran_olt.py
@@ -207,6 +207,28 @@
reactor.callLater(0, self.devices_handlers[device.id].reboot)
return device
+ def download_image(self, device, request):
+ log.info('image_download', device=device, request=request)
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ log.info('get_image_download', device=device, request=request)
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ log.info('cancel_image_download', device=device)
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ log.info('activate_image_update', device=device, \
+ request=request)
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ log.info('revert_image_update', device=device, \
+ request=request)
+ raise NotImplementedError()
+
def self_test_device(self, device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/adapters/broadcom_onu/broadcom_onu.py b/voltha/adapters/broadcom_onu/broadcom_onu.py
index 732085c..86f82ad 100644
--- a/voltha/adapters/broadcom_onu/broadcom_onu.py
+++ b/voltha/adapters/broadcom_onu/broadcom_onu.py
@@ -112,6 +112,21 @@
def reboot_device(self, device):
raise NotImplementedError()
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def self_test_device(self, device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/adapters/dpoe_onu/dpoe_onu.py b/voltha/adapters/dpoe_onu/dpoe_onu.py
index c261490..09e7f6e 100644
--- a/voltha/adapters/dpoe_onu/dpoe_onu.py
+++ b/voltha/adapters/dpoe_onu/dpoe_onu.py
@@ -217,6 +217,21 @@
def reboot_device(self, device):
raise NotImplementedError()
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def self_test_device(self, device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/adapters/iadapter.py b/voltha/adapters/iadapter.py
index dcb373b..eda32f9 100644
--- a/voltha/adapters/iadapter.py
+++ b/voltha/adapters/iadapter.py
@@ -100,6 +100,21 @@
reactor.callLater(0, self.devices_handlers[device.id].reboot)
return device
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def self_test_device(self, device):
log.info('self-test-req', device_id=device.id)
result = reactor.callLater(0, self.devices_handlers[device.id].self_test_device)
diff --git a/voltha/adapters/interface.py b/voltha/adapters/interface.py
index 1f2da87..c2431fb 100644
--- a/voltha/adapters/interface.py
+++ b/voltha/adapters/interface.py
@@ -127,6 +127,66 @@
:return: (Deferred) Shall be fired to acknowledge the reboot.
"""
+ def download_image(device, request):
+ """
+ This is called to request downloading a specified image into
+ the standby partition of a device based on a NBI call.
+ This call is expected to be non-blocking.
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) Shall be fired to acknowledge the download.
+ """
+
+ def get_image_download_status(device, request):
+ """
+ This is called to inquire about a requested image download
+ status based on a NBI call.
+ The adapter is expected to update the DownloadImage DB object
+ with the query result
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) Shall be fired to acknowledge
+ """
+
+ def cancel_image_download(device, request):
+ """
+ This is called to cancel a requested image download
+ based on a NBI call. The admin state of the device will not
+ change after the download.
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) Shall be fired to acknowledge
+ """
+
+ def activate_image_update(device, request):
+ """
+ This is called to activate a downloaded image from
+ a standby partition into active partition.
+ Depending on the device implementation, this call
+ may or may not cause device reboot.
+ If no reboot, then a reboot is required to make the
+ activated image running on device
+ This call is expected to be non-blocking.
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) OperationResponse object.
+ """
+
+ def revert_image_update(device, request):
+ """
+ This is called to deactivate the specified image at
+ active partition, and revert to previous image at
+ standby partition.
+ Depending on the device implementation, this call
+ may or may not cause device reboot.
+ If no reboot, then a reboot is required to make the
+ previous image running on device
+ This call is expected to be non-blocking.
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) OperationResponse object.
+ """
+
def self_test_device(device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/adapters/maple_olt/maple_olt.py b/voltha/adapters/maple_olt/maple_olt.py
index 528ae01..03f7961 100644
--- a/voltha/adapters/maple_olt/maple_olt.py
+++ b/voltha/adapters/maple_olt/maple_olt.py
@@ -445,6 +445,21 @@
def reboot_device(self, device):
raise NotImplementedError()
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def self_test_device(self, device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/adapters/microsemi_olt/microsemi_olt.py b/voltha/adapters/microsemi_olt/microsemi_olt.py
index 64c6d9a..a04a712 100644
--- a/voltha/adapters/microsemi_olt/microsemi_olt.py
+++ b/voltha/adapters/microsemi_olt/microsemi_olt.py
@@ -123,6 +123,21 @@
def reboot_device(self, device):
raise NotImplementedError()
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def self_test_device(self, device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/adapters/pmcs_onu/pmcs_onu.py b/voltha/adapters/pmcs_onu/pmcs_onu.py
index 2da0e10..aedbb99 100644
--- a/voltha/adapters/pmcs_onu/pmcs_onu.py
+++ b/voltha/adapters/pmcs_onu/pmcs_onu.py
@@ -114,6 +114,21 @@
def reboot_device(self, device):
raise NotImplementedError()
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def self_test_device(self, device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/adapters/simulated_olt/simulated_olt.py b/voltha/adapters/simulated_olt/simulated_olt.py
index 465efef..0b6eb94 100644
--- a/voltha/adapters/simulated_olt/simulated_olt.py
+++ b/voltha/adapters/simulated_olt/simulated_olt.py
@@ -38,7 +38,8 @@
from voltha.core.logical_device_agent import mac_str_to_tuple
from voltha.protos.adapter_pb2 import Adapter, AdapterConfig
from voltha.protos.device_pb2 import DeviceType, DeviceTypes, Device, Port, \
-PmConfigs, PmConfig, PmGroupConfig, Image
+ PmConfigs, PmConfig, PmGroupConfig, Image, ImageDownload
+from voltha.protos.common_pb2 import OperationResp
from voltha.protos.voltha_pb2 import SelfTestResponse
from voltha.protos.events_pb2 import KpiEvent, KpiEventType, MetricValuePairs
from voltha.protos.health_pb2 import HealthStatus
@@ -236,6 +237,79 @@
def delete_device(self, device):
raise NotImplementedError()
+ def download_image(self, device, request):
+ log.info('download-image', device=device, request=request)
+ try:
+ # initiate requesting software download to device
+ log.info('device.image_downloads', img_dnld=device.image_downloads)
+ pass
+ except Exception as e:
+ log.exception(e.message)
+
+ def get_image_download_status(self, device, request):
+ log.info('get-image-download-status', device=device,\
+ request=request)
+ try:
+ download_completed = False
+ # initiate query for progress of download to device
+ request.state = random.choice([ImageDownload.DOWNLOAD_SUCCEEDED,
+ ImageDownload.DOWNLOAD_STARTED,
+ ImageDownload.DOWNLOAD_FAILED])
+ if request.state != ImageDownload.DOWNLOAD_STARTED:
+ download_completed = True
+ request.downloaded_bytes = random.choice(range(1024,65536))
+ # update status based on query output
+ self.adapter_agent.update_image_download(request)
+ if download_completed == True:
+ # restore admin state to enabled
+ device.admin_state = AdminState.ENABLED
+ self.adapter_agent.update_device(device)
+ # TODO:
+ # the device admin state will also restore
+ # when adapter receiving event notification
+ # this will be handled in event handler
+ except Exception as e:
+ log.exception(e.message)
+
+ def cancel_image_download(self, device, request):
+ log.info('cancel-sw-download', device=device,
+ request=request)
+ try:
+ # intiate cancelling software download to device
+ # at success delete image download record
+ self.adapter_agent.delete_image_download(request)
+ # restore admin state to enabled
+ device.admin_state = AdminState.ENABLED
+ self.adapter_agent.update_device(device)
+ except Exception as e:
+ log.exception(e.message)
+
+ def activate_image_update(self, device, request):
+ log.info('activate-image-update', device=device, request=request)
+ try:
+ # initiate activating software update to device
+ # at succcess, update image state
+ request.image_state = ImageDownload.IMAGE_ACTIVE
+ self.adapter_agent.update_image_download(request)
+ # restore admin state to enabled
+ device.admin_state = AdminState.ENABLED
+ self.adapter_agent.update_device(device)
+ except Exception as e:
+ log.exception(e.message)
+
+ def revert_image_update(self, device, request):
+ log.info('revert-image-updade', device=device, request=request)
+ try:
+ # initiate reverting software update to device
+ # at succcess, update image state
+ request.image_state = ImageDownload.IMAGE_INACTIVE
+ self.adapter_agent.update_image_download(request)
+ # restore admin state to enabled
+ device.admin_state = AdminState.ENABLED
+ self.adapter_agent.update_device(device)
+ except Exception as e:
+ log.exception(e.message)
+
def get_device_details(self, device):
raise NotImplementedError()
diff --git a/voltha/adapters/simulated_onu/simulated_onu.py b/voltha/adapters/simulated_onu/simulated_onu.py
index 7a27e45..c61ba8a 100644
--- a/voltha/adapters/simulated_onu/simulated_onu.py
+++ b/voltha/adapters/simulated_onu/simulated_onu.py
@@ -109,6 +109,21 @@
def reboot_device(self, device):
raise NotImplementedError()
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def delete_device(self, device):
raise NotImplementedError()
diff --git a/voltha/adapters/tibit_olt/tibit_olt.py b/voltha/adapters/tibit_olt/tibit_olt.py
index cb90ee7..da3269e 100644
--- a/voltha/adapters/tibit_olt/tibit_olt.py
+++ b/voltha/adapters/tibit_olt/tibit_olt.py
@@ -34,8 +34,6 @@
from scapy.fields import XLongField, StrFixedLenField, XIntField, \
FieldLenField, StrLenField, IntField
-
-
from twisted.internet import reactor
from twisted.internet.defer import DeferredQueue, inlineCallbacks
from twisted.internet.task import LoopingCall
@@ -862,6 +860,21 @@
connect_status=ConnectStatus.REACHABLE)
log.info('OLT Rebooted: {}'.format(device.mac_address))
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def self_test_device(self, device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/adapters/tibit_onu/tibit_onu.py b/voltha/adapters/tibit_onu/tibit_onu.py
index cd67b05..137f572 100644
--- a/voltha/adapters/tibit_onu/tibit_onu.py
+++ b/voltha/adapters/tibit_onu/tibit_onu.py
@@ -369,6 +369,21 @@
log.info('ONU Rebooted: {}'.format(device.mac_address))
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
def self_test_device(self, device):
"""
This is called to Self a device based on a NBI call.
diff --git a/voltha/core/adapter_agent.py b/voltha/core/adapter_agent.py
index 07b9510..9fdc563 100644
--- a/voltha/core/adapter_agent.py
+++ b/voltha/core/adapter_agent.py
@@ -169,6 +169,21 @@
def reboot_device(self, device):
return self.adapter.reboot_device(device)
+ def download_image(self, device, request):
+ return self.adapter.download_image(device, request)
+
+ def get_image_download_status(self, device, request):
+ return self.adapter.get_image_download_status(device, request)
+
+ def cancel_image_download(self, device, request):
+ return self.adapter.cancel_image_download(device, request)
+
+ def activate_image_update(self, device, request):
+ return self.adapter.activate_image_update(device, request)
+
+ def revert_image_update(self, device, request):
+ return self.adapter.revert_image_update(device, request)
+
def self_test(self, device):
return self.adapter.self_test_device(device)
@@ -294,6 +309,29 @@
device = self.get_device(device_id)
self.adapter.update_pm_config(device, device_pm_config)
+ def update_image_download(self, img_dnld):
+ self.log.info('update-image-download', img_dnld=img_dnld)
+ try:
+ # we run the update through the device_agent so that the change
+ # does not loop back to the adapter unnecessarily
+ device_agent = self.core.get_device_agent(img_dnld.id)
+ device_agent.update_device_image_download(img_dnld)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ def delete_image_download(self, img_dnld):
+ self.log.info('delete-image-download', img_dnld=img_dnld)
+ try:
+ root_proxy = self.core.get_proxy('/')
+ path = '/devices/{}/image_downloads/{}'.\
+ format(img_dnld.id, img_dnld.name)
+ root_proxy.get(path)
+ root_proxy.remove(path)
+ device_agent = self.core.get_device_agent(img_dnld.id)
+ device_agent.unregister_device_image_download(img_dnld.name)
+ except Exception as e:
+ self.log.exception(e.message)
+
def _add_peer_reference(self, device_id, port):
# for referential integrity, add/augment references
port.device_id = device_id
diff --git a/voltha/core/device_agent.py b/voltha/core/device_agent.py
index 0de15bc..64e5d69 100644
--- a/voltha/core/device_agent.py
+++ b/voltha/core/device_agent.py
@@ -23,7 +23,9 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from voltha.core.config.config_proxy import CallbackType
-from voltha.protos.common_pb2 import AdminState, OperStatus, ConnectStatus
+from voltha.protos.common_pb2 import AdminState, OperStatus, ConnectStatus, \
+ OperationResp
+from voltha.protos.device_pb2 import ImageDownload
from voltha.registry import registry
from voltha.protos.openflow_13_pb2 import Flows, FlowGroups
@@ -48,6 +50,8 @@
self.pm_config_proxy = core.get_proxy(
'/devices/{}/pm_configs'.format(initial_data.id))
+ self.img_dnld_proxies = {}
+
self.proxy.register_callback(
CallbackType.PRE_UPDATE, self._validate_update)
self.proxy.register_callback(
@@ -104,6 +108,98 @@
if not dry_run:
yield self.adapter_agent.reboot_device(device)
+ def register_image_download(self, request):
+ try:
+ self.log.debug('register-image-download', request=request)
+ path = '/devices/{}/image_downloads/{}'.format(request.id, request.name)
+ self.img_dnld_proxies[request.name] = self.core.get_proxy(path)
+ self.img_dnld_proxies[request.name].register_callback(
+ CallbackType.POST_UPDATE, self._update_image)
+ # trigger update callback
+ request.state = ImageDownload.DOWNLOAD_REQUESTED
+ self.img_dnld_proxies[request.name].update('/', request)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ def activate_image_update(self, request):
+ try:
+ self.log.debug('activate-image-download', request=request)
+ request.image_state = ImageDownload.IMAGE_ACTIVATE
+ self.img_dnld_proxies[request.name].update('/', request)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ def revert_image_update(self, request):
+ try:
+ self.log.debug('revert-image-download', request=request)
+ request.image_state = ImageDownload.IMAGE_REVERT
+ self.img_dnld_proxies[request.name].update('/', request)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ @inlineCallbacks
+ def _download_image(self, device, img_dnld):
+ try:
+ self.log.debug('download-image', img_dnld=img_dnld)
+ yield self.adapter_agent.download_image(device, img_dnld)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ def get_image_download_status(self, request):
+ try:
+ self.log.debug('get-image-download-status',
+ request=request)
+ device = self.proxy.get('/')
+ self.adapter_agent.get_image_download_status(device, request)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ def cancel_image_download(self, img_dnld):
+ try:
+ self.log.debug('cancel-image-download',
+ img_dnld=img_dnld)
+ device = self.proxy.get('/')
+ self.adapter_agent.cancel_image_download(device, img_dnld)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ def update_device_image_download(self, img_dnld):
+ try:
+ self.log.debug('update-device-image-download',
+ img_dnld=img_dnld)
+ self.proxy.update('/image_downloads/{}'\
+ .format(img_dnld.name), img_dnld)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ def unregister_device_image_download(self, name):
+ try:
+ self.log.debug('unregister-device-image-download',
+ name=name)
+ self.self_proxies[name].unregister_callback(
+ CallbackType.POST_ADD, self._download_image)
+ self.self_proxies[name].unregister_callback(
+ CallbackType.POST_UPDATE, self._process_image)
+ except Exception as e:
+ self.log.exception(e.message)
+
+ @inlineCallbacks
+ def _update_image(self, img_dnld):
+ try:
+ self.log.debug('update-image', img_dnld=img_dnld)
+ # handle download
+ if img_dnld.state == ImageDownload.DOWNLOAD_REQUESTED:
+ device = self.proxy.get('/')
+ yield self._download_image(device, img_dnld)
+ if img_dnld.image_state == ImageDownload.IMAGE_ACTIVATE:
+ device = self.proxy.get('/')
+ yield self.adapter_agent.activate_image_update(device, img_dnld)
+ elif img_dnld.image_state == ImageDownload.IMAGE_REVERT:
+ device = self.proxy.get('/')
+ yield self.adapter_agent.revert_image_update(device, img_dnld)
+ except Exception as e:
+ self.log.exception(e.message)
+
@inlineCallbacks
def self_test(self, device, dry_run=False):
self.log.debug('self-test-device', device=device, dry_run=dry_run)
@@ -267,6 +363,7 @@
(AdminState.PREPROVISIONED, AdminState.UNKNOWN): False,
(AdminState.PREPROVISIONED, AdminState.ENABLED): _activate_device,
+ (AdminState.PREPROVISIONED, AdminState.DOWNLOADING_IMAGE): False,
(AdminState.ENABLED, AdminState.UNKNOWN): False,
(AdminState.ENABLED, AdminState.ENABLED): _propagate_change,
@@ -275,7 +372,10 @@
(AdminState.DISABLED, AdminState.UNKNOWN): False,
(AdminState.DISABLED, AdminState.PREPROVISIONED): _abandon_device,
- (AdminState.DISABLED, AdminState.ENABLED): _reenable_device
+ (AdminState.DISABLED, AdminState.ENABLED): _reenable_device,
+ (AdminState.DISABLED, AdminState.DOWNLOADING_IMAGE): False,
+
+ (AdminState.DOWNLOADING_IMAGE, AdminState.DISABLED): False
}
diff --git a/voltha/core/global_handler.py b/voltha/core/global_handler.py
index ef32bcd..f626cc8 100644
--- a/voltha/core/global_handler.py
+++ b/voltha/core/global_handler.py
@@ -22,7 +22,9 @@
from common.utils.id_generation import \
create_cluster_id, create_empty_broadcast_id
from voltha.core.config.config_root import ConfigRoot
-from voltha.protos.device_pb2 import PmConfigs, Images
+from voltha.protos.device_pb2 import PmConfigs, Images, \
+ ImageDownload, ImageDownloads
+from voltha.protos.common_pb2 import OperationResp
from voltha.protos.voltha_pb2 import \
add_VolthaGlobalServiceServicer_to_server, VolthaLocalServiceStub, \
VolthaGlobalServiceServicer, Voltha, VolthaInstances, VolthaInstance, \
@@ -1522,3 +1524,158 @@
else:
log.info('grpc-success-response', response=response)
returnValue(response)
+
+ @twisted_async
+ @inlineCallbacks
+ def DownloadImage(self, request, context):
+ try:
+ log.info('grpc-request', request=request)
+ response = yield self.dispatcher.dispatch('DownloadImage',
+ request,
+ context,
+ id=request.id)
+ log.info('grpc-response', response=response)
+ except Exception as e:
+ log.exception('grpc-exception', e=e)
+
+ if isinstance(response, DispatchError):
+ log.info('grpc-error-response', error=response.error_code)
+ context.set_details('Device \'{}\' error'.format(request.id))
+ context.set_code(response.error_code)
+ returnValue(OperationResp(code=OperationResp.OPERATION_FAILURE))
+ else:
+ log.info('grpc-success-response', response=response)
+ returnValue(response)
+
+ @twisted_async
+ @inlineCallbacks
+ def GetImageDownloadStatus(self, request, context):
+ try:
+ log.info('grpc-request', request=request)
+ response = yield self.dispatcher.dispatch('GetImageDownloadStatus',
+ request,
+ context,
+ id=request.id)
+ log.info('grpc-response', response=response)
+ except Exception as e:
+ log.exception('grpc-exception', e=e)
+
+ if isinstance(response, DispatchError):
+ log.info('grpc-error-response', error=response.error_code)
+ context.set_details('Device \'{}\' error'.format(request.id))
+ context.set_code(response.error_code)
+ returnValue(ImageDownloads())
+ else:
+ log.info('grpc-success-response', response=response)
+ returnValue(response)
+
+ @twisted_async
+ @inlineCallbacks
+ def GetImageDownload(self, request, context):
+ try:
+ log.info('grpc-request', request=request)
+ response = yield self.dispatcher.dispatch('GetImageDownload',
+ request,
+ context,
+ id=request.id)
+ log.info('grpc-response', response=response)
+ except Exception as e:
+ log.exception('grpc-exception', e=e)
+
+ if isinstance(response, DispatchError):
+ log.info('grpc-error-response', error=response.error_code)
+ context.set_details('Device \'{}\' error'.format(request.id))
+ context.set_code(response.error_code)
+ returnValue(ImageDownload())
+ else:
+ log.info('grpc-success-response', response=response)
+ returnValue(response)
+
+ @twisted_async
+ @inlineCallbacks
+ def ListImageDownloads(self, request, context):
+ try:
+ log.info('grpc-request', request=request)
+ response = yield self.dispatcher.dispatch('ListImageDownloads',
+ request,
+ context,
+ id=request.id)
+ log.info('grpc-response', response=response)
+ except Exception as e:
+ log.exception('grpc-exception', e=e)
+
+ if isinstance(response, DispatchError):
+ log.info('grpc-error-response', error=response.error_code)
+ context.set_details('Device \'{}\' error'.format(request.id))
+ context.set_code(response.error_code)
+ returnValue(ImageDownloads())
+ else:
+ log.info('grpc-success-response', response=response)
+ returnValue(response)
+
+
+ @twisted_async
+ @inlineCallbacks
+ def CancelImageDownload(self, request, context):
+ try:
+ log.info('grpc-request', request=request)
+ response = yield self.dispatcher.dispatch('CancelImageDownload',
+ request,
+ context,
+ id=request.id)
+ log.info('grpc-response', response=response)
+ except Exception as e:
+ log.exception('grpc-exception', e=e)
+
+ if isinstance(response, DispatchError):
+ log.info('grpc-error-response', error=response.error_code)
+ context.set_details('Device \'{}\' error'.format(request.id))
+ context.set_code(response.error_code)
+ returnValue(OperationResp(code=OperationResp.OPERATION_FAILURE))
+ else:
+ log.info('grpc-success-response', response=response)
+ returnValue(response)
+
+ @twisted_async
+ @inlineCallbacks
+ def ActivateImageUpdate(self, request, context):
+ try:
+ log.info('grpc-request', request=request)
+ response = yield self.dispatcher.dispatch('ActivateImageUpdate',
+ request,
+ context,
+ id=request.id)
+ log.info('grpc-response', response=response)
+ except Exception as e:
+ log.exception('grpc-exception', e=e)
+
+ if isinstance(response, DispatchError):
+ log.info('grpc-error-response', error=response.error_code)
+ context.set_details('Device \'{}\' error'.format(request.id))
+ context.set_code(response.error_code)
+ returnValue(OperationResp(code=OperationResp.OPERATION_FAILURE))
+ else:
+ log.info('grpc-success-response', response=response)
+ returnValue(response)
+
+ @twisted_async
+ @inlineCallbacks
+ def RevertImageUpdate(self, request, context):
+ try:
+ log.info('grpc-request', request=request)
+ response = yield self.dispatcher.dispatch('RevertImageUpdate',
+ request,
+ context,
+ id=request.id)
+ log.info('grpc-response', response=response)
+ except Exception as e:
+ log.exception('grpc-exception', e=e)
+
+ if isinstance(response, DispatchError):
+ log.info('grpc-error-response', error=response.error_code)
+ context.set_details('Device \'{}\' error'.format(request.id))
+ context.set_code(response.error_code)
+ returnValue(OperationResp(code=OperationResp.OPERATION_FAILURE))
+ else:
+ log.info('grpc-success-response', response=response)
+ returnValue(response)
diff --git a/voltha/core/local_handler.py b/voltha/core/local_handler.py
index 3115b4d..a172e51 100644
--- a/voltha/core/local_handler.py
+++ b/voltha/core/local_handler.py
@@ -30,7 +30,8 @@
LogicalPorts, Devices, Device, DeviceType, \
DeviceTypes, DeviceGroups, DeviceGroup, AdminState, OperStatus, ChangeEvent, \
AlarmFilter, AlarmFilters, SelfTestResponse
-from voltha.protos.device_pb2 import PmConfigs, Images
+from voltha.protos.device_pb2 import PmConfigs, Images, ImageDownload, ImageDownloads
+from voltha.protos.common_pb2 import OperationResp
from voltha.registry import registry
from requests.api import request
@@ -371,7 +372,9 @@
try:
path = '/devices/{}'.format(request.id)
device = self.root.get(path)
-
+ assert device.admin_state != AdminState.DOWNLOADING_IMAGE, \
+ 'Device to reboot cannot be ' \
+ 'in admin state \'{}\''.format(device.admin_state)
agent = self.core.get_device_agent(device.id)
agent.reboot_device(device)
@@ -383,6 +386,210 @@
return Empty()
@twisted_async
+ def DownloadImage(self, request, context):
+ log.info('grpc-request', request=request)
+
+ if '/' in request.id:
+ context.set_details(
+ 'Malformed device id \'{}\''.format(request.id))
+ context.set_code(StatusCode.INVALID_ARGUMENT)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ try:
+ path = '/devices/{}'.format(request.id)
+ device = self.root.get(path)
+ assert isinstance(request, ImageDownload)
+ self.root.add('/devices/{}/image_downloads'.\
+ format(request.id), request)
+ assert device.admin_state == AdminState.ENABLED, \
+ 'Device to DOWNLOADING_IMAGE cannot be ' \
+ 'in admin state \'{}\''.format(device.admin_state)
+ device.admin_state = AdminState.DOWNLOADING_IMAGE
+ self.root.update(path, device, strict=True)
+ agent = self.core.get_device_agent(device.id)
+ agent.register_image_download(request)
+ return OperationResp(code=OperationResp.OPERATION_SUCCESS)
+
+ except AssertionError as e:
+ context.set_details(e.message)
+ context.set_code(StatusCode.INVALID_ARGUMENT)
+ return OperationResp(code=OperationResp.OPERATION_UNSUPPORTED)
+
+ except KeyError:
+ context.set_details(
+ 'Device \'{}\' not found'.format(request.id))
+ context.set_code(StatusCode.NOT_FOUND)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ except Exception as e:
+ log.exception(e.message)
+ context.set_code(StatusCode.NOT_FOUND)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ @twisted_async
+ def GetImageDownloadStatus(self, request, context):
+ log.info('grpc-request', request=request)
+
+ if '/' in request.id:
+ context.set_details(
+ 'Malformed device id \'{}\''.format(request.id))
+ context.set_code(StatusCode.INVALID_ARGUMENT)
+ response = ImageDownload(state=ImageDownload.DOWNLOAD_UNKNOWN)
+ return response
+
+ try:
+ path = '/devices/{}'.format(request.id)
+ device = self.root.get(path)
+ agent = self.core.get_device_agent(device.id)
+ img_dnld = self.root.get('/devices/{}/image_downloads/{}'.\
+ format(request.id, request.name))
+ agent.get_image_download_status(img_dnld)
+ try:
+ response = self.root.get('/devices/{}/image_downloads/{}'.\
+ format(request.id, request.name))
+ except Exception as e:
+ log.exception(e.message)
+ return response
+
+ except KeyError:
+ context.set_details(
+ 'Device \'{}\' not found'.format(request.id))
+ context.set_code(StatusCode.NOT_FOUND)
+ response = ImageDownload(state=ImageDownload.DOWNLOAD_UNKNOWN)
+ return response
+ except Exception as e:
+ log.exception(e.message)
+ response = ImageDownload(state=ImageDownload.DOWNLOAD_FAILED)
+ return response
+
+ @twisted_async
+ def GetImageDownload(self, request, context):
+ log.info('grpc-request', request=request)
+
+ if '/' in request.id:
+ context.set_details(
+ 'Malformed device id \'{}\''.format(request.id))
+ context.set_code(StatusCode.INVALID_ARGUMENT)
+ response = ImageDownload(state=ImageDownload.DOWNLOAD_UNKNOWN)
+ return response
+
+ try:
+ response = self.root.get('/devices/{}/image_downloads/{}'.\
+ format(request.id, request.name))
+ return response
+
+ except KeyError:
+ context.set_details(
+ 'Device \'{}\' not found'.format(request.id))
+ context.set_code(StatusCode.NOT_FOUND)
+ response = ImageDownload(state=ImageDownload.DOWNLOAD_UNKNOWN)
+ return response
+
+ @twisted_async
+ def ListImageDownloads(self, request, context):
+ log.info('grpc-request', request=request)
+
+ if '/' in request.id:
+ context.set_details(
+ 'Malformed device id \'{}\''.format(request.id))
+ context.set_code(StatusCode.INVALID_ARGUMENT)
+ response = ImageDownload(state=ImageDownload.DOWNLOAD_UNKNOWN)
+ return response
+
+ try:
+ response = self.root.get('/devices/{}/image_downloads'.\
+ format(request.id))
+ return ImageDownloads(items=response)
+
+ except KeyError:
+ context.set_details(
+ 'Device \'{}\' not found'.format(request.id))
+ context.set_code(StatusCode.NOT_FOUND)
+ response = ImageDownload(state=ImageDownload.DOWNLOAD_UNKNOWN)
+ return response
+
+ @twisted_async
+ def CancelImageDownload(self, request, context):
+ log.info('grpc-request', request=request)
+
+ if '/' in request.id:
+ context.set_details(
+ 'Malformed device id \'{}\''.format(request.id))
+ context.set_code(StatusCode.INVALID_ARGUMENT)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ try:
+ assert isinstance(request, ImageDownload)
+ path = '/devices/{}'.format(request.id)
+ device = self.root.get(path)
+ assert device.admin_state == AdminState.DOWNLOADING_IMAGE, \
+ 'Device to cancel DOWNLOADING_IMAGE cannot be ' \
+ 'in admin state \'{}\''.format(device.admin_state)
+ agent = self.core.get_device_agent(device.id)
+ agent.cancel_image_download(request)
+ return OperationResp(code=OperationResp.OPERATION_SUCCESS)
+
+ except KeyError:
+ context.set_details(
+ 'Device \'{}\' not found'.format(request.id))
+ context.set_code(StatusCode.NOT_FOUND)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ @twisted_async
+ def ActivateImageUpdate(self, request, context):
+ log.info('grpc-request', request=request)
+
+ if '/' in request.id:
+ context.set_details(
+ 'Malformed device id \'{}\''.format(request.id))
+ context.set_code(StatusCode.INVALID_ARGUMENT)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ try:
+ assert isinstance(request, ImageDownload)
+ path = '/devices/{}'.format(request.id)
+ device = self.root.get(path)
+ assert device.admin_state == AdminState.ENABLED, \
+ 'Device to activate image cannot be ' \
+ 'in admin state \'{}\''.format(device.admin_state)
+ agent = self.core.get_device_agent(device.id)
+ agent.activate_image_update(request)
+ return OperationResp(code=OperationResp.OPERATION_SUCCESS)
+
+ except KeyError:
+ context.set_details(
+ 'Device \'{}\' not found'.format(request.id))
+ context.set_code(StatusCode.NOT_FOUND)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ @twisted_async
+ def RevertImageUpdate(self, request, context):
+ log.info('grpc-request', request=request)
+
+ if '/' in request.id:
+ context.set_details(
+ 'Malformed device id \'{}\''.format(request.id))
+ context.set_code(StatusCode.INVALID_ARGUMENT)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ try:
+ assert isinstance(request, ImageDownload)
+ path = '/devices/{}'.format(request.id)
+ device = self.root.get(path)
+ assert device.admin_state == AdminState.ENABLED, \
+ 'Device to revert image cannot be ' \
+ 'in admin state \'{}\''.format(device.admin_state)
+ agent = self.core.get_device_agent(device.id)
+ agent.revert_image_update(request)
+ return OperationResp(code=OperationResp.OPERATION_SUCCESS)
+
+ except KeyError:
+ context.set_details(
+ 'Device \'{}\' not found'.format(request.id))
+ context.set_code(StatusCode.NOT_FOUND)
+ return OperationResp(code=OperationResp.OPERATION_FAILURE)
+
+ @twisted_async
def DeleteDevice(self, request, context):
log.info('grpc-request', request=request)
diff --git a/voltha/protos/common.proto b/voltha/protos/common.proto
index 2486b9b..5f311e1 100644
--- a/voltha/protos/common.proto
+++ b/voltha/protos/common.proto
@@ -40,6 +40,10 @@
// The device is disabled and shall not perform its intended forwarding
// functions other than being available for re-activation.
DISABLED = 2;
+
+ // The deive is in the state of image download
+ DOWNLOADING_IMAGE = 4;
+
}
}
@@ -85,3 +89,18 @@
REACHABLE = 2;
}
}
+
+message OperationResp {
+ option (yang_child_rule) = MOVE_TO_PARENT_LEVEL;
+
+ enum OperationReturnCode {
+ OPERATION_SUCCESS = 0;
+ OPERATION_FAILURE = 1;
+ OPERATION_UNSUPPORTED = 2;
+ }
+ // Return code
+ OperationReturnCode code = 1;
+
+ // Additional Info
+ string additional_info = 2;
+}
diff --git a/voltha/protos/device.proto b/voltha/protos/device.proto
index 1cafdd8..bc0139b 100644
--- a/voltha/protos/device.proto
+++ b/voltha/protos/device.proto
@@ -95,6 +95,76 @@
repeated Image image = 1;
}
+message ImageDownload {
+ option (yang_child_rule) = MOVE_TO_PARENT_LEVEL;
+
+ enum ImageDownloadState {
+ DOWNLOAD_UNKNOWN = 0;
+ DOWNLOAD_SUCCEEDED = 1;
+ DOWNLOAD_REQUESTED = 2;
+ DOWNLOAD_STARTED = 3;
+ DOWNLOAD_FAILED = 4;
+ DOWNLOAD_UNSUPPORTED = 5;
+ }
+
+ enum ImageDownloadFailureReason {
+ NO_ERROR = 0;
+ INVALID_URL = 1;
+ DEVICE_BUSY = 2;
+ INSUFFICIENT_SPACE = 3;
+ UNKNOWN_ERROR = 4;
+ }
+
+ enum ImageActivateState {
+ IMAGE_UNKNOWN = 0;
+ IMAGE_INACTIVE = 1;
+ IMAGE_ACTIVATE = 2;
+ IMAGE_ACTIVE = 3;
+ IMAGE_REVERT = 4;
+ }
+
+ // Device Identifier
+ string id = 1;
+
+ // Image unique identifier
+ string name = 2;
+
+ // URL where the image is available
+ // should include username password
+ string url = 3;
+
+ // CRC of the image to be verified aginst
+ uint32 crc = 4;
+
+ // Download state
+ ImageDownloadState state = 5;
+
+ // Downloaded version
+ string image_version = 6;
+
+ // Bytes downloaded
+ uint32 downloaded_bytes = 7;
+
+ // Download failure reason
+ ImageDownloadFailureReason reason= 8;
+
+ // Additional info
+ string additional_info = 9;
+
+ // Save current configuration
+ bool save_config = 10;
+
+ // Image local location
+ string local_dir = 11;
+
+ // Image activation state
+ ImageActivateState image_state = 12;
+}
+
+message ImageDownloads {
+ repeated ImageDownload items = 2;
+}
+
message Port {
option (voltha.yang_child_rule) = MOVE_TO_PARENT_LEVEL;
@@ -213,6 +283,7 @@
// Channel Terminations for the OLT device
repeated bbf_fiber.ChannelterminationConfig channel_terminations = 132 [(child_node) = {key: "name"}];
+ repeated ImageDownload image_downloads = 133 [(child_node) = {key: "name"}];
}
message Devices {
diff --git a/voltha/protos/voltha.proto b/voltha/protos/voltha.proto
index a79a151..30e1834 100644
--- a/voltha/protos/voltha.proto
+++ b/voltha/protos/voltha.proto
@@ -360,6 +360,73 @@
};
}
+ // Request an image download to the standby partition
+ // of a device.
+ // Note that the call is expected to be non-blocking.
+ rpc DownloadImage(ImageDownload) returns(OperationResp) {
+ option (google.api.http) = {
+ post: "/api/v1/devices/{id}/image_downloads/{name}"
+ body: "*"
+ };
+ }
+
+ // Get image download status on a device
+ // The request retrieves progress on device and updates db record
+ rpc GetImageDownloadStatus(ImageDownload) returns(ImageDownload) {
+ option (google.api.http) = {
+ get: "/api/v1/devices/{id}/image_downloads/{name}/status"
+ };
+ }
+
+ // Get image download db record
+ rpc GetImageDownload(ImageDownload) returns(ImageDownload) {
+ option (google.api.http) = {
+ get: "/api/v1/devices/{id}/image_downloads/{name}"
+ };
+ }
+
+ // List image download db records for a given device
+ rpc ListImageDownloads(ID) returns(ImageDownloads) {
+ option (google.api.http) = {
+ get: "/api/v1/devices/{id}/image_downloads"
+ };
+ }
+
+ // Cancel an existing image download process on a device
+ rpc CancelImageDownload(ImageDownload) returns(OperationResp) {
+ option (google.api.http) = {
+ delete: "/api/v1/devices/{id}/image_downloads/{name}"
+ };
+ }
+
+ // Activate the specified image at a standby partition
+ // to active partition.
+ // Depending on the device implementation, this call
+ // may or may not cause device reboot.
+ // If no reboot, then a reboot is required to make the
+ // activated image running on device
+ // Note that the call is expected to be non-blocking.
+ rpc ActivateImageUpdate(ImageDownload) returns(OperationResp) {
+ option (google.api.http) = {
+ post: "/api/v1/devices/{id}/image_downloads/{name}/image_update"
+ body: "*"
+ };
+ }
+
+ // Revert the specified image at standby partition
+ // to active partition, and revert to previous image
+ // Depending on the device implementation, this call
+ // may or may not cause device reboot.
+ // If no reboot, then a reboot is required to make the
+ // previous image running on device
+ // Note that the call is expected to be non-blocking.
+ rpc RevertImageUpdate(ImageDownload) returns(OperationResp) {
+ option (google.api.http) = {
+ post: "/api/v1/devices/{id}/image_downloads/{name}/image_revert"
+ body: "*"
+ };
+ }
+
// List ports of a device
rpc ListDevicePorts(ID) returns(Ports) {
option (google.api.http) = {
@@ -986,6 +1053,69 @@
};
}
+ // Request an image download to the standby partition
+ // of a device.
+ // Note that the call is expected to be non-blocking
+ rpc DownloadImage(ImageDownload) returns(OperationResp) {
+ option (google.api.http) = {
+ post: "/api/v1/local/devices/{id}/image_downloads/{name}"
+ body: "*"
+ };
+ }
+
+ // Get image download status on a device
+ // The request retrieves progress on device and updates db record
+ rpc GetImageDownloadStatus(ImageDownload) returns(ImageDownload) {
+ option (google.api.http) = {
+ get: "/api/v1/local/devices/{id}/image_downloads/{name}/status"
+ };
+ }
+
+ // Get image download db record
+ rpc GetImageDownload(ImageDownload) returns(ImageDownload) {
+ option (google.api.http) = {
+ get: "/api/v1/local/devices/{id}/image_downloads/{name}"
+ };
+ }
+
+ // List image download db records for a given device
+ rpc ListImageDownloads(ID) returns(ImageDownloads) {
+ option (google.api.http) = {
+ get: "/api/v1/local/devices/{id}/image_downloads"
+ };
+ }
+
+ // Cancel an image download process on a device
+ rpc CancelImageDownload(ImageDownload) returns(OperationResp) {
+ option (google.api.http) = {
+ delete: "/api/v1/local/devices/{id}/image_downloads/{name}"
+ };
+ }
+
+ // Install and Activate a downloaded image from standby
+ // partition to active partition
+ // A subsequent call to reboot will cause the newly update image
+ // to become active
+ // Note that the call is expected to be non-blocking.
+ rpc ActivateImageUpdate(ImageDownload) returns(OperationResp) {
+ option (google.api.http) = {
+ post: "/api/v1/local/devices/{id}/image_downloads/{name}/image_update"
+ body: "*"
+ };
+ }
+
+ // Uninstall and deactivate an image update on a device,
+ // and revert back to pre update image
+ // A subsequent call to reboot will cause the pre update image
+ // to become active
+ // Note that the call is expected to be non-blocking.
+ rpc RevertImageUpdate(ImageDownload) returns(OperationResp) {
+ option (google.api.http) = {
+ post: "/api/v1/local/devices/{id}/image_downloads/{name}/image_revert"
+ body: "*"
+ };
+ }
+
// List ports of a device
rpc ListDevicePorts(ID) returns(Ports) {
option (google.api.http) = {