]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
ITRI DISCO cinder driver
authorThelo Gaultier <thelo.gaultier@gmail.com>
Fri, 4 Dec 2015 07:29:44 +0000 (15:29 +0800)
committerThelo Gaultier <thelo.gaultier@gmail.com>
Fri, 15 Jan 2016 15:30:27 +0000 (23:30 +0800)
    This commit adds a Cinder volume driver for ITRI DISCO product.
    The DISCO driver supports the following features:
    * Volume Create/Delete
    * Volume Attach/Detach
    * Snapshot Create/Delete
    * Create Volume from Snapshot
    * Get Volume Stats
    * Copy Image to Volume
    * Copy Volume to Image
    * Clone Volume
    * Extend volume

    Implements: blueprint disco-driver-cinder

This patchset also includes the unit tests.

New config options for the cinder driver were also added.
related nova patchset : https://review.openstack.org/253353
related os_brick patchset : https://review.openstack.org/253352

DocImpact
Change-Id: I0168af34364343246a2855bdbd4a9f5ed5b05438

12 files changed:
cinder/opts.py
cinder/tests/unit/volume/drivers/disco/__init__.py [new file with mode: 0644]
cinder/tests/unit/volume/drivers/disco/test_create_cloned_volume.py [new file with mode: 0644]
cinder/tests/unit/volume/drivers/disco/test_create_snapshot.py [new file with mode: 0644]
cinder/tests/unit/volume/drivers/disco/test_create_volume.py [new file with mode: 0644]
cinder/tests/unit/volume/drivers/disco/test_create_volume_from_snapshot.py [new file with mode: 0644]
cinder/tests/unit/volume/drivers/disco/test_delete_snapshot.py [new file with mode: 0644]
cinder/tests/unit/volume/drivers/disco/test_delete_volume.py [new file with mode: 0644]
cinder/tests/unit/volume/drivers/disco/test_extend_volume.py [new file with mode: 0644]
cinder/volume/drivers/disco/__init__.py [new file with mode: 0644]
cinder/volume/drivers/disco/disco.py [new file with mode: 0644]
releasenotes/notes/disco-cinder-driver-9dac5fb04511de1f.yaml [new file with mode: 0644]

index 9f65dca10598255bbc20c1110baa69d0e0f9f152..78e6d38ef79f60160768bc2b890d78d3cf667a7f 100644 (file)
@@ -67,6 +67,8 @@ from cinder.volume.drivers.cloudbyte import options as \
 from cinder.volume.drivers import datera as cinder_volume_drivers_datera
 from cinder.volume.drivers.dell import dell_storagecenter_common as \
     cinder_volume_drivers_dell_dellstoragecentercommon
+from cinder.volume.drivers.disco import disco as \
+    cinder_volume_drivers_disco_disco
 from cinder.volume.drivers.dothill import dothill_common as \
     cinder_volume_drivers_dothill_dothillcommon
 from cinder.volume.drivers import drbdmanagedrv as \
@@ -280,6 +282,7 @@ def list_opts():
                 [cinder_scheduler_scheduleroptions.
                     scheduler_json_config_location_opt],
                 cinder_volume_drivers_zfssa_zfssanfs.ZFSSA_OPTS,
+                cinder_volume_drivers_disco_disco.disco_opts,
                 cinder_volume_drivers_hgst.hgst_opts,
                 cinder_image_imageutils.image_helper_opts,
                 cinder_compute_nova.nova_opts,
