From: Xing Yang Date: Sat, 16 May 2015 04:31:51 +0000 (-0400) Subject: EMC ScaleIO Cinder Driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=185abac1c21308049ce62b75c3bd1baacdb24097;p=openstack-build%2Fcinder-build.git EMC ScaleIO Cinder Driver This patch adds a Cinder volume driver for EMC ScaleIO product. The ScaleIO driver supports the following Cinder features: * Create/delete volume * Create/delete snapshot * Create volume from snapshot * Create cloned volume * Extend volume implements blueprint scaleio-cinder-volume-driver Change-Id: I7007e4ec36586d98cbf53e4f322ab25aa9b414c6 --- diff --git a/cinder/tests/unit/volume/drivers/emc/__init__.py b/cinder/tests/unit/volume/drivers/emc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/__init__.py b/cinder/tests/unit/volume/drivers/emc/scaleio/__init__.py new file mode 100644 index 000000000..07720b22b --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/__init__.py @@ -0,0 +1,161 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import copy +import requests + +from cinder import test +from cinder.tests.unit.volume.drivers.emc.scaleio import mocks + + +class CustomResponseMode(object): + """A context manager to define a custom set of per-request response modes. + + Example: + + with CustomResponseMode(self, **{ + 'some/api/path': RESPONSE_MODE.Valid, + 'another/api/path': RESPONSE_MODE.BadStatus, + 'last/api/path': MockResponse('some data', + status_code=403), + }): + self.assertRaises(SomeException, self.driver.api_call, data) + """ + def __init__(self, test_instance, **kwargs): + self.test_instance = test_instance + self.custom_responses = kwargs + self.current_responses = None + + def __enter__(self): + self.current_responses = self.test_instance.HTTPS_MOCK_RESPONSES + + https_responses = copy.deepcopy( + self.test_instance.HTTPS_MOCK_RESPONSES + ) + current_mode = self.test_instance.current_https_response_mode + + for call, new_mode in self.custom_responses.items(): + if isinstance(new_mode, mocks.MockHTTPSResponse): + https_responses[current_mode][call] = new_mode + else: + https_responses[current_mode][call] = \ + self.test_instance.get_https_response(call, new_mode) + + self.test_instance.HTTPS_MOCK_RESPONSES = https_responses + + def __exit__(self, exc_type, exc_val, exc_tb): + self.test_instance.HTTPS_MOCK_RESPONSES = self.current_responses + + +class TestScaleIODriver(test.TestCase): + """Base ``TestCase`` subclass for the ``ScaleIODriver``""" + RESPONSE_MODE = type(str('ResponseMode'), (object, ), dict( + Valid='0', + Invalid='1', + BadStatus='2', + )) + __RESPONSE_MODE_NAMES = { + '0': 'Valid', + '1': 'Invalid', + '2': 'BadStatus', + } + + BAD_STATUS_RESPONSE = mocks.MockHTTPSResponse( + { + 'errorCode': 500, + 'message': 'BadStatus Response Test', + }, 500 + ) + + HTTPS_MOCK_RESPONSES = {} + __COMMON_HTTPS_MOCK_RESPONSES = { + RESPONSE_MODE.Valid: { + 'login': 'login_token', + }, + RESPONSE_MODE.BadStatus: { + 'login': mocks.MockHTTPSResponse( + { + 'errorCode': 403, + 'message': 'Bad Login Response Test', + }, 403 + ), + }, + } + __https_response_mode = RESPONSE_MODE.Valid + log = None + + def setUp(self): + """Setup a test case environment. + + Creates a ``ScaleIODriver`` instance + Mocks the ``requests.get/post`` methods to return + ``MockHTTPSResponse``'s instead. + """ + super(TestScaleIODriver, self).setUp() + self.driver = mocks.ScaleIODriver() + + self.mock_object(requests, 'get', self.do_request) + self.mock_object(requests, 'post', self.do_request) + + def do_request(self, url, *args, **kwargs): + """Do a fake GET/POST API request. + + Splits `url` on '/api/' to get the what API call is, then returns + the value of `self.HTTPS_MOCK_RESPONSES[][]` + converting to a `MockHTTPSResponse` if necessary. + + :raises test.TestingException: If the current mode/api_call does not + exist. + :returns MockHTTPSResponse: + """ + return self.get_https_response(url.split('/api/')[1]) + + def set_https_response_mode(self, mode=RESPONSE_MODE.Valid): + """Set the HTTPS response mode. + + RESPONSE_MODE.Valid: Respond with valid data + RESPONSE_MODE.Invalid: Respond with invalid data + RESPONSE_MODE.BadStatus: Response with not-OK status code. + """ + self.__https_response_mode = mode + + def get_https_response(self, api_path, mode=None): + if mode is None: + mode = self.__https_response_mode + + try: + response = self.HTTPS_MOCK_RESPONSES[mode][api_path] + except KeyError: + try: + response = self.__COMMON_HTTPS_MOCK_RESPONSES[mode][api_path] + except KeyError: + raise test.TestingException( + 'Mock API Endpoint not implemented: [{}]{}'.format( + self.__RESPONSE_MODE_NAMES[mode], api_path + ) + ) + + if not isinstance(response, mocks.MockHTTPSResponse): + return mocks.MockHTTPSResponse(response, 200) + return response + + @property + def current_https_response_mode(self): + return self.__https_response_mode + + def https_response_mode_name(self, mode): + return self.__RESPONSE_MODE_NAMES[mode] + + def custom_response_mode(self, **kwargs): + return CustomResponseMode(self, **kwargs) diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/mocks.py b/cinder/tests/unit/volume/drivers/emc/scaleio/mocks.py new file mode 100644 index 000000000..91bdd687a --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/mocks.py @@ -0,0 +1,116 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import json +import requests +import six + +from cinder.volume import configuration as conf +from cinder.volume.drivers.emc import scaleio +from oslo_config import cfg + + +class ScaleIODriver(scaleio.ScaleIODriver): + """Mock ScaleIO Driver class. + + Provides some fake configuration options + """ + def __init__(self, *args, **kwargs): + configuration = conf.Configuration( + [ + cfg.StrOpt('fake', default=None), + ], + None + ) + + # Override the defaults to fake values + configuration.set_override('san_ip', override='127.0.0.1') + configuration.set_override('sio_rest_server_port', override='8888') + configuration.set_override('san_login', override='test') + configuration.set_override('san_password', override='pass') + configuration.set_override('sio_storage_pool_id', override='test_pool') + configuration.set_override('sio_protection_domain_id', + override='test_domain') + configuration.set_override('sio_storage_pools', + override='test_domain:test_pool') + + super(ScaleIODriver, self).__init__(configuration=configuration, + *args, + **kwargs) + + def update_consistencygroup(self, context, group, add_volumes=None, + remove_volumes=None): + pass + + def local_path(self, volume): + pass + + def reenable_replication(self, context, volume): + pass + + def manage_existing(self, volume, existing_ref): + pass + + def promote_replica(self, context, volume): + pass + + def delete_consistencygroup(self, context, group): + pass + + def create_consistencygroup_from_src(self, context, group, volumes, + cgsnapshot=None, snapshots=None): + pass + + def create_replica_test_volume(self, volume, src_vref): + pass + + def create_consistencygroup(self, context, group): + pass + + def manage_existing_get_size(self, volume, existing_ref): + pass + + def unmanage(self, volume): + pass + + def create_cgsnapshot(self, context, cgsnapshot): + pass + + def delete_cgsnapshot(self, context, cgsnapshot): + pass + + +class MockHTTPSResponse(requests.Response): + """Mock HTTP Response + + Defines the https replies from the mocked calls to do_request() + """ + def __init__(self, content, status_code=200): + super(MockHTTPSResponse, self).__init__() + + self._content = content + self.status_code = status_code + + def json(self, **kwargs): + if isinstance(self._content, six.string_types): + return super(MockHTTPSResponse, self).json(**kwargs) + + return self._content + + @property + def text(self): + if not isinstance(self._content, six.string_types): + return json.dumps(self._content) + + return super(MockHTTPSResponse, self).text diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_cloned_volume.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_cloned_volume.py new file mode 100644 index 000000000..8c68368db --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_cloned_volume.py @@ -0,0 +1,91 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import urllib + +import six + +from cinder import context +from cinder import exception +from cinder.tests.unit import fake_volume +from cinder.tests.unit.volume.drivers.emc import scaleio + + +class TestCreateClonedVolume(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.create_cloned_volume()``""" + STORAGE_POOL_ID = six.text_type('1') + STORAGE_POOL_NAME = 'SP1' + + PROT_DOMAIN_ID = six.text_type('1') + PROT_DOMAIN_NAME = 'PD1' + + def setUp(self): + """Setup a test case environment. + + Creates fake volume objects and sets up the required API responses. + """ + super(TestCreateClonedVolume, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + + self.src_volume = fake_volume.fake_volume_obj(ctx) + self.src_volume_name_2x_enc = urllib.quote( + urllib.quote( + self.driver.id_to_base64(self.src_volume.id) + ) + ) + + self.new_volume = fake_volume.fake_volume_obj( + ctx, **{'id': 'cloned', 'name': 'cloned_volume'} + ) + + self.new_volume_name_2x_enc = urllib.quote( + urllib.quote( + self.driver.id_to_base64(self.new_volume.id) + ) + ) + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'types/Volume/instances/getByName::' + + self.src_volume_name_2x_enc: self.src_volume.id, + 'instances/System/action/snapshotVolumes': '"{}"'.format( + self.new_volume.id + ), + }, + self.RESPONSE_MODE.BadStatus: { + 'instances/System/action/snapshotVolumes::': + self.BAD_STATUS_RESPONSE, + 'types/Volume/instances/getByName::' + + self.src_volume_name_2x_enc: self.BAD_STATUS_RESPONSE, + }, + self.RESPONSE_MODE.Invalid: { + 'types/Volume/instances/getByName::' + + self.src_volume_name_2x_enc: None, + }, + } + + def test_bad_login(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + self.new_volume, self.src_volume) + + def test_invalid_source_volume(self): + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + self.new_volume, self.src_volume) + + def test_create_cloned_volume(self): + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + self.driver.create_cloned_volume(self.new_volume, self.src_volume) diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_snapshot.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_snapshot.py new file mode 100644 index 000000000..1ca9dfdc0 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_snapshot.py @@ -0,0 +1,103 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import urllib + +import six + +from cinder import context +from cinder import exception +from cinder.tests.unit import fake_snapshot +from cinder.tests.unit.volume.drivers.emc import scaleio +from cinder.tests.unit.volume.drivers.emc.scaleio import mocks + + +class TestCreateSnapShot(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.create_snapshot()``""" + STORAGE_POOL_ID = six.text_type('1') + STORAGE_POOL_NAME = 'SP1' + + PROT_DOMAIN_ID = six.text_type('1') + PROT_DOMAIN_NAME = 'PD1' + + def setUp(self): + """Setup a test case environment. + + Creates fake volume and snapshot objects and sets up the required + API responses. + """ + super(TestCreateSnapShot, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + + self.snapshot = fake_snapshot.fake_snapshot_obj(ctx) + self.volume_name_2x_enc = urllib.quote( + urllib.quote(self.driver.id_to_base64(self.snapshot.volume_id)) + ) + self.snapshot_name_2x_enc = urllib.quote( + urllib.quote(self.driver.id_to_base64(self.snapshot.id)) + ) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: '"{}"'.format( + self.snapshot.volume_id + ), + 'instances/System/action/snapshotVolumes': self.snapshot.id, + 'types/Volume/instances/getByName::' + + self.snapshot_name_2x_enc: self.snapshot.id, + }, + self.RESPONSE_MODE.BadStatus: { + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: self.BAD_STATUS_RESPONSE, + 'types/Volume/instances/getByName::' + + self.snapshot_name_2x_enc: mocks.MockHTTPSResponse( + { + 'errorCode': 401, + 'message': 'BadStatus Snapshot Test', + }, 401 + ), + }, + self.RESPONSE_MODE.Invalid: { + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: None, + 'instances/System/action/snapshotVolumes': + mocks.MockHTTPSResponse( + { + 'errorCode': 400, + 'message': 'Invalid Volume Snapshot Test' + }, 400 + ), + }, + } + + def test_bad_login(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.create_snapshot, + self.snapshot + ) + + def test_invalid_volume(self): + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.create_snapshot, + self.snapshot + ) + + def test_create_snapshot(self): + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + self.driver.create_snapshot(self.snapshot) diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_volume.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_volume.py new file mode 100644 index 000000000..32956dd6f --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_volume.py @@ -0,0 +1,138 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +from cinder import context +from cinder import exception +from cinder.tests.unit import fake_volume + +from cinder.tests.unit.volume.drivers.emc import scaleio + + +class TestCreateVolume(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.create_volume()``""" + + PROTECTION_DOMAIN_ID = 'test_prot_id' + PROTECTION_DOMAIN_NAME = 'test_prot_name' + + STORAGE_POOL_ID = 'test_pool_id' + STORAGE_POOL_NAME = 'test_pool_name' + + def setUp(self): + """Setup a test case environment. + + Creates a fake volume object and sets up the required API responses. + """ + super(TestCreateVolume, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + + self.volume = fake_volume.fake_volume_obj(ctx) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'types/Volume/instances/getByName::' + + self.volume.name: '"{}"'.format(self.volume.id), + 'types/Volume/instances': [ + { + 'Id': self.volume.id, + 'Name': self.volume.name, + 'volumeSizeInKb': self.volume.size, + 'isObfuscated': False, + 'creationTime': self.volume.launched_at, + 'mappingToAllSdcsEnabled': False, + 'mappingSdcInfoList': [], + 'mappingScsiInitiatorList': [], + 'ancestorVolumeId': '', + 'vtreeId': '', + 'storagePoolId': self.STORAGE_POOL_ID, + 'useRmcache': False, + } + ], + 'types/Domain/instances/getByName::' + + self.PROTECTION_DOMAIN_NAME: + '"{}"'.format(self.PROTECTION_DOMAIN_ID), + 'types/Pool/instances/getByName::{},{}'.format( + self.PROTECTION_DOMAIN_ID, + self.STORAGE_POOL_NAME + ): '"{}"'.format(self.STORAGE_POOL_ID), + }, + self.RESPONSE_MODE.Invalid: { + 'types/Domain/instances/getByName::' + + self.PROTECTION_DOMAIN_NAME: None, + 'types/Pool/instances/getByName::{},{}'.format( + self.PROTECTION_DOMAIN_ID, + self.STORAGE_POOL_NAME + ): None, + }, + self.RESPONSE_MODE.BadStatus: { + 'types/Volume/instances': self.BAD_STATUS_RESPONSE, + 'types/Domain/instances/getByName::' + + self.PROTECTION_DOMAIN_NAME: self.BAD_STATUS_RESPONSE, + 'types/Pool/instances/getByName::{},{}'.format( + self.PROTECTION_DOMAIN_ID, + self.STORAGE_POOL_NAME + ): self.BAD_STATUS_RESPONSE, + }, + } + + def test_no_domain(self): + """No protection domain name or ID provided.""" + self.driver.protection_domain_name = None + self.driver.protection_domain_id = None + self.assertRaises(exception.VolumeBackendAPIException, + self.test_create_volume) + + def test_no_domain_id(self): + """Only protection domain name provided.""" + self.driver.protection_domain_id = None + self.driver.protection_domain_name = self.PROTECTION_DOMAIN_NAME + self.driver.storage_pool_name = None + self.driver.storage_pool_id = self.STORAGE_POOL_ID + self.test_create_volume() + + def test_no_domain_id_invalid_response(self): + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.assertRaises(exception.VolumeBackendAPIException, + self.test_no_domain_id) + + def test_no_domain_id_badstatus_response(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.VolumeBackendAPIException, + self.test_no_domain_id) + + def test_no_storage_id(self): + """Only protection domain name provided.""" + self.driver.storage_pool_id = None + self.driver.storage_pool_name = self.STORAGE_POOL_NAME + self.driver.protection_domain_id = self.PROTECTION_DOMAIN_ID + self.driver.protection_domain_name = None + self.test_create_volume() + + def test_no_storage_id_invalid_response(self): + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.assertRaises(exception.VolumeBackendAPIException, + self.test_no_storage_id) + + def test_no_storage_id_badstatus_response(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.VolumeBackendAPIException, + self.test_no_storage_id) + + def test_create_volume(self): + """Valid create volume parameters""" + self.driver.create_volume(self.volume) + + def test_create_volume_badstatus_response(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.VolumeBackendAPIException, + self.test_create_volume) diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_volume_from_snapshot.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_volume_from_snapshot.py new file mode 100644 index 000000000..431b32d9a --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_create_volume_from_snapshot.py @@ -0,0 +1,91 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import urllib + +import six + +from cinder import context +from cinder import exception +from cinder.tests.unit import fake_snapshot +from cinder.tests.unit import fake_volume +from cinder.tests.unit.volume.drivers.emc import scaleio + + +class TestCreateVolumeFromSnapShot(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.create_volume_from_snapshot()``""" + STORAGE_POOL_ID = six.text_type('1') + STORAGE_POOL_NAME = 'SP1' + + PROT_DOMAIN_ID = six.text_type('1') + PROT_DOMAIN_NAME = 'PD1' + + def setUp(self): + """Setup a test case environment. + + Creates fake volume and snapshot objects and sets up the required + API responses. + """ + super(TestCreateVolumeFromSnapShot, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + + self.snapshot = fake_snapshot.fake_snapshot_obj(ctx) + self.snapshot_name_2x_enc = urllib.quote( + urllib.quote(self.driver.id_to_base64(self.snapshot.id)) + ) + self.volume = fake_volume.fake_volume_obj(ctx) + self.volume_name_2x_enc = urllib.quote( + urllib.quote(self.driver.id_to_base64(self.volume.id)) + ) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'types/Volume/instances/getByName::' + + self.snapshot_name_2x_enc: self.snapshot.id, + 'instances/System/action/snapshotVolumes': self.volume.id, + }, + self.RESPONSE_MODE.BadStatus: { + 'instances/System/action/snapshotVolumes::': + self.BAD_STATUS_RESPONSE, + 'types/Volume/instances/getByName::' + + self.snapshot_name_2x_enc: self.BAD_STATUS_RESPONSE, + self.snapshot_name_2x_enc: self.BAD_STATUS_RESPONSE, + }, + self.RESPONSE_MODE.Invalid: { + 'types/Volume/instances/getByName::' + + self.snapshot_name_2x_enc: None, + }, + } + + def test_bad_login(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + self.volume, + self.snapshot + ) + + def test_invalid_snapshot(self): + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + self.volume, + self.snapshot + ) + + def test_create_volume_from_snapshot(self): + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + self.driver.create_volume_from_snapshot(self.volume, self.snapshot) diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_snapshot.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_snapshot.py new file mode 100644 index 000000000..8e46c7ad4 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_snapshot.py @@ -0,0 +1,99 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import urllib + +import six + +from cinder import context +from cinder import exception +from cinder.tests.unit.fake_snapshot import fake_snapshot_obj +from cinder.tests.unit.volume.drivers.emc import scaleio +from cinder.tests.unit.volume.drivers.emc.scaleio import mocks + + +class TestDeleteSnapShot(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.delete_snapshot()``""" + STORAGE_POOL_ID = six.text_type('1') + STORAGE_POOL_NAME = 'SP1' + + PROT_DOMAIN_ID = six.text_type('1') + PROT_DOMAIN_NAME = 'PD1' + VOLUME_NOT_FOUND_ERROR = 3 + + def setUp(self): + """Setup a test case environment. + + Creates fake volume and snapshot objects and sets up the required + API responses. + """ + super(TestDeleteSnapShot, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + + self.snapshot = fake_snapshot_obj(ctx) + self.snapshot_name_2x_enc = urllib.quote( + urllib.quote( + self.driver.id_to_base64(self.snapshot.id) + ) + ) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'types/Volume/instances/getByName::' + + self.snapshot_name_2x_enc: self.snapshot.id, + 'instances/Volume::{}/action/removeMappedSdc'.format( + self.snapshot.id + ): self.snapshot.id, + 'instances/Volume::{}/action/removeVolume'.format( + self.snapshot.id + ): self.snapshot.id, + }, + self.RESPONSE_MODE.BadStatus: { + 'types/Volume/instances/getByName::' + + self.snapshot_name_2x_enc: self.BAD_STATUS_RESPONSE, + }, + self.RESPONSE_MODE.Invalid: { + 'types/Volume/instances/getByName::' + + self.snapshot_name_2x_enc: mocks.MockHTTPSResponse( + { + 'errorCode': self.VOLUME_NOT_FOUND_ERROR, + 'message': 'Test Delete Invalid Snapshot', + }, 400 + ), + }, + } + + def test_bad_login(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_snapshot, self.snapshot) + + def test_delete_invalid_snapshot_force_delete(self): + self.driver.configuration.set_override('sio_force_delete', + override=True) + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.driver.delete_snapshot(self.snapshot) + + def test_delete_invalid_snapshot(self): + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_snapshot, self.snapshot) + + def test_delete_snapshot(self): + """Setting the unmap volume before delete flag for tests """ + self.driver.configuration.set_override( + 'sio_unmap_volume_before_deletion', + override=True) + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + self.driver.delete_snapshot(self.snapshot) diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_volume.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_volume.py new file mode 100644 index 000000000..f082b7b7d --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_volume.py @@ -0,0 +1,79 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import urllib + +import six + +from cinder import context +from cinder import exception +from cinder.tests.unit import fake_volume +from cinder.tests.unit.volume.drivers.emc import scaleio +from cinder.tests.unit.volume.drivers.emc.scaleio import mocks + + +class TestDeleteVolume(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.delete_volume()``""" + STORAGE_POOL_ID = six.text_type('1') + STORAGE_POOL_NAME = 'SP1' + + PROT_DOMAIN_ID = six.text_type('1') + PROT_DOMAIN_NAME = 'PD1' + + def setUp(self): + """Setup a test case environment. + + Creates a fake volume object and sets up the required API responses. + """ + super(TestDeleteVolume, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + + self.volume = fake_volume.fake_volume_obj(ctx) + self.volume_name_2x_enc = urllib.quote( + urllib.quote(self.driver.id_to_base64(self.volume.id)) + ) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: self.volume.id, + 'instances/Volume::{}/action/removeMappedSdc'.format( + self.volume.id): self.volume.id, + 'instances/Volume::{}/action/removeVolume'.format( + self.volume.id + ): self.volume.id, + }, + self.RESPONSE_MODE.BadStatus: { + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: mocks.MockHTTPSResponse( + { + 'errorCode': 401, + 'message': 'BadStatus Volume Test', + }, 401 + ), + }, + } + + def test_bad_login_and_volume(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_volume, + self.volume) + + def test_delete_volume(self): + """Setting the unmap volume before delete flag for tests """ + self.driver.configuration.set_override( + 'sio_unmap_volume_before_deletion', + override=True) + self.driver.delete_volume(self.volume) diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_extend_volume.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_extend_volume.py new file mode 100644 index 000000000..c1dc87482 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_extend_volume.py @@ -0,0 +1,110 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import urllib + +import six + +from cinder import context +from cinder import exception +from cinder.tests.unit.fake_volume import fake_volume_obj +from cinder.tests.unit.volume.drivers.emc import scaleio +from cinder.tests.unit.volume.drivers.emc.scaleio import mocks + + +class TestExtendVolume(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.extend_volume()``""" + STORAGE_POOL_ID = six.text_type('1') + STORAGE_POOL_NAME = 'SP1' + + PROT_DOMAIN_ID = six.text_type('1') + PROT_DOMAIN_NAME = 'PD1' + + """ New sizes for the volume. + Since ScaleIO has a granularity of 8 GB, multiples of 8 always work. + The 7 size should be either rounded up to 8 or raise an exception + based on the round_volume_capacity config setting. + """ + NEW_SIZE = 8 + BAD_SIZE = 7 + + def setUp(self): + """Setup a test case environment. + + Creates fake volume object and sets up the required API responses. + """ + super(TestExtendVolume, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + + self.volume = fake_volume_obj(ctx, **{'id': 'fake_volume'}) + self.volume_name_2x_enc = urllib.quote( + urllib.quote(self.driver.id_to_base64(self.volume.id)) + ) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: '"{}"'.format(self.volume.id), + 'instances/Volume::{}/action/setVolumeSize'.format( + self.volume.id + ): 'OK', + }, + self.RESPONSE_MODE.BadStatus: { + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: self.BAD_STATUS_RESPONSE, + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: mocks.MockHTTPSResponse( + { + 'errorCode': 401, + 'message': 'BadStatus Extend Volume Test', + }, 401 + ), + }, + self.RESPONSE_MODE.Invalid: { + 'types/Volume/instances/getByName::' + + self.volume_name_2x_enc: None, + }, + } + + def test_bad_login(self): + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.extend_volume, + self.volume, + self.NEW_SIZE) + + def test_invalid_volume(self): + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.extend_volume, + self.volume, + self.NEW_SIZE) + + def test_extend_volume_bad_size_no_round(self): + self.driver.configuration.set_override('sio_round_volume_capacity', + override=False) + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.extend_volume, + self.volume, + self.BAD_SIZE) + + def test_extend_volume_bad_size_round(self): + self.driver.configuration.set_override('sio_round_volume_capacity', + override=True) + self.driver.extend_volume(self.volume, self.BAD_SIZE) + + def test_extend_volume(self): + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + self.driver.extend_volume(self.volume, self.NEW_SIZE) diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_misc.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_misc.py new file mode 100644 index 000000000..eea2c2da0 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_misc.py @@ -0,0 +1,110 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +import urllib + +from cinder import exception +from cinder.tests.unit.volume.drivers.emc import scaleio + + +class TestMisc(scaleio.TestScaleIODriver): + DOMAIN_NAME = 'PD1' + POOL_NAME = 'SP1' + STORAGE_POOLS = ['{}:{}'.format(DOMAIN_NAME, POOL_NAME)] + + def setUp(self): + """Set up the test case environment. + + Defines the mock HTTPS responses for the REST API calls. + """ + super(TestMisc, self).setUp() + + self.domain_name_enc = urllib.quote(self.DOMAIN_NAME) + self.pool_name_enc = urllib.quote(self.POOL_NAME) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'types/Domain/instances/getByName::' + + self.domain_name_enc: '"{}"'.format(self.DOMAIN_NAME).encode( + 'ascii', + 'ignore' + ), + 'types/Pool/instances/getByName::{},{}'.format( + self.DOMAIN_NAME, + self.POOL_NAME + ): '"{}"'.format(self.POOL_NAME).encode('ascii', 'ignore'), + 'types/StoragePool/instances/action/querySelectedStatistics': { + '"{}"'.format(self.POOL_NAME): { + 'capacityInUseInKb': 502, + 'capacityLimitInKb': 1024, + }, + }, + }, + self.RESPONSE_MODE.BadStatus: { + 'types/Domain/instances/getByName::' + + self.domain_name_enc: self.BAD_STATUS_RESPONSE, + }, + self.RESPONSE_MODE.Invalid: { + 'types/Domain/instances/getByName::' + + self.domain_name_enc: None, + }, + } + + def test_valid_configuration(self): + self.driver.check_for_setup_error() + + def test_both_storage_pool(self): + """Both storage name and ID provided.""" + self.driver.storage_pool_id = "test_pool_id" + self.driver.storage_pool_name = "test_pool_name" + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + + def test_no_storage_pool(self): + """No storage name or ID provided.""" + self.driver.storage_pool_name = None + self.driver.storage_pool_id = None + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + + def test_both_domain(self): + self.driver.protection_domain_name = "test_domain_name" + self.driver.protection_domain_id = "test_domain_id" + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + + def test_volume_size_round_true(self): + self.driver._check_volume_size(1) + + def test_volume_size_round_false(self): + self.driver.configuration.set_override('sio_round_volume_capacity', + override=False) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver._check_volume_size, 1) + + def test_get_volume_stats_bad_status(self): + self.driver.storage_pools = self.STORAGE_POOLS + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.get_volume_stats, True) + + def test_get_volume_stats_invalid_domain(self): + self.driver.storage_pools = self.STORAGE_POOLS + self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.get_volume_stats, True) + + def test_get_volume_stats(self): + self.driver.storage_pools = self.STORAGE_POOLS + self.driver.get_volume_stats(True) diff --git a/cinder/volume/drivers/emc/scaleio.py b/cinder/volume/drivers/emc/scaleio.py new file mode 100644 index 000000000..23b3c5a23 --- /dev/null +++ b/cinder/volume/drivers/emc/scaleio.py @@ -0,0 +1,1154 @@ +# Copyright (c) 2013 - 2015 EMC Corporation. +# 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. +""" +Driver for EMC ScaleIO based on ScaleIO remote CLI. +""" + +import base64 +import json +import os + +import eventlet +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units +import requests +import six +import urllib + +from cinder import context +from cinder import exception +from cinder.i18n import _, _LI, _LW +from cinder.image import image_utils +from cinder import utils +from cinder.volume import driver +from cinder.volume.drivers.san import san +from cinder.volume import volume_types + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) + +scaleio_opts = [ + cfg.StrOpt('sio_rest_server_port', + default='443', + help='REST server port.'), + cfg.BoolOpt('sio_verify_server_certificate', + default=False, + help='Whether to verify server certificate.'), + cfg.StrOpt('sio_server_certificate_path', + default=None, + help='Server certificate path.'), + cfg.BoolOpt('sio_round_volume_capacity', + default=True, + help='Whether to round volume capacity.'), + cfg.BoolOpt('sio_force_delete', + default=False, + help='Whether to allow force delete.'), + cfg.BoolOpt('sio_unmap_volume_before_deletion', + default=False, + help='Whether to unmap volume before deletion.'), + cfg.StrOpt('sio_protection_domain_id', + default=None, + help='Protection domain id.'), + cfg.StrOpt('sio_protection_domain_name', + default=None, + help='Protection domain name.'), + cfg.StrOpt('sio_storage_pools', + default=None, + help='Storage pools.'), + cfg.StrOpt('sio_storage_pool_name', + default=None, + help='Storage pool name.'), + cfg.StrOpt('sio_storage_pool_id', + default=None, + help='Storage pool id.') +] + +CONF.register_opts(scaleio_opts) + +STORAGE_POOL_NAME = 'sio:sp_name' +STORAGE_POOL_ID = 'sio:sp_id' +PROTECTION_DOMAIN_NAME = 'sio:pd_name' +PROTECTION_DOMAIN_ID = 'sio:pd_id' +PROVISIONING_KEY = 'sio:provisioning' +IOPS_LIMIT_KEY = 'sio:iops_limit' +BANDWIDTH_LIMIT = 'sio:bandwidth_limit' + +BLOCK_SIZE = 8 +OK_STATUS_CODE = 200 +VOLUME_NOT_FOUND_ERROR = 3 +VOLUME_NOT_MAPPED_ERROR = 84 +VOLUME_ALREADY_MAPPED_ERROR = 81 + + +class ScaleIODriver(driver.VolumeDriver): + """EMC ScaleIO Driver.""" + + VERSION = "2.0" + + def __init__(self, *args, **kwargs): + super(ScaleIODriver, self).__init__(*args, **kwargs) + + self.configuration.append_config_values(san.san_opts) + self.configuration.append_config_values(scaleio_opts) + self.server_ip = self.configuration.san_ip + self.server_port = self.configuration.sio_rest_server_port + self.server_username = self.configuration.san_login + self.server_password = self.configuration.san_password + self.server_token = None + self.verify_server_certificate = ( + self.configuration.sio_verify_server_certificate) + self.server_certificate_path = None + if self.verify_server_certificate: + self.server_certificate_path = ( + self.configuration.sio_server_certificate_path) + LOG.info(_LI( + "REST server IP: %(ip)s, port: %(port)s, username: %(user)s. " + "Verify server's certificate: %(verify_cert)s."), + {'ip': self.server_ip, + 'port': self.server_port, + 'user': self.server_username, + 'verify_cert': self.verify_server_certificate}) + + self.storage_pools = [e.strip() for e in + self.configuration.sio_storage_pools.split(',')] + self.storage_pool_name = self.configuration.sio_storage_pool_name + self.storage_pool_id = self.configuration.sio_storage_pool_id + if (self.storage_pool_name is None and self.storage_pool_id is None): + LOG.warning(_LW("No storage pool name or id was found.")) + else: + LOG.info(_LI( + "Storage pools names: %(pools)s, " + "storage pool name: %(pool)s, pool id: %(pool_id)s."), + {'pools': self.storage_pools, + 'pool': self.storage_pool_name, + 'pool_id': self.storage_pool_id}) + + self.protection_domain_name = ( + self.configuration.sio_protection_domain_name) + LOG.info(_LI( + "Protection domain name: %(domain_name)s."), + {'domain_name': self.protection_domain_name}) + self.protection_domain_id = self.configuration.sio_protection_domain_id + LOG.info(_LI( + "Protection domain name: %(domain_id)s."), + {'domain_id': self.protection_domain_id}) + + def check_for_setup_error(self): + if (not self.protection_domain_name and + not self.protection_domain_id): + LOG.warning(_LW("No protection domain name or id " + "was specified in configuration.")) + + if self.protection_domain_name and self.protection_domain_id: + msg = _("Cannot specify both protection domain name " + "and protection domain id.") + raise exception.InvalidInput(reason=msg) + + if not self.server_ip: + msg = _("REST server IP must by specified.") + raise exception.InvalidInput(reason=msg) + + if not self.server_username: + msg = _("REST server username must by specified.") + raise exception.InvalidInput(reason=msg) + + if not self.server_password: + msg = _("REST server password must by specified.") + raise exception.InvalidInput(reason=msg) + + if not self.verify_server_certificate: + LOG.warning(_LW("Verify certificate is not set, using default of " + "False.")) + + if self.verify_server_certificate and not self.server_certificate_path: + msg = _("Path to REST server's certificate must be specified.") + raise exception.InvalidInput(reason=msg) + + if self.storage_pool_name and self.storage_pool_id: + msg = _("Cannot specify both storage pool name and storage " + "pool id.") + raise exception.InvalidInput(reason=msg) + + if not self.storage_pool_name and not self.storage_pool_id: + msg = _("Must specify storage pool name or id.") + raise exception.InvalidInput(reason=msg) + + def _find_storage_pool_id_from_storage_type(self, storage_type): + # Default to what was configured in configuration file if not defined. + return storage_type.get(STORAGE_POOL_ID, + self.storage_pool_id) + + def _find_storage_pool_name_from_storage_type(self, storage_type): + return storage_type.get(STORAGE_POOL_NAME, + self.storage_pool_name) + + def _find_protection_domain_id_from_storage_type(self, storage_type): + # Default to what was configured in configuration file if not defined. + return storage_type.get(PROTECTION_DOMAIN_ID, + self.protection_domain_id) + + def _find_protection_domain_name_from_storage_type(self, storage_type): + # Default to what was configured in configuration file if not defined. + return storage_type.get(PROTECTION_DOMAIN_NAME, + self.protection_domain_name) + + def _find_provisioning_type(self, storage_type): + return storage_type.get(PROVISIONING_KEY) + + def _find_iops_limit(self, storage_type): + return storage_type.get(IOPS_LIMIT_KEY) + + def _find_bandwidth_limit(self, storage_type): + return storage_type.get(BANDWIDTH_LIMIT) + + def id_to_base64(self, id): + # Base64 encode the id to get a volume name less than 32 characters due + # to ScaleIO limitation. + name = six.text_type(id).replace("-", "") + try: + name = base64.b16decode(name.upper()) + except TypeError: + pass + encoded_name = base64.b64encode(name) + LOG.debug( + "Converted id %(id)s to scaleio name %(name)s.", + {'id': id, 'name': encoded_name}) + return encoded_name + + def create_volume(self, volume): + """Creates a scaleIO volume.""" + self._check_volume_size(volume.size) + + volname = self.id_to_base64(volume.id) + + storage_type = self._get_volumetype_extraspecs(volume) + storage_pool_name = self._find_storage_pool_name_from_storage_type( + storage_type) + storage_pool_id = self._find_storage_pool_id_from_storage_type( + storage_type) + protection_domain_id = ( + self._find_protection_domain_id_from_storage_type(storage_type)) + protection_domain_name = ( + self._find_protection_domain_name_from_storage_type(storage_type)) + provisioning_type = self._find_provisioning_type(storage_type) + + LOG.info(_LI( + "Volume type: %(volume_type)s, storage pool name: %(pool_name)s, " + "storage pool id: %(pool_id)s, protection domain id: " + "%(domain_id)s, protection domain name: %(domain_name)s."), + {'volume_type': storage_type, + 'pool_name': storage_pool_name, + 'pool_id': storage_pool_id, + 'domain_id': protection_domain_id, + 'domain_name': protection_domain_name}) + + verify_cert = self._get_verify_cert() + + if storage_pool_name: + self.storage_pool_name = storage_pool_name + self.storage_pool_id = None + if storage_pool_id: + self.storage_pool_id = storage_pool_id + self.storage_pool_name = None + if protection_domain_name: + self.protection_domain_name = protection_domain_name + self.protection_domain_id = None + if protection_domain_id: + self.protection_domain_id = protection_domain_id + self.protection_domain_name = None + + domain_id = self.protection_domain_id + if not domain_id: + if not self.protection_domain_name: + msg = _("Must specify protection domain name or" + " protection domain id.") + raise exception.VolumeBackendAPIException(data=msg) + + encoded_domain_name = urllib.quote(self.protection_domain_name, '') + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'encoded_domain_name': encoded_domain_name} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/types/Domain/instances/getByName::" + "%(encoded_domain_name)s") % req_vars + LOG.info(_LI("ScaleIO get domain id by name request: %s."), + request) + r = requests.get( + request, + auth=( + self.server_username, + self.server_token), + verify=verify_cert) + r = self._check_response(r, request) + + domain_id = r.json() + if not domain_id: + msg = (_("Domain with name %s wasn't found.") + % self.protection_domain_name) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + if r.status_code != OK_STATUS_CODE and "errorCode" in domain_id: + msg = (_("Error getting domain id from name %(name)s: %(id)s.") + % {'name': self.protection_domain_name, + 'id': domain_id['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.info(_LI("Domain id is %s."), domain_id) + pool_name = self.storage_pool_name + pool_id = self.storage_pool_id + if pool_name: + encoded_domain_name = urllib.quote(pool_name, '') + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'domain_id': domain_id, + 'encoded_domain_name': encoded_domain_name} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/types/Pool/instances/getByName::" + "%(domain_id)s,%(encoded_domain_name)s") % req_vars + LOG.info(_LI("ScaleIO get pool id by name request: %s."), request) + r = requests.get( + request, + auth=( + self.server_username, + self.server_token), + verify=verify_cert) + pool_id = r.json() + if not pool_id: + msg = (_("Pool with name %(pool_name)s wasn't found in " + "domain %(domain_id)s.") + % {'pool_name': pool_name, + 'domain_id': domain_id}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + if r.status_code != OK_STATUS_CODE and "errorCode" in pool_id: + msg = (_("Error getting pool id from name %(pool_name)s: " + "%(err_msg)s.") + % {'pool_name': pool_name, + 'err_msg': pool_id['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.info(_LI("Pool id is %s."), pool_id) + if provisioning_type == 'thin': + provisioning = "ThinProvisioned" + # Default volume type is thick. + else: + provisioning = "ThickProvisioned" + + # units.Mi = 1024 ** 2 + volume_size_kb = volume.size * units.Mi + params = {'protectionDomainId': domain_id, + 'volumeSizeInKb': six.text_type(volume_size_kb), + 'name': volname, + 'volumeType': provisioning, + 'storagePoolId': pool_id} + + LOG.info(_LI("Params for add volume request: %s."), params) + r = requests.post( + "https://" + + self.server_ip + + ":" + + self.server_port + + "/api/types/Volume/instances", + data=json.dumps(params), + headers=self._get_headers(), + auth=( + self.server_username, + self.server_token), + verify=verify_cert) + response = r.json() + LOG.info(_LI("Add volume response: %s"), response) + + if r.status_code != OK_STATUS_CODE and "errorCode" in response: + msg = (_("Error creating volume: %s.") % response['message']) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.info(_LI("Created volume %(volname)s, volume id %(volid)s."), + {'volname': volname, 'volid': volume.id}) + + def _check_volume_size(self, size): + if size % 8 != 0: + round_volume_capacity = ( + self.configuration.sio_round_volume_capacity) + if not round_volume_capacity: + exception_msg = (_( + "Cannot create volume of size %s: not multiple of 8GB.") % + size) + LOG.error(exception_msg) + raise exception.VolumeBackendAPIException(data=exception_msg) + + def create_snapshot(self, snapshot): + """Creates a scaleio snapshot.""" + volname = self.id_to_base64(snapshot.volume_id) + snapname = self.id_to_base64(snapshot.id) + self._snapshot_volume(volname, snapname) + + def _snapshot_volume(self, volname, snapname): + vol_id = self._get_volume_id(volname) + params = { + 'snapshotDefs': [{"volumeId": vol_id, "snapshotName": snapname}]} + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/instances/System/action/snapshotVolumes") % req_vars + r = requests.post( + request, + data=json.dumps(params), + headers=self._get_headers(), + auth=( + self.server_username, + self.server_token), + verify=self._get_verify_cert()) + r = self._check_response(r, request, False, params) + response = r.json() + LOG.info(_LI("snapshot volume response: %s."), response) + if r.status_code != OK_STATUS_CODE and "errorCode" in response: + msg = (_("Failed creating snapshot for volume %(volname)s: " + "%(response)s.") % + {'volname': volname, + 'response': response['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def _check_response(self, response, request, is_get_request=True, + params=None): + if response.status_code == 401 or response.status_code == 403: + LOG.info(_LI("Token is invalid, going to re-login and get " + "a new one.")) + login_request = ( + "https://" + self.server_ip + + ":" + self.server_port + "/api/login") + verify_cert = self._get_verify_cert() + r = requests.get( + login_request, + auth=( + self.server_username, + self.server_password), + verify=verify_cert) + token = r.json() + self.server_token = token + # Repeat request with valid token. + LOG.info(_LI( + "Going to perform request again %s with valid token."), + request) + if is_get_request: + res = requests.get(request, + auth=(self.server_username, + self.server_token), + verify=verify_cert) + else: + res = requests.post(request, + data=json.dumps(params), + headers=self._get_headers(), + auth=(self.server_username, + self.server_token), + verify=verify_cert) + return res + return response + + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + # We interchange 'volume' and 'snapshot' because in ScaleIO + # snapshot is a volume: once a snapshot is generated it + # becomes a new unmapped volume in the system and the user + # may manipulate it in the same manner as any other volume + # exposed by the system + volname = self.id_to_base64(snapshot.id) + snapname = self.id_to_base64(volume.id) + LOG.info(_LI( + "ScaleIO create volume from snapshot: snapshot %(snapname)s " + "to volume %(volname)s."), + {'volname': volname, + 'snapname': snapname}) + self._snapshot_volume(volname, snapname) + + def _get_volume_id(self, volname): + volname_encoded = urllib.quote(volname, '') + volname_double_encoded = urllib.quote(volname_encoded, '') + LOG.info(_LI("Volume name after double encoding is %s."), + volname_double_encoded) + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'encoded': volname_double_encoded} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/types/Volume/instances/getByName::" + "%(encoded)s") % req_vars + LOG.info(_LI("ScaleIO get volume id by name request: %s"), request) + r = requests.get( + request, + auth=(self.server_username, + self.server_token), + verify=self._get_verify_cert()) + r = self._check_response(r, request) + + vol_id = r.json() + + if not vol_id: + msg = _("Volume with name %s wasn't found.") % volname + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + if r.status_code != OK_STATUS_CODE and "errorCode" in vol_id: + msg = (_("Error getting volume id from name %(volname)s: %(err)s.") + % {'volname': volname, + 'err': vol_id['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.info(_LI("volume id is %s."), vol_id) + return vol_id + + def _get_headers(self): + return {'content-type': 'application/json'} + + def _get_verify_cert(self): + verify_cert = False + if self.verify_server_certificate: + verify_cert = self.server_certificate_path + return verify_cert + + def extend_volume(self, volume, new_size): + """Extends the size of an existing available ScaleIO volume.""" + + self._check_volume_size(new_size) + + volname = self.id_to_base64(volume.id) + + LOG.info(_LI( + "ScaleIO extend volume: volume %(volname)s to size %(new_size)s."), + {'volname': volname, + 'new_size': new_size}) + + vol_id = self._get_volume_id(volname) + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'vol_id': vol_id} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/instances/Volume::%(vol_id)s" + "/action/setVolumeSize") % req_vars + LOG.info(_LI("Change volume capacity request: %s."), request) + volume_new_size = new_size + params = {'sizeInGB': six.text_type(volume_new_size)} + r = requests.post( + request, + data=json.dumps(params), + headers=self._get_headers(), + auth=(self.server_username, + self.server_token), + verify=self._get_verify_cert()) + r = self._check_response(r, request, False, params) + + if r.status_code != OK_STATUS_CODE: + response = r.json() + msg = (_("Error extending volume %(vol)s: %(err)s.") + % {'vol': volname, + 'err': response['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def create_cloned_volume(self, volume, src_vref): + """Creates a cloned volume.""" + volname = self.id_to_base64(src_vref.id) + snapname = self.id_to_base64(volume.id) + LOG.info(_LI( + "ScaleIO create cloned volume: source volume %(src)s to target " + "volume %(tgt)."), + {'src': volname, + 'tgt': snapname}) + self._snapshot_volume(volname, snapname) + + def delete_volume(self, volume): + """Deletes a self.logical volume""" + volname = self.id_to_base64(volume.id) + self._delete_volume(volname) + + def _delete_volume(self, volname): + volname_encoded = urllib.quote(volname, '') + volname_double_encoded = urllib.quote(volname_encoded, '') + LOG.info(_LI("Volume name after double encoding is %s."), + volname_double_encoded) + + verify_cert = self._get_verify_cert() + + # convert volume name to id + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'encoded': volname_double_encoded} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/types/Volume/instances/getByName::" + "%(encoded)s") % req_vars + LOG.info(_LI("ScaleIO get volume id by name request: %s."), request) + r = requests.get( + request, + auth=( + self.server_username, + self.server_token), + verify=verify_cert) + r = self._check_response(r, request) + LOG.info(_LI("Get by name response: %s."), r.text) + vol_id = r.json() + LOG.info(_LI("ScaleIO volume id to delete is %s."), vol_id) + + if r.status_code != OK_STATUS_CODE and "errorCode" in vol_id: + msg = (_("Error getting volume id from name %(vol)s: %(err)s.") + % {'vol': volname, 'err': vol_id['message']}) + LOG.error(msg) + + error_code = vol_id['errorCode'] + if (error_code == VOLUME_NOT_FOUND_ERROR): + force_delete = self.configuration.sio_force_delete + if force_delete: + LOG.warning(_LW( + "Ignoring error in delete volume %s: volume not found " + "due to force delete settings."), volname) + return + + raise exception.VolumeBackendAPIException(data=msg) + + unmap_before_delete = ( + self.configuration.sio_unmap_volume_before_deletion) + # Ensure that the volume is not mapped to any SDC before deletion in + # case unmap_before_deletion is enabled. + if unmap_before_delete: + params = {'allSdcs': ''} + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'vol_id': vol_id} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/instances/Volume::%(vol_id)s" + "/action/removeMappedSdc") % req_vars + LOG.info(_LI( + "Trying to unmap volume from all sdcs before deletion: %s."), + request) + r = requests.post( + request, + data=json.dumps(params), + headers=self._get_headers(), + auth=( + self.server_username, + self.server_token), + verify=verify_cert) + r = self._check_response(r, request, False, params) + LOG.debug("Unmap volume response: %s.", r.text) + + params = {'removeMode': 'ONLY_ME'} + r = requests.post( + "https://" + + self.server_ip + + ":" + + self.server_port + + "/api/instances/Volume::" + + six.text_type(vol_id) + + "/action/removeVolume", + data=json.dumps(params), + headers=self._get_headers(), + auth=(self.server_username, + self.server_token), + verify=verify_cert) + r = self._check_response(r, request, False, params) + + if r.status_code != OK_STATUS_CODE: + response = r.json() + error_code = response['errorCode'] + if error_code == 78: + force_delete = self.configuration.sio_orce_delete + if force_delete: + LOG.warning(_LW( + "Ignoring error in delete volume %s: volume not found " + "due to force delete settings."), vol_id) + else: + msg = (_("Error deleting volume %s: volume not found.") % + vol_id) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + else: + msg = (_("Error deleting volume %(vol)s: %(err)s.") % + {'vol': vol_id, + 'err': response['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def delete_snapshot(self, snapshot): + """Deletes a ScaleIO snapshot.""" + snapname = self.id_to_base64(snapshot.id) + LOG.info(_LI("ScaleIO delete snapshot.")) + self._delete_volume(snapname) + + def initialize_connection(self, volume, connector): + """Initializes the connection and returns connection info. + + The scaleio driver returns a driver_volume_type of 'scaleio'. + """ + + LOG.debug("Connector is %s.", connector) + volname = self.id_to_base64(volume.id) + properties = {} + + properties['scaleIO_volname'] = volname + properties['hostIP'] = connector['ip'] + properties['serverIP'] = self.server_ip + properties['serverPort'] = self.server_port + properties['serverUsername'] = self.server_username + properties['serverPassword'] = self.server_password + properties['serverToken'] = self.server_token + + storage_type = self._get_volumetype_extraspecs(volume) + LOG.info(_LI("Volume type is %s."), storage_type) + iops_limit = self._find_iops_limit(storage_type) + LOG.info(_LI("iops limit is: %s."), iops_limit) + bandwidth_limit = self._find_bandwidth_limit(storage_type) + LOG.info(_LI("Bandwidth limit is: %s."), bandwidth_limit) + properties['iopsLimit'] = iops_limit + properties['bandwidthLimit'] = bandwidth_limit + + return {'driver_volume_type': 'scaleio', + 'data': properties} + + def terminate_connection(self, volume, connector, **kwargs): + LOG.debug("scaleio driver terminate connection.") + + def _update_volume_stats(self): + stats = {} + + backend_name = self.configuration.safe_get('volume_backend_name') + stats['volume_backend_name'] = backend_name or 'scaleio' + stats['vendor_name'] = 'EMC' + stats['driver_version'] = self.VERSION + stats['storage_protocol'] = 'scaleio' + stats['total_capacity_gb'] = 'unknown' + stats['free_capacity_gb'] = 'unknown' + stats['reserved_percentage'] = 0 + stats['QoS_support'] = False + + pools = [] + + verify_cert = self._get_verify_cert() + + max_free_capacity = 0 + total_capacity = 0 + + for sp_name in self.storage_pools: + splitted_name = sp_name.split(':') + domain_name = splitted_name[0] + pool_name = splitted_name[1] + LOG.debug("domain name is %(domain)s, pool name is %(pool)s.", + {'domain': domain_name, + 'pool': pool_name}) + # Get domain id from name. + encoded_domain_name = urllib.quote(domain_name, '') + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'encoded_domain_name': encoded_domain_name} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/types/Domain/instances/getByName::" + "%(encoded_domain_name)s") % req_vars + LOG.info(_LI("ScaleIO get domain id by name request: %s."), + request) + LOG.info(_LI("username: %(username)s, verify_cert: %(verify)s."), + {'username': self.server_username, + 'verify': verify_cert}) + r = requests.get( + request, + auth=( + self.server_username, + self.server_token), + verify=verify_cert) + r = self._check_response(r, request) + LOG.info(_LI("Get domain by name response: %s"), r.text) + domain_id = r.json() + if not domain_id: + msg = (_("Domain with name %s wasn't found.") + % self.protection_domain_name) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + if r.status_code != OK_STATUS_CODE and "errorCode" in domain_id: + msg = (_("Error getting domain id from name %(name)s: " + "%(err)s.") + % {'name': self.protection_domain_name, + 'err': domain_id['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + LOG.info(_LI("Domain id is %s."), domain_id) + + # Get pool id from name. + encoded_pool_name = urllib.quote(pool_name, '') + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'domain_id': domain_id, + 'encoded_pool_name': encoded_pool_name} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/types/Pool/instances/getByName::" + "%(domain_id)s,%(encoded_pool_name)s") % req_vars + LOG.info(_LI("ScaleIO get pool id by name request: %s."), request) + r = requests.get( + request, + auth=( + self.server_username, + self.server_token), + verify=verify_cert) + pool_id = r.json() + if not pool_id: + msg = (_("Pool with name %(pool)s wasn't found in domain " + "%(domain)s.") + % {'pool': pool_name, + 'domain': domain_id}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + if r.status_code != OK_STATUS_CODE and "errorCode" in pool_id: + msg = (_("Error getting pool id from name %(pool)s: " + "%(err)s.") + % {'pool': pool_name, + 'err': pool_id['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + LOG.info(_LI("Pool id is %s."), pool_id) + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/types/StoragePool/instances/action/" + "querySelectedStatistics") % req_vars + params = {'ids': [pool_id], 'properties': [ + "capacityInUseInKb", "capacityLimitInKb"]} + r = requests.post( + request, + data=json.dumps(params), + headers=self._get_headers(), + auth=( + self.server_username, + self.server_token), + verify=verify_cert) + response = r.json() + LOG.info(_LI("Query capacity stats response: %s."), response) + for res in response.itervalues(): + capacityInUse = res['capacityInUseInKb'] + capacityLimit = res['capacityLimitInKb'] + total_capacity_gb = capacityLimit / units.Mi + used_capacity_gb = capacityInUse / units.Mi + free_capacity_gb = total_capacity_gb - used_capacity_gb + LOG.info(_LI( + "free capacity of pool %(pool)s is: %(free)s, " + "total capacity: %(total)s."), + {'pool': pool_name, + 'free': free_capacity_gb, + 'total': total_capacity_gb}) + pool = {'pool_name': sp_name, + 'total_capacity_gb': total_capacity_gb, + 'free_capacity_gb': free_capacity_gb, + 'QoS_support': False, + 'reserved_percentage': 0 + } + + pools.append(pool) + if free_capacity_gb > max_free_capacity: + max_free_capacity = free_capacity_gb + total_capacity = total_capacity + total_capacity_gb + + stats['volume_backend_name'] = backend_name or 'scaleio' + stats['vendor_name'] = 'EMC' + stats['driver_version'] = self.VERSION + stats['storage_protocol'] = 'scaleio' + # Use zero capacities here so we always use a pool. + stats['total_capacity_gb'] = total_capacity + stats['free_capacity_gb'] = max_free_capacity + LOG.info(_LI( + "Free capacity for backend is: %(free)s, total capacity: " + "%(total)s."), + {'free': max_free_capacity, + 'total': total_capacity}) + + stats['reserved_percentage'] = 0 + stats['QoS_support'] = False + stats['pools'] = pools + + LOG.info(_LI("Backend name is %s."), stats["volume_backend_name"]) + + self._stats = stats + + def get_volume_stats(self, refresh=False): + """Get volume stats. + + If 'refresh' is True, run update the stats first. + """ + if refresh: + self._update_volume_stats() + + return self._stats + + def _get_volumetype_extraspecs(self, volume): + specs = {} + ctxt = context.get_admin_context() + type_id = volume['volume_type_id'] + if type_id: + volume_type = volume_types.get_volume_type(ctxt, type_id) + specs = volume_type.get('extra_specs') + for key, value in specs.iteritems(): + specs[key] = value + + return specs + + def find_volume_path(self, volume_id): + + LOG.info(_LI("looking for volume %s."), volume_id) + # Look for the volume in /dev/disk/by-id directory. + disk_filename = "" + tries = 0 + while not disk_filename: + if tries > self.configuration.num_volume_device_scan_tries: + msg = (_( + "scaleIO volume %s not found at expected path.") + % volume_id) + raise exception.VolumeBackendAPIException(msg) + by_id_path = "/dev/disk/by-id" + if not os.path.isdir(by_id_path): + LOG.warning(_LW( + "scaleIO volume %(vol)s not yet found (no directory " + "/dev/disk/by-id yet). Try number: %(tries)d."), + {'vol': volume_id, + 'tries': tries}) + tries = tries + 1 + eventlet.sleep(1) + continue + filenames = os.listdir(by_id_path) + LOG.info(_LI( + "Files found in path %(path)s: %(file)s."), + {'path': by_id_path, + 'file': filenames}) + for filename in filenames: + if (filename.startswith("emc-vol") and + filename.endswith(volume_id)): + disk_filename = filename + if not disk_filename: + LOG.warning(_LW( + "scaleIO volume %(vol)s not yet found. " + "Try number: %(tries)d."), + {'vol': volume_id, + 'tries': tries}) + tries = tries + 1 + eventlet.sleep(1) + + if (tries != 0): + LOG.info(_LI( + "Found scaleIO device %(file)s after %(tries)d retries "), + {'file': disk_filename, + 'tries': tries}) + full_disk_name = by_id_path + "/" + disk_filename + LOG.info(_LI("Full disk name is %s."), full_disk_name) + return full_disk_name + + def _get_client_id( + self, server_ip, server_username, server_password, sdc_ip): + req_vars = {'server_ip': server_ip, + 'server_port': self.server_port, + 'sdc_ip': sdc_ip} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/types/Client/instances/getByIp::" + "%(sdc_ip)s/") % req_vars + LOG.info(_LI("ScaleIO get client id by ip request: %s."), request) + r = requests.get( + request, + auth=( + server_username, + self.server_token), + verify=self._get_verify_cert()) + r = self._check_response(r, request) + + sdc_id = r.json() + if not sdc_id: + msg = _("Client with ip %s wasn't found.") % sdc_ip + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + if r.status_code != 200 and "errorCode" in sdc_id: + msg = (_("Error getting sdc id from ip %(ip)s: %(id)s.") + % {'ip': sdc_ip, 'id': sdc_id['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + LOG.info(_LI("ScaleIO sdc id is %s."), sdc_id) + return sdc_id + + def _sio_attach_volume(self, volume, sdc_ip): + # We need to make sure we even *have* a local path + LOG.info(_LI("ScaleIO attach volume in scaleio cinder driver.")) + volname = self.id_to_base64(volume.id) + + cmd = ['drv_cfg'] + cmd += ["--query_guid"] + + LOG.info(_LI("ScaleIO sdc query guid command: %s"), six.text_type(cmd)) + + try: + (out, err) = utils.execute(*cmd, run_as_root=True) + LOG.debug("Map volume %(cmd)s: stdout=%(out)s stderr=%(err)s", + {'cmd': cmd, 'out': out, 'err': err}) + except processutils.ProcessExecutionError as e: + msg = _("Error querying sdc guid: %s.") % six.text_type(e.stderr) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + guid = out + LOG.info(_LI("Current sdc guid: %s."), guid) + + params = {'guid': guid} + + volume_id = self._get_volume_id(volname) + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'volume_id': volume_id} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/instances/Volume::%(volume_id)s" + "/action/addMappedSdc") % req_vars + LOG.info(_LI("Map volume request: %s."), request) + r = requests.post( + request, + data=json.dumps(params), + headers=self._get_headers(), + auth=( + self.server_username, + self.server_token), + verify=self._get_verify_cert()) + r = self._check_response(r, request, False) + + if r.status_code != OK_STATUS_CODE: + response = r.json() + error_code = response['errorCode'] + if (error_code == VOLUME_ALREADY_MAPPED_ERROR): + LOG.warning(_LW("Ignoring error mapping volume %s: " + "volume already mapped."), volname) + else: + msg = (_("Error mapping volume %(vol)s: %(err)s.") + % {'vol': volname, + 'err': response['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + formated_id = volume_id + + return self.find_volume_path(formated_id) + + def _sio_detach_volume(self, volume, sdc_ip): + LOG.info(_LI("ScaleIO detach volume in scaleio cinder driver.")) + volname = self.id_to_base64(volume.id) + + cmd = ['drv_cfg'] + cmd += ["--query_guid"] + + LOG.info(_LI("ScaleIO sdc query guid command: %s."), cmd) + + try: + (out, err) = utils.execute(*cmd, run_as_root=True) + LOG.debug("Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s", + {'cmd': cmd, 'out': out, 'err': err}) + + except processutils.ProcessExecutionError as e: + msg = _("Error querying sdc guid: %s.") % six.text_type(e.stderr) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + guid = out + LOG.info(_LI("Current sdc guid: %s."), guid) + + params = {'guid': guid} + + volume_id = self._get_volume_id(volname) + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'vol_id': volume_id} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/instances/Volume::%(vol_id)s" + "/action/removeMappedSdc") % req_vars + LOG.info(_LI("Unmap volume request: %s."), request) + r = requests.post( + request, + data=json.dumps(params), + headers=self._get_headers(), + auth=( + self.server_username, + self.server_token), + verify=self._get_verify_cert()) + r = self._check_response(r, request, False, params) + + if r.status_code != OK_STATUS_CODE: + response = r.json() + error_code = response['errorCode'] + if error_code == VOLUME_NOT_MAPPED_ERROR: + LOG.warning(_LW("Ignoring error unmapping volume %s: " + "volume not mapped."), volname) + else: + msg = (_("Error unmapping volume %(vol)s: %(err)s.") + % {'vol': volname, + 'err': response['message']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + 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.info(_LI( + "ScaleIO copy_image_to_volume volume: %(vol)s image service: " + "%(service)s image id: %(id)s."), + {'vol': volume, + 'service': six.text_type(image_service), + 'id': six.text_type(image_id)}) + properties = utils.brick_get_connector_properties() + sdc_ip = properties['ip'] + LOG.debug("SDC ip is: %s", sdc_ip) + + try: + image_utils.fetch_to_raw(context, + image_service, + image_id, + self._sio_attach_volume(volume, sdc_ip), + BLOCK_SIZE, + size=volume['size']) + + finally: + self._sio_detach_volume(volume, sdc_ip) + + def copy_volume_to_image(self, context, volume, image_service, image_meta): + """Copy the volume to the specified image.""" + LOG.info(_LI( + "ScaleIO copy_volume_to_image volume: %(vol)s image service: " + "%(service)s image meta: %(meta)s."), + {'vol': volume, + 'service': six.text_type(image_service), + 'meta': six.text_type(image_meta)}) + properties = utils.brick_get_connector_properties() + sdc_ip = properties['ip'] + LOG.debug("SDC ip is: {0}".format(sdc_ip)) + try: + image_utils.upload_volume(context, + image_service, + image_meta, + self._sio_attach_volume(volume, sdc_ip)) + finally: + self._sio_detach_volume(volume, sdc_ip) + + def ensure_export(self, context, volume): + """Driver entry point to get the export info for an existing volume.""" + pass + + def create_export(self, context, volume): + """Driver entry point to get the export info for a new volume.""" + pass + + def remove_export(self, context, volume): + """Driver entry point to remove an export for a volume.""" + pass + + def check_for_export(self, context, volume_id): + """Make sure volume is exported.""" + pass