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
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 \
[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,
--- /dev/null
+# 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."""
--- /dev/null
+# (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)
--- /dev/null
+# (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)
--- /dev/null
+# (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)
--- /dev/null
+# (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)
--- /dev/null
+# (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)
--- /dev/null
+# (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)
--- /dev/null
+# (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)
--- /dev/null
+# 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)
--- /dev/null
+---
+features:
+ - Added backend driver for DISCO storage.