diff --git a/cinder/tests/unit/volume/drivers/disco/__init__.py b/cinder/tests/unit/volume/drivers/disco/__init__.py
new file mode 100644 (file)
index 0000000..7229723
--- /dev/null
@@ -0,0 +1,149 @@
+# Copyright (c) 2015 Industrial Technology Research Institute.
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Parent class for the DISCO driver unit test."""
+
+import mock
+from suds import client
+
+from os_brick.initiator import connector
+
+from cinder import context
+from cinder import test
+from cinder.tests.unit import fake_volume
+from cinder.volume import configuration as conf
+import cinder.volume.drivers.disco.disco as driver
+
+
+class TestDISCODriver(test.TestCase):
+    """Generic class for the DISCO test case."""
+
+    DETAIL_OPTIONS = {
+        'success': 1,
+        'pending': 2,
+        'failure': 3
+    }
+
+    ERROR_STATUS = 1
+
+    def setUp(self):
+        """Initialise variable common to all the test cases."""
+        super(TestDISCODriver, self).setUp()
+
+        mock_exec = mock.Mock()
+        mock_exec.return_value = ('', '')
+        self.cfg = mock.Mock(spec=conf.Configuration)
+        self.cfg.disco_client = '127.0.0.1'
+        self.cfg.disco_client_port = '9898'
+        self.cfg.disco_wsdl_path = 'somewhere'
+        self.cfg.volume_name_prefix = 'openstack-'
+        self.cfg.num_volume_device_scan_tries = 1
+        self.cfg.snapshot_check_timeout = 3600
+        self.cfg.restore_check_timeout = 3600
+        self.cfg.clone_check_timeout = 3600
+        self.cfg.snapshot_reserve_days = -1
+        self.cfg.retry_interval = 1
+
+        self.FAKE_SOAP_RESPONSE = {
+            'standard': {
+                'success': {'status': 0, 'result': 'a normal message'},
+                'fail': {'status': 1, 'result': 'an error message'}}
+        }
+
+        mock.patch.object(client,
+                          'Client',
+                          self.create_client).start()
+
+        mock.patch.object(connector.InitiatorConnector,
+                          'factory',
+                          self.get_mock_connector).start()
+
+        mock.patch.object(driver.DiscoDriver,
+                          '_get_connector_identifier',
+                          self.get_mock_attribute).start()
+
+        self.driver = driver.DiscoDriver(execute=mock_exec,
+                                         configuration=self.cfg)
+        self.driver.do_setup(None)
+
+        self.ctx = context.RequestContext('fake', 'fake', auth_token=True)
+        self.volume = fake_volume.fake_volume_obj(self.ctx)
+        self.volume['volume_id'] = '1234567'
+
+        self.requester = self.driver.client.service
+
+    def create_client(self, *cmd, **kwargs):
+        """Mock the suds client."""
+        return FakeClient()
+
+    def get_mock_connector(self, *cmd, **kwargs):
+        """Mock the os_brick connector."""
+        return None
+
+    def get_mock_attribute(self, *cmd, **kwargs):
+        """Mock the os_brick connector."""
+        return 'DISCO'
+
+
+class FakeClient(object):
+    """Fake class to mock suds.Client."""
+
+    def __init__(self, *args, **kwargs):
+        """Create a fake service attribute."""
+        self.service = FakeMethod()
+
+
+class FakeMethod(object):
+    """Fake class recensing some of the method of the suds client."""
+
+    def __init__(self, *args, **kwargs):
+        """Fake class to mock the suds client."""
+
+    def volumeCreate(self, *args, **kwargs):
+        """"Mock function to create a volume."""
+
+    def volumeDelete(self, *args, **kwargs):
+        """"Mock function to delete a volume."""
+
+    def snapshotCreate(self, *args, **kwargs):
+        """"Mock function to create a snapshot."""
+
+    def snapshotDetail(self, *args, **kwargs):
+        """"Mock function to get the snapshot detail."""
+
+    def snapshotDelete(self, *args, **kwargs):
+        """"Mock function to delete snapshot."""
+
+    def restoreFromSnapshot(self, *args, **kwargs):
+        """"Mock function to create a volume from a snasphot."""
+
+    def restoreDetail(self, *args, **kwargs):
+        """"Mock function to detail the restore operation."""
+
+    def volumeDetailByName(self, *args, **kwargs):
+        """"Mock function to get the volume detail from its name."""
+
+    def volumeClone(self, *args, **kwargs):
+        """"Mock function to clone a volume."""
+
+    def cloneDetail(self, *args, **kwargs):
+        """Mock function to get the clone detail."""
+
+    def volumeExtend(self, *args, **kwargs):
+        """Mock function to extend a volume."""
+
+    def systemInformationList(self, *args, **kwargs):
+        """Mock function to get the backend properties."""
diff --git a/cinder/tests/unit/volume/drivers/disco/test_create_cloned_volume.py b/cinder/tests/unit/volume/drivers/disco/test_create_cloned_volume.py
new file mode 100644 (file)
index 0000000..cfabf6c
--- /dev/null
@@ -0,0 +1,153 @@
+# (c) Copyright 2015 Industrial Technology Research Institute.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Test cases for create cloned volume."""
+
+import copy
+import mock
+import six
+
+
+from cinder import exception
+from cinder.tests.unit import fake_volume
+from cinder.tests.unit.volume.drivers import disco
+
+
+class CreateCloneVolumeTestCase(disco.TestDISCODriver):
+    """Test cases for DISCO connector."""
+
+    def setUp(self):
+        """Initialise variables and mock functions."""
+        super(CreateCloneVolumeTestCase, self).setUp()
+
+        self.dest_volume = fake_volume.fake_volume_obj(self.ctx)
+        # Create mock functions for all the suds call done by the driver."""
+        mock.patch.object(self.requester,
+                          'volumeClone',
+                          self.clone_request).start()
+
+        mock.patch.object(self.requester,
+                          'cloneDetail',
+                          self.clone_detail_request).start()
+
+        mock.patch.object(self.requester,
+                          'volumeDetailByName',
+                          self.volume_detail_request).start()
+
+        self.volume_detail_response = {
+            'status': 0,
+            'volumeInfoResult':
+                {'volumeId': 1234567}
+        }
+
+        clone_success = (
+            copy.deepcopy(self.FAKE_SOAP_RESPONSE['standard']['success']))
+        clone_pending = (
+            copy.deepcopy(self.FAKE_SOAP_RESPONSE['standard']['success']))
+        clone_fail = (
+            copy.deepcopy(self.FAKE_SOAP_RESPONSE['standard']['success']))
+        clone_response_fail = (
+            copy.deepcopy(self.FAKE_SOAP_RESPONSE['standard']['success']))
+
+        clone_success['result'] = (
+            six.text_type(self.DETAIL_OPTIONS['success']))
+        clone_pending['result'] = (
+            six.text_type(self.DETAIL_OPTIONS['pending']))
+        clone_fail['result'] = (
+            six.text_type(self.DETAIL_OPTIONS['failure']))
+        clone_response_fail['status'] = 1
+
+        self.FAKE_SOAP_RESPONSE['clone_detail'] = {
+            'success': clone_success,
+            'fail': clone_fail,
+            'pending': clone_pending,
+            'request_fail': clone_response_fail
+        }
+
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response['result'] = '1234'
+
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['clone_detail']['success'])
+        self.test_pending = False
+        self.test_pending_count = 0
+
+    def clone_request(self, *cmd, **kwargs):
+        """Mock function for the createVolumeFromSnapshot function."""
+        return self.response
+
+    def clone_detail_request(self, *cmd, **kwargs):
+        """Mock function for the restoreDetail function."""
+        if self.test_pending:
+            if self.test_pending_count == 0:
+                self.test_pending_count += 1
+                return self.FAKE_SOAP_RESPONSE['clone_detail']['pending']
+            else:
+                return self.FAKE_SOAP_RESPONSE['clone_detail']['success']
+        else:
+            return self.response_detail
+
+    def volume_detail_request(self, *cmd, **kwargs):
+        """Mock function for the volumeDetail function."""
+        return self.volume_detail_response
+
+    def test_create_cloned_volume(self):
+        """Normal case."""
+        expected = 1234567
+        actual = self.driver.create_cloned_volume(self.dest_volume,
+                                                  self.volume)
+        self.assertEqual(expected, actual['provider_location'])
+
+    def test_create_clone_volume_fail(self):
+        """Clone volume request to DISCO fails."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_cloned_volume)
+
+    def test_create_cloned_volume_fail_not_immediate(self):
+        """Get clone detail returns that the clone fails."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['clone_detail']['fail'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_cloned_volume)
+
+    def test_create_cloned_volume_fail_not_immediate_response_fail(self):
+        """Get clone detail request to DISCO fails."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['clone_detail']['request_fail'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_cloned_volume)
+
+    def test_create_cloned_volume_fail_not_immediate_request_fail(self):
+        """Get clone detail returns the task is pending then complete."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.test_pending = True
+        self.test_create_cloned_volume()
+
+    def test_create_cloned_volume_timeout(self):
+        """Clone request timeout."""
+        self.driver.configuration.clone_check_timeout = 3
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['clone_detail']['pending'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_cloned_volume)
+
+    def test_create_cloned_volume_volume_detail_fail(self):
+        """Get volume detail request to DISCO fails."""
+        self.volume_detail_response['status'] = 1
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_cloned_volume)
diff --git a/cinder/tests/unit/volume/drivers/disco/test_create_snapshot.py b/cinder/tests/unit/volume/drivers/disco/test_create_snapshot.py
new file mode 100644 (file)
index 0000000..1308729
--- /dev/null
@@ -0,0 +1,148 @@
+# (c) Copyright 2015 Industrial Technology Research Institute.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Test case for the function create snapshot."""
+
+
+import copy
+import mock
+
+from cinder import db
+from cinder import exception
+from cinder.tests.unit import fake_snapshot
+from cinder.tests.unit.volume.drivers import disco
+
+
+class CreateSnapshotTestCase(disco.TestDISCODriver):
+    """Test cases for DISCO connector."""
+
+    def get_fake_volume(self, ctx, id):
+        """Return fake volume from db calls."""
+        return self.volume
+
+    def setUp(self):
+        """Initialise variables and mock functions."""
+        super(CreateSnapshotTestCase, self).setUp()
+
+        self.snapshot = fake_snapshot.fake_snapshot_obj(
+            self.ctx, **{'volume': self.volume})
+
+        # Mock db call in the cinder driver
+        self.mock_object(db.sqlalchemy.api, 'volume_get',
+                         self.get_fake_volume)
+
+        mock.patch.object(self.requester,
+                          'snapshotCreate',
+                          self.snapshot_request).start()
+
+        mock.patch.object(self.requester,
+                          'snapshotDetail',
+                          self.snapshot_detail_request).start()
+
+        snapshot_detail_response = {
+            'status': 0,
+            'snapshotInfoResult':
+                {'snapshotId': 1234,
+                 'description': 'a description',
+                 'createTime': '',
+                 'expireTime': '',
+                 'isDeleted': False,
+                 'status': 0}
+        }
+
+        snap_success = copy.deepcopy(snapshot_detail_response)
+        snap_pending = copy.deepcopy(snapshot_detail_response)
+        snap_fail = copy.deepcopy(snapshot_detail_response)
+        snap_response_fail = copy.deepcopy(snapshot_detail_response)
+        snap_success['snapshotInfoResult']['status'] = (
+            self.DETAIL_OPTIONS['success'])
+        snap_pending['snapshotInfoResult']['status'] = (
+            self.DETAIL_OPTIONS['pending'])
+        snap_fail['snapshotInfoResult']['status'] = (
+            self.DETAIL_OPTIONS['failure'])
+        snap_response_fail['status'] = 1
+
+        self.FAKE_SOAP_RESPONSE['snapshot_detail'] = {
+            'success': snap_success,
+            'fail': snap_fail,
+            'pending': snap_pending,
+            'request_fail': snap_response_fail}
+
+        self.response = (
+            self.FAKE_SOAP_RESPONSE['standard']['success'])
+        self.response['result'] = 1234
+
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['snapshot_detail']['success'])
+        self.test_pending = False
+
+        self.test_pending_count = 0
+
+    def snapshot_request(self, *cmd, **kwargs):
+        """Mock function for the createSnapshot call."""
+        return self.response
+
+    def snapshot_detail_request(self, *cmd, **kwargs):
+        """Mock function for the snapshotDetail call."""
+        if self.test_pending:
+            if self.test_pending_count == 0:
+                self.test_pending_count += 1
+                return self.FAKE_SOAP_RESPONSE['snapshot_detail']['pending']
+            else:
+                return self.FAKE_SOAP_RESPONSE['snapshot_detail']['success']
+        else:
+            return self.response_detail
+
+    def test_create_snapshot(self):
+        """Normal test case."""
+        expected = 1234
+        actual = self.driver.create_snapshot(self.volume)
+        self.assertEqual(expected, actual['provider_location'])
+
+    def test_create_snapshot_fail(self):
+        """Request to DISCO failed."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_snapshot)
+
+    def test_create_snapshot_fail_not_immediate(self):
+        """Request to DISCO failed when monitoring the snapshot details."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['snapshot_detail']['fail'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_snapshot)
+
+    def test_create_snapshot_fail_not_immediate_response_fail(self):
+        """Request to get the snapshot details returns a failure."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['snapshot_detail']['request_fail'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_snapshot)
+
+    def test_create_snapshot_detail_pending(self):
+        """Request to get the snapshot detail return pending then success."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.test_pending = True
+        self.test_create_snapshot()
+
+    def test_create_snapshot_timeout(self):
+        """Snapshot request timeout."""
+        self.driver.configuration.snapshot_check_timeout = 3
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['snapshot_detail']['pending'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_snapshot)
diff --git a/cinder/tests/unit/volume/drivers/disco/test_create_volume.py b/cinder/tests/unit/volume/drivers/disco/test_create_volume.py
new file mode 100644 (file)
index 0000000..65d91c5
--- /dev/null
@@ -0,0 +1,53 @@
+# (c) Copyright 2015 Industrial Technology Research Institute.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Test case for the create volume function."""
+
+import mock
+
+from cinder import exception
+from cinder.tests.unit.volume.drivers import disco
+
+
+class CreateVolumeTestCase(disco.TestDISCODriver):
+    """Test cases for DISCO connector."""
+
+    def setUp(self):
+        """Prepare variables and mock functions."""
+        super(CreateVolumeTestCase, self).setUp()
+
+        # Mock the suds cliebt.
+        mock.patch.object(self.requester,
+                          'volumeCreate',
+                          self.perform_disco_request).start()
+
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+
+    def perform_disco_request(self, *cmd, **kwargs):
+        """Mock function for the suds client."""
+        return self.response
+
+    def test_create_volume(self):
+        """Normal case."""
+        expected = '1234567'
+        self.response['result'] = expected
+        ret = self.driver.create_volume(self.volume)
+        actual = ret['provider_location']
+        self.assertEqual(expected, actual)
+
+    def test_create_volume_fail(self):
+        """Request to DISCO failed."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_volume)
diff --git a/cinder/tests/unit/volume/drivers/disco/test_create_volume_from_snapshot.py b/cinder/tests/unit/volume/drivers/disco/test_create_volume_from_snapshot.py
new file mode 100644 (file)
index 0000000..2f89f31
--- /dev/null
@@ -0,0 +1,161 @@
+# (c) Copyright 2015 Industrial Technology Research Institute.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Test case for create volume from snapshot."""
+
+import copy
+import mock
+
+from cinder import exception
+from cinder.tests.unit import fake_snapshot
+from cinder.tests.unit.volume.drivers import disco
+
+
+class CreateVolumeFromSnapshotTestCase(disco.TestDISCODriver):
+    """Test cases for the create volume from snapshot of DISCO connector."""
+
+    def setUp(self):
+        """Initialise variables and mock functions."""
+        super(CreateVolumeFromSnapshotTestCase, self).setUp()
+
+        self.snapshot = fake_snapshot.fake_snapshot_obj(
+            self.ctx, **{'volume': self.volume})
+
+        # Mock restoreFromSnapshot, restoreDetail
+        # and volume detail since they are in the function path
+        mock.patch.object(self.requester,
+                          'restoreFromSnapshot',
+                          self.restore_request).start()
+
+        mock.patch.object(self.requester,
+                          'restoreDetail',
+                          self.restore_detail_request).start()
+
+        mock.patch.object(self.requester,
+                          'volumeDetailByName',
+                          self.volume_detail_request).start()
+
+        restore_detail_response = {
+            'status': 0,
+            'restoreInfoResult':
+                {'restoreId': 1234,
+                 'startTime': '',
+                 'statusPercent': '',
+                 'volumeName': 'aVolumeName',
+                 'snapshotId': 1234,
+                 'status': 0}
+        }
+
+        self.volume_detail_response = {
+            'status': 0,
+            'volumeInfoResult':
+                {'volumeId': 1234567}
+        }
+
+        rest_success = copy.deepcopy(restore_detail_response)
+        rest_pending = copy.deepcopy(restore_detail_response)
+        rest_fail = copy.deepcopy(restore_detail_response)
+        rest_response_fail = copy.deepcopy(restore_detail_response)
+        rest_success['restoreInfoResult']['status'] = (
+            self.DETAIL_OPTIONS['success'])
+        rest_pending['restoreInfoResult']['status'] = (
+            self.DETAIL_OPTIONS['pending'])
+        rest_fail['restoreInfoResult']['status'] = (
+            self.DETAIL_OPTIONS['failure'])
+        rest_response_fail['status'] = 1
+
+        self.FAKE_SOAP_RESPONSE['restore_detail'] = {
+            'success': rest_success,
+            'fail': rest_fail,
+            'pending': rest_pending,
+            'request_fail': rest_response_fail
+        }
+
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response['result'] = '1234'
+
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['restore_detail']['success'])
+        self.test_pending = False
+
+        self.test_pending_count = 0
+
+    def restore_request(self, *cmd, **kwargs):
+        """Mock function for the createVolumeFromSnapshot function."""
+        return self.response
+
+    def restore_detail_request(self, *cmd, **kwargs):
+        """Mock function for the restoreDetail function."""
+        if self.test_pending:
+            if self.test_pending_count == 0:
+                self.test_pending_count += 1
+                return self.FAKE_SOAP_RESPONSE['restore_detail']['pending']
+            else:
+                return self.FAKE_SOAP_RESPONSE['restore_detail']['success']
+        else:
+            return self.response_detail
+
+    def volume_detail_request(self, *cmd, **kwargs):
+        """Mock function for the volumeDetail function."""
+        return self.volume_detail_response
+
+    def test_create_volume_from_snapshot(self):
+        """Normal case."""
+        expected = 1234567
+        actual = self.driver.create_volume_from_snapshot(self.volume,
+                                                         self.snapshot)
+        self.assertEqual(expected, actual['provider_location'])
+
+    def test_create_volume_from_snapshot_fail(self):
+        """Create volume from snapshot request fails."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_volume_from_snapshot)
+
+    def test_create_volume_from_snapshot_fail_not_immediate(self):
+        """Get restore details request fails."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['restore_detail']['fail'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_volume_from_snapshot)
+
+    def test_create_volume_from_snapshot_fail_detail_response_fail(self):
+        """Get restore details reports that restore operation fails."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['restore_detail']['request_fail'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_volume_from_snapshot)
+
+    def test_create_volume_from_snapshot_fail_not_immediate_resp_fail(self):
+        """Get restore details reports that the task is pending, then done."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.test_pending = True
+        self.test_create_volume_from_snapshot()
+
+    def test_create_volume_from_snapshot_timeout(self):
+        """Create volume from snapshot task timeout."""
+        self.driver.configuration.restore_check_timeout = 3
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.response_detail = (
+            self.FAKE_SOAP_RESPONSE['restore_detail']['pending'])
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_volume_from_snapshot)
+
+    def test_create_volume_from_snapshot_volume_detail_fail(self):
+        """Cannot get the newly created volume information."""
+        self.volume_detail_response['status'] = 1
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_create_volume_from_snapshot)
diff --git a/cinder/tests/unit/volume/drivers/disco/test_delete_snapshot.py b/cinder/tests/unit/volume/drivers/disco/test_delete_snapshot.py
new file mode 100644 (file)
index 0000000..88cbeaa
--- /dev/null
@@ -0,0 +1,51 @@
+# (c) Copyright 2015 Industrial Technology Research Institute.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Test case for the delete snapshot function."""
+import mock
+
+from cinder import exception
+from cinder.tests.unit import fake_snapshot
+from cinder.tests.unit.volume.drivers import disco
+
+
+class DeleteSnapshotTestCase(disco.TestDISCODriver):
+    """Test cases to delete DISCO volumes."""
+
+    def setUp(self):
+        """Initialise variables and mock functions."""
+        super(DeleteSnapshotTestCase, self).setUp()
+
+        # Mock snapshotDelete function from suds client.
+        mock.patch.object(self.requester,
+                          'snapshotDelete',
+                          self.perform_disco_request).start()
+
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.snapshot = fake_snapshot.fake_snapshot_obj(
+            self.ctx, **{'volume': self.volume})
+
+    def perform_disco_request(self, *cmd, **kwargs):
+        """Mock function to delete a snapshot."""
+        return self.response
+
+    def test_delete_snapshot(self):
+        """Delete a snapshot."""
+        self.driver.delete_snapshot(self.snapshot)
+
+    def test_delete_snapshot_fail(self):
+        """Make the API returns an error while deleting."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_delete_snapshot)
diff --git a/cinder/tests/unit/volume/drivers/disco/test_delete_volume.py b/cinder/tests/unit/volume/drivers/disco/test_delete_volume.py
new file mode 100644 (file)
index 0000000..7967503
--- /dev/null
@@ -0,0 +1,49 @@
+# (c) Copyright 2015 Industrial Technology Research Institute.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Test case for the delete volume function."""
+
+import mock
+
+from cinder import exception
+from cinder.tests.unit.volume.drivers import disco
+
+
+class DeleteVolumeTestCase(disco.TestDISCODriver):
+    """Test cases to delete DISCO volumes."""
+
+    def setUp(self):
+        """Initialise variables and mock functions."""
+        super(DeleteVolumeTestCase, self).setUp()
+
+        # Mock volumeDelete function from suds client.
+        mock.patch.object(self.requester,
+                          'volumeDelete',
+                          self.perform_disco_request).start()
+
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+
+    def perform_disco_request(self, *cmd, **kwargs):
+        """Mock function to delete a volume."""
+        return self.response
+
+    def test_delete_volume(self):
+        """Delete a volume."""
+        self.driver.delete_volume(self.volume)
+
+    def test_delete_volume_fail(self):
+        """Make the API returns an error while deleting."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_delete_volume)
diff --git a/cinder/tests/unit/volume/drivers/disco/test_extend_volume.py b/cinder/tests/unit/volume/drivers/disco/test_extend_volume.py
new file mode 100644 (file)
index 0000000..8c4ad28
--- /dev/null
@@ -0,0 +1,50 @@
+# (c) Copyright 2015 Industrial Technology Research Institute.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Test cases for the extend volume feature."""
+
+import mock
+
+from cinder import exception
+from cinder.tests.unit.volume.drivers import disco
+
+
+class VolumeExtendTestCase(disco.TestDISCODriver):
+    """Test cases for DISCO connector."""
+
+    def setUp(self):
+        """Initialise variables and mock functions."""
+        super(VolumeExtendTestCase, self).setUp()
+
+        # Mock function to extend a volume.
+        mock.patch.object(self.requester,
+                          'volumeExtend',
+                          self.perform_disco_request).start()
+
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
+        self.new_size = 5
+
+    def perform_disco_request(self, *cmd, **kwargs):
+        """Mock volumExtend function from suds client."""
+        return self.response
+
+    def test_extend_volume(self):
+        """Extend a volume, normal case."""
+        self.driver.extend_volume(self.volume, self.new_size)
+
+    def test_extend_volume_fail(self):
+        """Request to DISCO failed."""
+        self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.test_extend_volume)
diff --git a/cinder/volume/drivers/disco/__init__.py b/cinder/volume/drivers/disco/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/cinder/volume/drivers/disco/disco.py b/cinder/volume/drivers/disco/disco.py
new file mode 100644 (file)
index 0000000..4928300
--- /dev/null
@@ -0,0 +1,542 @@
+#    Copyright (c) 2015 Industrial Technology Research Institute.
+#    All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""DISCO Block device Driver."""
+
+import os
+import time
+
+from os_brick.initiator import connector
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_service import loopingcall
+from oslo_utils import units
+import six
+from suds import client
+
+from cinder import context
+from cinder.db.sqlalchemy import api
+from cinder import exception
+from cinder.i18n import _
+from cinder.image import image_utils
+from cinder import utils
+from cinder.volume import driver
+
+
+LOG = logging.getLogger(__name__)
+
+disco_opts = [
+    cfg.IPOpt('disco_client',
+              default='127.0.0.1',
+              help='The IP of DMS client socket server'),
+    cfg.PortOpt('disco_client_port',
+                default='9898',
+                help='The port to connect DMS client socket server'),
+    cfg.StrOpt('disco_wsdl_path',
+               default='/etc/cinder/DISCOService.wsdl',
+               help='Path to the wsdl file '
+                    'to communicate with DISCO request manager'),
+    cfg.StrOpt('volume_name_prefix',
+               default='openstack-',
+               help='Prefix before volume name to differenciate '
+                    'DISCO volume created through openstack '
+                    'and the other ones'),
+    cfg.IntOpt('snapshot_check_timeout',
+               default=3600,
+               help='How long we check whether a snapshot '
+                    'is finished before we give up'),
+    cfg.IntOpt('restore_check_timeout',
+               default=3600,
+               help='How long we check whether a restore '
+                    'is finished before we give up'),
+    cfg.IntOpt('clone_check_timeout',
+               default=3600,
+               help='How long we check whether a clone '
+                    'is finished before we give up'),
+    cfg.IntOpt('retry_interval',
+               default=1,
+               help='How long we wait before retrying to '
+                    'get an item detail')
+]
+
+DISCO_CODE_MAPPING = {
+    'request.success': 1,
+    'request.ongoing': 2,
+    'request.failure': 3,
+}
+
+CONF = cfg.CONF
+CONF.register_opts(disco_opts)
+
+
+# Driver to communicate with DISCO storage solution
+class DiscoDriver(driver.VolumeDriver):
+    """Execute commands related to DISCO Volumes."""
+
+    VERSION = "1.0"
+
+    def __init__(self, *args, **kwargs):
+        """Init Disco driver : get configuration, create client."""
+        super(DiscoDriver, self).__init__(*args, **kwargs)
+        self.configuration.append_config_values(disco_opts)
+        self.ctxt = context.get_admin_context()
+
+        self.connector = connector.InitiatorConnector.factory(
+            self._get_connector_identifier(), utils.get_root_helper(),
+            device_scan_attempts=(
+                self.configuration.num_volume_device_scan_tries)
+        )
+
+        self.connection_conf = {}
+        self.connection_conf['server_ip'] = self.configuration.disco_client
+        self.connection_conf['server_port'] = (
+            self.configuration.disco_client_port)
+
+        self.connection_properties = {}
+        self.connection_properties['name'] = None
+        self.connection_properties['disco_id'] = None
+        self.connection_properties['conf'] = self.connection_conf
+
+    def do_setup(self, context):
+        """Create client for DISCO request manager."""
+        LOG.debug("Enter in DiscoDriver do_setup.")
+        path = ''.join(['file:', self.configuration.disco_wsdl_path])
+        self.client = client.Client(path, cache=None)
+
+    def check_for_setup_error(self):
+        """Make sure we have the pre-requisites."""
+        LOG.debug("Enter in DiscoDriver check_for_setup_error.")
+        path = self.configuration.disco_wsdl_path
+        if not os.path.exists(path):
+            msg = _("Could not find DISCO wsdl file.")
+            raise exception.VolumeBackendAPIException(data=msg)
+
+    def _get_connector_identifier(self):
+        """Return connector identifier, put here to mock it in unit tests."""
+        return connector.DISCO
+
+    def create_volume(self, volume):
+        """Create a disco volume."""
+        name = self.configuration.volume_name_prefix, volume["id"]
+        vol_name = ''.join(name)
+        vol_size = volume['size'] * units.Ki
+        LOG.debug("Create volume : [name] %(vname)s - [size] %(vsize)s.",
+                  {'vname': vol_name, 'vsize': six.text_type(vol_size)})
+        reply = self.client.service.volumeCreate(vol_name, vol_size)
+        status = reply['status']
+        result = reply['result']
+        LOG.debug("Create volume : [status] %(stat)s - [result] %(res)s.",
+                  {'stat': six.text_type(status), 'res': result})
+
+        if status != 0:
+            msg = (_("Error while creating volume "
+                     "[status] %(stat)s - [result] %(res)s.") %
+                   {'stat': six.text_type(status), 'res': result})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+        LOG.debug("Volume %s created.", volume["name"])
+        return {'provider_location': result}
+
+    def delete_volume(self, volume):
+        """Delete a logical volume."""
+        disco_vol_id = volume['provider_location']
+        LOG.debug("Delete disco volume : %s.", disco_vol_id)
+        reply = self.client.service.volumeDelete(disco_vol_id)
+        status = reply['status']
+        result = reply['result']
+
+        LOG.debug("Delete volume [status] %(stat)s - [result] %(res)s.",
+                  {'stat': six.text_type(status), 'res': result})
+
+        if status != 0:
+            msg = (_("Error while deleting volume "
+                     "[status] %(stat)s - [result] %(res)s.") %
+                   {'stat': six.text_type(status), 'res': result})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        LOG.debug("Volume %s deleted.", volume['name'])
+
+    def create_snapshot(self, snapshot):
+        """Create a disco snapshot."""
+        volume = api.volume_get(self.ctxt, snapshot['volume_id'])
+        description = snapshot['display_description']
+        vol_id = volume['provider_location']
+        LOG.debug("Create snapshot of volume : %(id)s, "
+                  "description : %(desc)s.",
+                  {'id': vol_id, 'desc': description})
+
+        # Trigger an asynchronous local snapshot
+        reply = self.client.service.snapshotCreate(vol_id,
+                                                   -1, -1,
+                                                   description)
+        status = reply['status']
+        result = reply['result']
+        LOG.debug("Create snapshot : [status] %(stat)s - [result] %(res)s.",
+                  {'stat': six.text_type(status), 'res': result})
+
+        if status != 0:
+            msg = (_("Error while creating snapshot "
+                     "[status] %(stat)s - [result] %(res)s.") %
+                   {'stat': six.text_type(status), 'res': result})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        # Monitor the status until it becomes either success or fail
+        params = {'snapshot_id': int(result)}
+        start_time = int(time.time())
+
+        timer = loopingcall.FixedIntervalLoopingCall(
+            self._retry_get_detail,
+            start_time,
+            self.configuration.snapshot_check_timeout,
+            'snapshot_detail',
+            params)
+        reply = timer.start(interval=self.configuration.retry_interval).wait()
+
+        snapshot['provider_location'] = result
+        LOG.debug("snapshot taken successfully on volume : %(volume)s.",
+                  {'volume': volume['name']})
+        return {'provider_location': result}
+
+    def delete_snapshot(self, snapshot):
+        """Delete a disco snapshot."""
+        LOG.debug("Enter in delete a disco snapshot.")
+
+        snap_id = snapshot['provider_location']
+        LOG.debug("[start] Delete snapshot : %s.", snap_id)
+        reply = self.client.service.snapshotDelete(snap_id)
+        status = reply['status']
+        result = reply['result']
+        LOG.debug("[End] Delete snapshot : "
+                  "[status] %(stat)s - [result] %(res)s.",
+                  {'stat': six.text_type(status), 'res': result})
+
+        if status != 0:
+            msg = (_("Error while deleting snapshot "
+                     "[status] %(stat)s - [result] %(res)s") %
+                   {'stat': six.text_type(status), 'res': result})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Create a volume from a snapshot."""
+        name = self.configuration.volume_name_prefix, volume['id']
+        snap_id = snapshot['provider_location']
+        vol_name = ''.join(name)
+        # Trigger an asynchronous restore operation
+        LOG.debug("[start] Create volume from snapshot : "
+                  "%(snap_id)s - name : %(vol_name)s.",
+                  {'snap_id': snap_id, 'vol_name': vol_name})
+        reply = self.client.service.restoreFromSnapshot(snap_id, vol_name)
+        status = reply['status']
+        result = reply['result']
+        LOG.debug("Restore  volume from snapshot "
+                  "[status] %(stat)s - [result] %(res)s.",
+                  {'stat': six.text_type(status), 'res': result})
+
+        if status != 0:
+            msg = (_("Error[%(stat)s - %(res)s] while restoring snapshot "
+                     "[%(snap_id)s] into volume [%(vol)s].") %
+                   {'stat': six.text_type(status), 'res': result,
+                    'snap_id': snap_id, 'vol': vol_name})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        # Monitor the status until it becomes
+        # either success, fail or timeout
+        params = {'restore_id': int(result)}
+        start_time = int(time.time())
+
+        timer = loopingcall.FixedIntervalLoopingCall(
+            self._retry_get_detail,
+            start_time,
+            self.configuration.restore_check_timeout,
+            'restore_detail',
+            params)
+        reply = timer.start(interval=self.configuration.retry_interval).wait()
+
+        reply = self.client.service.volumeDetailByName(vol_name)
+        status = reply['status']
+        new_vol_id = reply['volumeInfoResult']['volumeId']
+
+        if status != 0:
+            msg = (_("Error[status] %(stat)s - [result] %(res)s] "
+                     "while getting volume id.") %
+                   {'stat': six.text_type(status), 'res': result})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+        LOG.debug("Restore done [status] %(stat)s - "
+                  "[volume id] %(vol_id)s.",
+                  {'stat': status, 'vol_id': six.text_type(new_vol_id)})
+        return {'provider_location': new_vol_id}
+
+    def create_cloned_volume(self, volume, src_vref):
+        """Create a clone of the specified volume."""
+        LOG.debug("Creating clone of volume: %s.", src_vref['id'])
+        name = self.configuration.volume_name_prefix, volume['id']
+        vol_name = ''.join(name)
+        vol_size = volume['size'] * units.Ki
+        src_vol_id = src_vref['provider_location']
+        LOG.debug("Clone volume : "
+                  "[name] %(name)s - [source] %(source)s - [size] %(size)s.",
+                  {'name': vol_name,
+                   'source': src_vol_id,
+                   'size': six.text_type(vol_size)})
+        reply = self.client.service.volumeClone(src_vol_id, vol_name)
+        status = reply['status']
+        result = reply['result']
+        LOG.debug("Clone volume : [status] %(stat)s - [result] %(res)s.",
+                  {'stat': six.text_type(status), 'res': result})
+
+        if status != 0:
+            msg = (_("Error while creating volume "
+                     "[status] %(stat)s - [result] %(res)s.") %
+                   {'stat': six.text_type(status), 'res': result})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        # Monitor the status until it becomes
+        # either success, fail or timeout
+        params = {'clone_id': int(result),
+                  'vol_name': vol_name}
+        start_time = int(time.time())
+
+        timer = loopingcall.FixedIntervalLoopingCall(
+            self._retry_get_detail,
+            start_time,
+            self.configuration.clone_check_timeout,
+            'clone_detail',
+            params)
+        reply = timer.start(interval=self.configuration.retry_interval).wait()
+
+        reply = self.client.service.volumeDetailByName(vol_name)
+        status = reply['status']
+        new_vol_id = reply['volumeInfoResult']['volumeId']
+
+        if status != 0:
+            msg = (_("Error[%(stat)s - %(res)s] "
+                     "while getting volume id."),
+                   {'stat': six.text_type(status), 'res': result})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        LOG.debug("clone done : "
+                  "[status] %(stat)s - [volume id] %(vol_id)s.",
+                  {'stat': status, 'vol_id': six.text_type(new_vol_id)})
+        return {'provider_location': new_vol_id}
+
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        """Fetch the image from image_service and write it to the volume."""
+        LOG.debug("Enter in copy image to volume for disco.")
+
+        try:
+            device_info = self._attach_volume(volume)
+            image_utils.fetch_to_raw(context,
+                                     image_service,
+                                     image_id,
+                                     device_info['path'],
+                                     self.configuration.volume_dd_blocksize,
+                                     size=volume['size'])
+        finally:
+            self._detach_volume(volume)
+
+    def _attach_volume(self, volume):
+        """Call the connector.connect_volume()."""
+        connection_properties = self._get_connection_properties(volume)
+        device_info = self.connector.connect_volume(connection_properties)
+        return device_info
+
+    def _detach_volume(self, volume):
+        """Call the connector.disconnect_volume()."""
+        connection_properties = self._get_connection_properties(volume)
+        self.connector.disconnect_volume(connection_properties, volume)
+
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
+        """Copy a  volume to a new image."""
+        LOG.debug("Enter in copy image to volume for disco.")
+        try:
+            device_info = self._attach_volume(volume)
+            image_utils.upload_volume(context,
+                                      image_service,
+                                      image_meta,
+                                      device_info['path'])
+        finally:
+            self._detach_volume(volume)
+
+    def extend_volume(self, volume, new_size):
+        """Extend an existing volume's size."""
+        vol_id = volume['provider_location']
+        LOG.debug("Extends volume : %(id)s, new size : %(size)s.",
+                  {'id': vol_id, 'size': new_size})
+        new_size_mb = new_size * units.Ki
+        reply = self.client.service.volumeExtend(vol_id, new_size_mb)
+        status = reply['status']
+        result = reply['result']
+
+        if status != 0:
+            msg = (_("Error while extending volume "
+                     "[status] %(stat)s - [result] %(res)s."),
+                   {'stat': six.text_type(status), 'res': result})
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+        LOG.debug("Volume extended : [id] %(vid)s - "
+                  "[status] %(stat)s - [result] %(res)s.",
+                  {'vid': vol_id,
+                   'stat': six.text_type(status),
+                   'res': result})
+
+    def initialize_connection(self, volume, connector):
+        """Function called before attaching a volume."""
+        LOG.debug("Enter in initialize connection with disco, "
+                  "connector is %s.", connector)
+        data = {
+            'driver_volume_type': 'disco',
+            'data': self._get_connection_properties(volume)
+        }
+        LOG.debug("Initialize connection [data]: %s.", data)
+        return data
+
+    def _get_connection_properties(self, volume):
+        """Return a dictionnary with the connection properties."""
+        connection_properties = dict(self.connection_properties)
+        connection_properties['name'] = volume['name']
+        connection_properties['disco_id'] = volume['provider_location']
+        return connection_properties
+
+    def terminate_connection(self, volume, connector, **kwargs):
+        """Function called after attaching a volume."""
+        LOG.debug("Enter in terminate connection with disco.")
+
+    def _update_volume_stats(self):
+        LOG.debug("Enter in update volume stats.")
+        stats = {}
+        backend_name = self.configuration.safe_get('volume_backend_name')
+        stats['volume_backend_name'] = backend_name or 'disco'
+        stats['storage_protocol'] = 'disco'
+        stats['driver_version'] = self.VERSION
+        stats['reserved_percentage'] = 0
+        stats['vendor_name'] = 'ITRI'
+        stats['QoS_support'] = False
+
+        try:
+            reply = self.client.service.systemInformationList()
+            status = reply['status']
+
+            if status != 0:
+                msg = (_("Error while getting "
+                         "disco information [%s].") %
+                       six.text_type(status))
+                LOG.error(msg)
+                raise exception.VolumeBackendAPIException(data=msg)
+
+            info_list = reply['propertyListResult']['PropertyInfoList']
+            for info in info_list:
+                if info['name'] == 'freeCapacityGB':
+                    stats['free_capacity_gb'] = float(info['value'])
+                elif info['name'] == 'totalCapacityGB':
+                    stats['total_capacity_gb'] = float(info['value'])
+        except Exception:
+            stats['total_capacity_gb'] = 'unknown'
+            stats['free_capacity_gb'] = 'unknown'
+
+        self._stats = stats
+
+    def get_volume_stats(self, refresh=False):
+        """Get backend information."""
+        if refresh:
+            self._update_volume_stats()
+        return self._stats
+
+    def local_path(self, volume):
+        """Return the path to the DISCO volume."""
+        return "/dev/dms%s" % volume['name']
+
+    def ensure_export(self, context, volume):
+        """Ensure an export."""
+        pass
+
+    def create_export(self, context, volume, connector):
+        """Export the volume."""
+        pass
+
+    def remove_export(self, context, volume):
+        """Remove an export for a logical volume."""
+        pass
+
+    def is_timeout(self, start_time, timeout):
+        """Check whether we reach the timeout."""
+        current_time = int(time.time())
+        if current_time - start_time > timeout:
+            return True
+        else:
+            return False
+
+    def _retry_get_detail(self, start_time, timeout, operation, params):
+        """Keep trying to query an item detail unless we reach the timeout."""
+        reply = self._call_api(operation, params)
+        status = reply['status']
+
+        msg = (_("Error while getting %(op)s details, "
+                 "returned code: %(status)s.") %
+               {'op': operation, 'status': six.text_type(status)})
+
+        if status != 0:
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        item_status = self._get_item_status(operation, reply)
+        if item_status == DISCO_CODE_MAPPING['request.failure']:
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        elif item_status == DISCO_CODE_MAPPING['request.success']:
+            raise loopingcall.LoopingCallDone(retvalue=reply)
+        elif self.is_timeout(start_time, timeout):
+            msg = (_("Timeout while calling %s ") % operation)
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+    def _call_api(self, operation, params):
+        """Make the call to the SOAP api."""
+        if operation == 'snapshot_detail':
+            return self.client.service.snapshotDetail(params['snapshot_id'])
+        if operation == 'restore_detail':
+            return self.client.service.restoreDetail(params['restore_id'])
+        if operation == 'clone_detail':
+            return self.client.service.cloneDetail(params['clone_id'],
+                                                   params['vol_name'])
+        else:
+            msg = (_("Unknown operation %s."), operation)
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+    def _get_item_status(self, operation, reply):
+        """Make the call to the SOAP api."""
+        if reply is None:
+            msg = (_("Call returned a None object"))
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+        elif operation == 'snapshot_detail':
+            return reply['snapshotInfoResult']['status']
+        elif operation == 'restore_detail':
+            return reply['restoreInfoResult']['status']
+        elif operation == 'clone_detail':
+            return int(reply['result'])
+        else:
+            msg = (_("Unknown operation "
+                     "%s."), operation)
+            LOG.error(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
diff --git a/releasenotes/notes/disco-cinder-driver-9dac5fb04511de1f.yaml b/releasenotes/notes/disco-cinder-driver-9dac5fb04511de1f.yaml
new file mode 100644 (file)
index 0000000..9c8a641
--- /dev/null
@@ -0,0 +1,3 @@
+---
+features:
+ - Added backend driver for DISCO storage.