From: Thelo Gaultier Date: Fri, 4 Dec 2015 07:29:44 +0000 (+0800) Subject: ITRI DISCO cinder driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=4495fad680d7a83ce54ac2d827f3300cbb9dc77f;p=openstack-build%2Fcinder-build.git ITRI DISCO cinder driver 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 --- diff --git a/cinder/opts.py b/cinder/opts.py index 9f65dca10..78e6d38ef 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -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 index 000000000..7229723d0 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/disco/__init__.py @@ -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 index 000000000..cfabf6cb4 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/disco/test_create_cloned_volume.py @@ -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 index 000000000..13087293e --- /dev/null +++ b/cinder/tests/unit/volume/drivers/disco/test_create_snapshot.py @@ -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 index 000000000..65d91c589 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/disco/test_create_volume.py @@ -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 index 000000000..2f89f312b --- /dev/null +++ b/cinder/tests/unit/volume/drivers/disco/test_create_volume_from_snapshot.py @@ -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 index 000000000..88cbeaab1 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/disco/test_delete_snapshot.py @@ -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 index 000000000..79675036e --- /dev/null +++ b/cinder/tests/unit/volume/drivers/disco/test_delete_volume.py @@ -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 index 000000000..8c4ad28ba --- /dev/null +++ b/cinder/tests/unit/volume/drivers/disco/test_extend_volume.py @@ -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 index 000000000..e69de29bb diff --git a/cinder/volume/drivers/disco/disco.py b/cinder/volume/drivers/disco/disco.py new file mode 100644 index 000000000..492830013 --- /dev/null +++ b/cinder/volume/drivers/disco/disco.py @@ -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 index 000000000..9c8a64189 --- /dev/null +++ b/releasenotes/notes/disco-cinder-driver-9dac5fb04511de1f.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added backend driver for DISCO storage.