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) = {