Add support for VxFlex OS 3.5 to VxFlex OS driver

Driver code is prepared for future VxFlex OS 3.5 release.

Unit tests are fixed to work properly with updated driver.

Implements: blueprint vxflexos-replication-support
Change-Id: I693980384df22b2fa581d8715f73c69b0598dd59
This commit is contained in:
Ivan Pchelintsev 2020-01-31 10:51:39 +03:00
parent 819b4a0fc0
commit f75b2865fe
18 changed files with 1410 additions and 1323 deletions

View File

@ -91,6 +91,7 @@ class TestVxFlexOSDriver(test.TestCase):
__COMMON_HTTPS_MOCK_RESPONSES = {
RESPONSE_MODE.Valid: {
'login': 'login_token',
'version': '3.5'
},
RESPONSE_MODE.BadStatus: {
'login': mocks.MockHTTPSResponse(
@ -99,6 +100,7 @@ class TestVxFlexOSDriver(test.TestCase):
'message': 'Bad Login Response Test',
}, 403
),
'version': '3.5'
},
}
__https_response_mode = RESPONSE_MODE.Valid
@ -124,10 +126,14 @@ class TestVxFlexOSDriver(test.TestCase):
conf.SHARED_CONF_GROUP)
self._set_overrides()
self.driver = mocks.VxFlexOSDriver(configuration=self.configuration)
self.driver.primary_client = mocks.VxFlexOSClient(self.configuration)
self.driver.do_setup({})
self.mock_object(requests, 'get', self.do_request)
self.mock_object(requests, 'post', self.do_request)
self.driver.primary_client.do_setup()
def _set_overrides(self):
# Override the defaults to fake values
self.override_config('san_ip', override='127.0.0.1',

View File

@ -19,6 +19,7 @@ import requests
import six
from cinder.volume.drivers.dell_emc.vxflexos import driver
from cinder.volume.drivers.dell_emc.vxflexos import rest_client
CONF = cfg.CONF
@ -28,6 +29,14 @@ class VxFlexOSDriver(driver.VxFlexOSDriver):
Provides some fake configuration options
"""
def do_setup(self, context):
self.provisioning_type = (
"thin" if self.configuration.san_thin_provision else "thick"
)
self.configuration.max_over_subscription_ratio = (
self.configuration.vxflexos_max_over_subscription_ratio
)
def local_path(self, volume):
pass
@ -40,7 +49,14 @@ class VxFlexOSDriver(driver.VxFlexOSDriver):
def unmanage(self, volume):
pass
def _is_volume_creation_safe(self, _pd, _sp):
class VxFlexOSClient(rest_client.RestClient):
"""Mock VxFlex OS Rest Client class.
Provides some fake configuration options
"""
def is_volume_creation_safe(self, _pd, _sp):
return True

View File

@ -23,6 +23,7 @@ from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_volume
from cinder.tests.unit.volume.drivers.dell_emc import vxflexos
from cinder.tests.unit.volume.drivers.dell_emc.vxflexos import mocks
from cinder.volume.drivers.dell_emc.vxflexos import utils as flex_utils
class TestCreateClonedVolume(vxflexos.TestVxFlexOSDriver):
@ -40,7 +41,7 @@ class TestCreateClonedVolume(vxflexos.TestVxFlexOSDriver):
self.src_volume_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(
self.driver._id_to_base64(self.src_volume.id)
flex_utils.id_to_base64(self.src_volume.id)
)
)
@ -55,7 +56,7 @@ class TestCreateClonedVolume(vxflexos.TestVxFlexOSDriver):
self.new_volume_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(
self.driver._id_to_base64(self.new_volume.id)
flex_utils.id_to_base64(self.new_volume.id)
)
)
self.HTTPS_MOCK_RESPONSES = {
@ -64,6 +65,7 @@ class TestCreateClonedVolume(vxflexos.TestVxFlexOSDriver):
self.src_volume_name_2x_enc: self.src_volume.id,
'instances/System/action/snapshotVolumes': '{}'.format(
json.dumps(self.new_volume_extras)),
'instances/Volume::cloned/action/setVolumeSize': None
},
self.RESPONSE_MODE.BadStatus: {
'instances/System/action/snapshotVolumes':

View File

@ -24,6 +24,7 @@ from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.tests.unit.volume.drivers.dell_emc import vxflexos
from cinder.tests.unit.volume.drivers.dell_emc.vxflexos import mocks
from cinder.volume.drivers.dell_emc.vxflexos import utils as flex_utils
class TestCreateSnapShot(vxflexos.TestVxFlexOSDriver):
@ -51,10 +52,10 @@ class TestCreateSnapShot(vxflexos.TestVxFlexOSDriver):
snap_vol_id = self.snapshot.volume_id
self.volume_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(self.driver._id_to_base64(snap_vol_id))
urllib.parse.quote(flex_utils.id_to_base64(snap_vol_id))
)
self.snapshot_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(self.driver._id_to_base64(self.snapshot.id))
urllib.parse.quote(flex_utils.id_to_base64(self.snapshot.id))
)
self.snapshot_reply = json.dumps(

View File

@ -22,6 +22,7 @@ from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.tests.unit.volume.drivers.dell_emc import vxflexos
from cinder.tests.unit.volume.drivers.dell_emc.vxflexos import mocks
from cinder.volume.drivers.dell_emc.vxflexos import utils as flex_utils
class TestCreateVolumeFromSnapShot(vxflexos.TestVxFlexOSDriver):
@ -37,11 +38,11 @@ class TestCreateVolumeFromSnapShot(vxflexos.TestVxFlexOSDriver):
self.snapshot = fake_snapshot.fake_snapshot_obj(ctx)
self.snapshot_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(self.driver._id_to_base64(self.snapshot.id))
urllib.parse.quote(flex_utils.id_to_base64(self.snapshot.id))
)
self.volume = fake_volume.fake_volume_obj(ctx)
self.volume_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(self.driver._id_to_base64(self.volume.id))
urllib.parse.quote(flex_utils.id_to_base64(self.volume.id))
)
self.snapshot_reply = json.dumps(
@ -57,6 +58,8 @@ class TestCreateVolumeFromSnapShot(vxflexos.TestVxFlexOSDriver):
self.snapshot_name_2x_enc: self.snapshot.id,
'instances/System/action/snapshotVolumes':
self.snapshot_reply,
'instances/Volume::{}/action/setVolumeSize'.format(
self.volume.id): None,
},
self.RESPONSE_MODE.BadStatus: {
'instances/System/action/snapshotVolumes':

View File

@ -18,9 +18,11 @@ from cinder import context
from cinder import exception
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit.fake_snapshot import fake_snapshot_obj
from cinder.tests.unit.fake_volume import fake_volume_obj
from cinder.tests.unit.volume.drivers.dell_emc import vxflexos
from cinder.tests.unit.volume.drivers.dell_emc.vxflexos import mocks
from cinder.volume import configuration
from cinder.volume.drivers.dell_emc.vxflexos import utils as flex_utils
class TestDeleteSnapShot(vxflexos.TestVxFlexOSDriver):
@ -34,11 +36,16 @@ class TestDeleteSnapShot(vxflexos.TestVxFlexOSDriver):
super(TestDeleteSnapShot, self).setUp()
ctx = context.RequestContext('fake', 'fake', auth_token=True)
self.fake_volume = fake_volume_obj(
ctx, **{'provider_id': fake.PROVIDER_ID})
self.snapshot = fake_snapshot_obj(
ctx, **{'provider_id': fake.SNAPSHOT_ID})
ctx, **{'volume': self.fake_volume,
'provider_id': fake.SNAPSHOT_ID})
self.snapshot_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(
self.driver._id_to_base64(self.snapshot.id)
flex_utils.id_to_base64(self.snapshot.id)
)
)
@ -46,6 +53,7 @@ class TestDeleteSnapShot(vxflexos.TestVxFlexOSDriver):
self.RESPONSE_MODE.Valid: {
'types/Volume/instances/getByName::' +
self.snapshot_name_2x_enc: self.snapshot.id,
'instances/Volume::' + self.snapshot.provider_id: {},
'instances/Volume::{}/action/removeMappedSdc'.format(
self.snapshot.provider_id
): self.snapshot.id,
@ -54,6 +62,8 @@ class TestDeleteSnapShot(vxflexos.TestVxFlexOSDriver):
): self.snapshot.id,
},
self.RESPONSE_MODE.BadStatus: {
'instances/Volume::' + self.snapshot.provider_id:
self.BAD_STATUS_RESPONSE,
'types/Volume/instances/getByName::' +
self.snapshot_name_2x_enc: self.BAD_STATUS_RESPONSE,
'instances/Volume::{}/action/removeVolume'.format(

View File

@ -21,6 +21,7 @@ from cinder.tests.unit import fake_volume
from cinder.tests.unit.volume.drivers.dell_emc import vxflexos
from cinder.tests.unit.volume.drivers.dell_emc.vxflexos import mocks
from cinder.volume import configuration
from cinder.volume.drivers.dell_emc.vxflexos import utils as flex_utils
class TestDeleteVolume(vxflexos.TestVxFlexOSDriver):
@ -37,11 +38,12 @@ class TestDeleteVolume(vxflexos.TestVxFlexOSDriver):
ctx, **{'provider_id': fake.PROVIDER_ID})
self.volume_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(self.driver._id_to_base64(self.volume.id))
urllib.parse.quote(flex_utils.id_to_base64(self.volume.id))
)
self.HTTPS_MOCK_RESPONSES = {
self.RESPONSE_MODE.Valid: {
'instances/Volume::' + self.volume.provider_id: {},
'types/Volume/instances/getByName::' +
self.volume_name_2x_enc: self.volume.id,
'instances/Volume::{}/action/removeMappedSdc'.format(
@ -51,6 +53,8 @@ class TestDeleteVolume(vxflexos.TestVxFlexOSDriver):
): self.volume.provider_id,
},
self.RESPONSE_MODE.BadStatus: {
'instances/Volume::' + self.volume.provider_id:
self.BAD_STATUS_RESPONSE,
'types/Volume/instances/getByName::' +
self.volume_name_2x_enc: mocks.MockHTTPSResponse(
{

View File

@ -21,6 +21,7 @@ from cinder.tests.unit.fake_volume import fake_volume_obj
from cinder.tests.unit.volume.drivers.dell_emc import vxflexos
from cinder.tests.unit.volume.drivers.dell_emc.vxflexos import mocks
from cinder.volume import configuration
from cinder.volume.drivers.dell_emc.vxflexos import utils as flex_utils
class TestExtendVolume(vxflexos.TestVxFlexOSDriver):
@ -45,7 +46,7 @@ class TestExtendVolume(vxflexos.TestVxFlexOSDriver):
self.volume = fake_volume_obj(ctx, **{'id': fake.VOLUME_ID,
'provider_id': fake.PROVIDER_ID})
self.volume_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(self.driver._id_to_base64(self.volume.id))
urllib.parse.quote(flex_utils.id_to_base64(self.volume.id))
)
self.HTTPS_MOCK_RESPONSES = {

View File

@ -104,6 +104,7 @@ class VxFlexOSManageableCase(vxflexos.TestVxFlexOSDriver):
def setUp(self):
"""Setup a test case environment."""
super(VxFlexOSManageableCase, self).setUp()
self.driver.storage_pools = super().STORAGE_POOLS
def _test_get_manageable_things(self,
vxflexos_objects=MANAGEABLE_VXFLEXOS_VOLS,

View File

@ -72,6 +72,8 @@ class TestGroups(vxflexos.TestVxFlexOSDriver):
'snapshotGroupId': 'sgid1'})
self.HTTPS_MOCK_RESPONSES = {
self.RESPONSE_MODE.Valid: {
'instances/Volume::' + fake_volume1['provider_id']: {},
'instances/Volume::' + fake_volume2['provider_id']: {},
'instances/Volume::{}/action/removeVolume'.format(
fake_volume1['provider_id']
): fake_volume1['provider_id'],
@ -185,10 +187,7 @@ class TestGroups(vxflexos.TestVxFlexOSDriver):
self.assertEqual(fields.GroupStatus.AVAILABLE,
result_model_update['status'])
def get_pid(snapshot):
return snapshot['provider_id']
volume_provider_list = list(map(get_pid, result_volumes_model_update))
self.assertListEqual(volume_provider_list, ['sid1', 'sid2'])
self.assertEqual(len(result_volumes_model_update), len(self.volumes))
@mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type')
def test_create_group_from_src_snapshot(self, is_group_a_cg_snapshot_type):
@ -212,10 +211,7 @@ class TestGroups(vxflexos.TestVxFlexOSDriver):
self.assertEqual(fields.GroupStatus.AVAILABLE,
result_model_update['status'])
def get_pid(snapshot):
return snapshot['provider_id']
volume_provider_list = list(map(get_pid, result_volumes_model_update))
self.assertListEqual(volume_provider_list, ['sid1', 'sid2'])
self.assertEqual(len(result_volumes_model_update), len(self.volumes))
@mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type')
def test_delete_group_snapshot(self, is_group_a_cg_snapshot_type):
@ -275,10 +271,5 @@ class TestGroups(vxflexos.TestVxFlexOSDriver):
result_model_update['status'])
self.assertTrue(all(snapshot['status'] == 'available' for snapshot in
result_snapshot_model_update))
def get_pid(snapshot):
return snapshot['provider_id']
snapshot_provider_list = list(map(get_pid,
result_snapshot_model_update))
self.assertListEqual(['sid1', 'sid2'], snapshot_provider_list)
self.assertEqual(len(result_snapshot_model_update),
len(self.snapshots))

View File

@ -23,6 +23,7 @@ from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_volume
from cinder.tests.unit.volume.drivers.dell_emc import vxflexos
from cinder.tests.unit.volume.drivers.dell_emc.vxflexos import mocks
from cinder.volume.drivers.dell_emc.vxflexos import utils as flex_utils
from cinder.volume import volume_types
@ -42,7 +43,7 @@ class TestManageExisting(vxflexos.TestVxFlexOSDriver):
ctx, **{'provider_id': fake.PROVIDER2_ID})
self.volume_no_provider_id = fake_volume.fake_volume_obj(ctx)
self.volume_name_2x_enc = urllib.parse.quote(
urllib.parse.quote(self.driver._id_to_base64(self.volume.id))
urllib.parse.quote(flex_utils.id_to_base64(self.volume.id))
)
self.HTTPS_MOCK_RESPONSES = {
@ -90,7 +91,7 @@ class TestManageExisting(vxflexos.TestVxFlexOSDriver):
self.volume['volume_type_id'] = fake.VOLUME_TYPE_ID
existing_ref = {'source-id': fake.PROVIDER_ID}
self.set_https_response_mode(self.RESPONSE_MODE.BadStatus)
self.assertRaises(exception.ManageExistingInvalidReference,
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.manage_existing, self.volume,
existing_ref)

View File

@ -12,7 +12,7 @@
# 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 mock import patch
from unittest.mock import patch
from cinder import context
from cinder import exception
@ -44,7 +44,7 @@ class TestManageExistingSnapshot(vxflexos.TestVxFlexOSDriver):
self.snapshot['volume_type_id'] = fake.VOLUME_TYPE_ID
self.snapshot2['volume_type_id'] = fake.VOLUME_TYPE_ID
self.snapshot_attached = fake_snapshot.fake_snapshot_obj(
ctx, **{'provider_id': fake.PROVIDER3_ID})
ctx, **{'provider_id': fake.PROVIDER4_ID})
self.HTTPS_MOCK_RESPONSES = {
self.RESPONSE_MODE.Valid: {
@ -84,7 +84,7 @@ class TestManageExistingSnapshot(vxflexos.TestVxFlexOSDriver):
}, 200),
'instances/Volume::' + self.snapshot_attached['provider_id']:
mocks.MockHTTPSResponse({
'id': fake.PROVIDER3_ID,
'id': fake.PROVIDER4_ID,
'sizeInKb': 8388608,
'mappedSdcInfo': 'Mapped',
'ancestorVolumeId': fake.PROVIDER_ID
@ -105,7 +105,7 @@ class TestManageExistingSnapshot(vxflexos.TestVxFlexOSDriver):
def test_snapshot_not_found(self, _mock_volume_type):
existing_ref = {'source-id': fake.PROVIDER2_ID}
self.set_https_response_mode(self.RESPONSE_MODE.BadStatus)
self.assertRaises(exception.ManageExistingInvalidReference,
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.manage_existing_snapshot, self.snapshot,
existing_ref)
@ -115,7 +115,7 @@ class TestManageExistingSnapshot(vxflexos.TestVxFlexOSDriver):
return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}})
def test_snapshot_attached(self, _mock_volume_type):
self.snapshot_attached['volume_type_id'] = fake.VOLUME_TYPE_ID
existing_ref = {'source-id': fake.PROVIDER2_ID}
existing_ref = {'source-id': fake.PROVIDER4_ID}
self.set_https_response_mode(self.RESPONSE_MODE.BadStatus)
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_snapshot,

View File

@ -114,6 +114,7 @@ class TestMisc(vxflexos.TestVxFlexOSDriver):
}
def test_valid_configuration(self):
self.driver.storage_pools = self.STORAGE_POOLS
self.driver.check_for_setup_error()
def test_no_storage_pools(self):
@ -219,8 +220,8 @@ class TestMisc(vxflexos.TestVxFlexOSDriver):
self.driver.get_volume_stats(True)
@mock.patch(
'cinder.volume.drivers.dell_emc.vxflexos.driver.VxFlexOSDriver.'
'_rename_volume',
'cinder.volume.drivers.dell_emc.vxflexos.rest_client.RestClient.'
'rename_volume',
return_value=None)
def test_update_migrated_volume(self, mock_rename):
test_vol = self.driver.update_migrated_volume(
@ -230,8 +231,8 @@ class TestMisc(vxflexos.TestVxFlexOSDriver):
test_vol)
@mock.patch(
'cinder.volume.drivers.dell_emc.vxflexos.driver.VxFlexOSDriver.'
'_rename_volume',
'cinder.volume.drivers.dell_emc.vxflexos.rest_client.RestClient.'
'rename_volume',
return_value=None)
def test_update_unavailable_migrated_volume(self, mock_rename):
test_vol = self.driver.update_migrated_volume(
@ -242,8 +243,8 @@ class TestMisc(vxflexos.TestVxFlexOSDriver):
test_vol)
@mock.patch(
'cinder.volume.drivers.dell_emc.vxflexos.driver.VxFlexOSDriver.'
'_rename_volume',
'cinder.volume.drivers.dell_emc.vxflexos.rest_client.RestClient.'
'rename_volume',
side_effect=exception.VolumeBackendAPIException(data='Error!'))
def test_fail_update_migrated_volume(self, mock_rename):
self.assertRaises(
@ -257,42 +258,56 @@ class TestMisc(vxflexos.TestVxFlexOSDriver):
mock_rename.assert_called_with(self.volume, "ff" + self.volume['id'])
def test_rename_volume(self):
rc = self.driver._rename_volume(
rc = self.driver.primary_client.rename_volume(
self.volume, self.new_volume['id'])
self.assertIsNone(rc)
def test_rename_volume_illegal_syntax(self):
self.set_https_response_mode(self.RESPONSE_MODE.Invalid)
rc = self.driver._rename_volume(
rc = self.driver.primary_client.rename_volume(
self.volume, self.new_volume['id'])
self.assertIsNone(rc)
def test_rename_volume_non_sio(self):
self.set_https_response_mode(self.RESPONSE_MODE.BadStatus)
rc = self.driver._rename_volume(
rc = self.driver.primary_client.rename_volume(
self.volume, self.new_volume['id'])
self.assertIsNone(rc)
def test_default_provisioning_type_unspecified(self):
empty_storage_type = {}
self.assertEqual(
'thin',
self.driver._find_provisioning_type(empty_storage_type))
provisioning, compression = (
self.driver._get_provisioning_and_compression(
empty_storage_type,
self.PROT_DOMAIN_NAME,
self.STORAGE_POOL_NAME)
)
self.assertEqual('ThinProvisioned', provisioning)
@ddt.data((True, 'thin'), (False, 'thick'))
@ddt.data((True, 'ThinProvisioned'), (False, 'ThickProvisioned'))
@ddt.unpack
def test_default_provisioning_type_thin(self, config_provisioning_type,
expected_provisioning_type):
self.override_config('san_thin_provision', config_provisioning_type,
configuration.SHARED_CONF_GROUP)
self.driver = mocks.VxFlexOSDriver(configuration=self.configuration)
self.driver.do_setup({})
self.driver.primary_client = mocks.VxFlexOSClient(self.configuration)
self.driver.primary_client.do_setup()
empty_storage_type = {}
self.assertEqual(
expected_provisioning_type,
self.driver._find_provisioning_type(empty_storage_type))
provisioning, compression = (
self.driver._get_provisioning_and_compression(
empty_storage_type,
self.PROT_DOMAIN_NAME,
self.STORAGE_POOL_NAME)
)
self.assertEqual(expected_provisioning_type, provisioning)
def test_get_volume_stats_v3(self):
self.driver.server_api_version = "3.0"
@mock.patch('cinder.volume.drivers.dell_emc.vxflexos.rest_client.'
'RestClient.query_rest_api_version',
return_value="3.0")
def test_get_volume_stats_v3(self, mock_version):
self.driver.storage_pools = self.STORAGE_POOLS
zero_data = {
'types/StoragePool/instances/action/querySelectedStatistics':
mocks.MockHTTPSResponse(content=json.dumps(

View File

@ -58,7 +58,7 @@ class TestMultipleVersions(vxflexos.TestVxFlexOSDriver):
def test_version(self):
"""Valid version request."""
self.driver._get_server_api_version(False)
self.driver.primary_client.query_rest_api_version(False)
def test_version_badstatus_response(self):
"""Version api returns a bad response."""
@ -86,8 +86,8 @@ class TestMultipleVersions(vxflexos.TestVxFlexOSDriver):
for vers in self.good_versions:
self.version = vers
self.setup_response()
self.driver._get_server_api_version(False)
self.driver.primary_client.query_rest_api_version(False)
self.assertEqual(
self.driver._get_server_api_version(False),
self.driver.primary_client.query_rest_api_version(False),
vers
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,500 @@
# Copyright (c) 2020 Dell Inc. or its subsidiaries.
# 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 re
from oslo_log import log as logging
from oslo_utils import units
import requests
import six
from six.moves import http_client
from six.moves import urllib
from cinder import exception
from cinder.i18n import _
from cinder.utils import retry
from cinder.volume.drivers.dell_emc.vxflexos import simplecache
from cinder.volume.drivers.dell_emc.vxflexos import utils as flex_utils
LOG = logging.getLogger(__name__)
VOLUME_NOT_FOUND_ERROR = 79
OLD_VOLUME_NOT_FOUND_ERROR = 78
ILLEGAL_SYNTAX = 0
class RestClient(object):
def __init__(self, configuration):
self.configuration = configuration
self.spCache = simplecache.SimpleCache("Storage Pool", age_minutes=5)
self.pdCache = simplecache.SimpleCache("Protection Domain",
age_minutes=5)
self.rest_ip = None
self.rest_port = None
self.rest_username = None
self.rest_password = None
self.rest_token = None
self.rest_api_version = None
self.verify_certificate = None
self.certificate_path = None
self.base_url = None
self.is_configured = False
@staticmethod
def _get_headers():
return {"content-type": "application/json"}
@property
def connection_properties(self):
return {
"scaleIO_volname": None,
"hostIP": None,
"serverIP": self.rest_ip,
"serverPort": self.rest_port,
"serverUsername": self.rest_username,
"serverPassword": self.rest_password,
"serverToken": self.rest_token,
"iopsLimit": None,
"bandwidthLimit": None,
}
def do_setup(self):
self.rest_port = self.configuration.vxflexos_rest_server_port
self.verify_certificate = (
self.configuration.safe_get("sio_verify_server_certificate") or
self.configuration.safe_get("driver_ssl_cert_verify")
)
self.rest_ip = self.configuration.safe_get("san_ip")
self.rest_username = self.configuration.safe_get("san_login")
self.rest_password = self.configuration.safe_get("san_password")
if self.verify_certificate:
self.certificate_path = (
self.configuration.safe_get("sio_server_certificate_path") or
self.configuration.safe_get("driver_ssl_cert_path")
)
if not all([self.rest_ip, self.rest_username, self.rest_password]):
msg = _("REST server IP, username and password must be specified.")
raise exception.InvalidInput(reason=msg)
# validate certificate settings
if self.verify_certificate and not self.certificate_path:
msg = _("Path to REST server's certificate must be specified.")
raise exception.InvalidInput(reason=msg)
# log warning if not using certificates
if not self.verify_certificate:
LOG.warning("Verify certificate is not set, using default of "
"False.")
self.base_url = ("https://%(server_ip)s:%(server_port)s/api" %
{
"server_ip": self.rest_ip,
"server_port": self.rest_port
})
LOG.info("REST server IP: %(ip)s, port: %(port)s, "
"username: %(user)s. Verify server's certificate: "
"%(verify_cert)s.",
{
"ip": self.rest_ip,
"port": self.rest_port,
"user": self.rest_username,
"verify_cert": self.verify_certificate,
})
self.is_configured = True
def query_rest_api_version(self, fromcache=True):
url = "/version"
if self.rest_api_version is None or fromcache is False:
r, unused = self.execute_vxflexos_get_request(url)
if r.status_code == http_client.OK:
self.rest_api_version = r.text.replace('\"', "")
LOG.info("REST API Version: %(api_version)s.",
{"api_version": self.rest_api_version})
else:
msg = (_("Failed to query REST API version. "
"Status code: %d.") % r.status_code)
raise exception.VolumeBackendAPIException(data=msg)
# make sure the response was valid
pattern = re.compile(r"^\d+(\.\d+)*$")
if not pattern.match(self.rest_api_version):
msg = (_("Failed to query REST API version. Response: %s.") %
r.text)
raise exception.VolumeBackendAPIException(data=msg)
return self.rest_api_version
def query_volume(self, vol_id):
url = "/instances/Volume::%(vol_id)s"
r, response = self.execute_vxflexos_get_request(url, vol_id=vol_id)
if r.status_code != http_client.OK and "errorCode" in response:
msg = (_("Failed to query volume: %s.") % response["message"])
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return response
def create_volume(self,
protection_domain_name,
storage_pool_name,
volume,
provisioning,
compression):
url = "/types/Volume/instances"
domain_id = self._get_protection_domain_id(protection_domain_name)
LOG.info("Protection Domain id: %s.", domain_id)
pool_id = self.get_storage_pool_id(protection_domain_name,
storage_pool_name)
LOG.info("Storage Pool id: %s.", pool_id)
volume_name = flex_utils.id_to_base64(volume.id)
# units.Mi = 1024 ** 2
volume_size_kb = volume.size * units.Mi
params = {
"protectionDomainId": domain_id,
"storagePoolId": pool_id,
"name": volume_name,
"volumeType": provisioning,
"volumeSizeInKb": six.text_type(volume_size_kb),
"compressionMethod": compression,
}
r, response = self.execute_vxflexos_post_request(url, params)
if r.status_code != http_client.OK and "errorCode" in response:
msg = (_("Failed to create volume: %s.") % response["message"])
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return response["id"]
def snapshot_volume(self, volume_provider_id, snapshot_id):
url = "/instances/System/action/snapshotVolumes"
snap_name = flex_utils.id_to_base64(snapshot_id)
params = {
"snapshotDefs": [
{
"volumeId": volume_provider_id,
"snapshotName": snap_name,
},
],
}
r, response = self.execute_vxflexos_post_request(url, params)
if r.status_code != http_client.OK and "errorCode" in response:
msg = (_("Failed to create snapshot for volume %(vol_name)s: "
"%(response)s.") %
{"vol_name": volume_provider_id,
"response": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return response["volumeIdList"][0]
def _get_protection_domain_id_by_name(self, domain_name):
url = "/types/Domain/instances/getByName::%(encoded_domain_name)s"
if not domain_name:
msg = _("Unable to query Protection Domain id with None name.")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
encoded_domain_name = urllib.parse.quote(domain_name, "")
r, domain_id = self.execute_vxflexos_get_request(
url, encoded_domain_name=encoded_domain_name
)
if not domain_id:
msg = (_("Prorection Domain with name %s wasn't found.")
% domain_name)
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
if r.status_code != http_client.OK and "errorCode" in domain_id:
msg = (_("Failed to get Protection Domain id with name "
"%(name)s: %(err_msg)s.") %
{"name": domain_name, "err_msg": domain_id["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
LOG.info("Protection Domain id: %s.", domain_id)
return domain_id
def _get_protection_domain_id(self, domain_name):
response = self._get_protection_domain_properties(domain_name)
if response is None:
return None
return response["id"]
def _get_protection_domain_properties(self, domain_name):
url = "/instances/ProtectionDomain::%(domain_id)s"
cached_val = self.pdCache.get_value(domain_name)
if cached_val is not None:
return cached_val
domain_id = self._get_protection_domain_id_by_name(domain_name)
r, response = self.execute_vxflexos_get_request(
url, domain_id=domain_id
)
if r.status_code != http_client.OK:
msg = (_("Failed to get domain properties from id %(domain_id)s: "
"%(err_msg)s.") %
{"domain_id": domain_id, "err_msg": response})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
self.pdCache.update(domain_name, response)
return response
def _get_storage_pool_id_by_name(self, domain_name, pool_name):
url = ("/types/Pool/instances/getByName::"
"%(domain_id)s,%(encoded_pool_name)s")
if not domain_name or not pool_name:
msg = (_("Unable to query storage pool id for "
"Pool %(pool_name)s and Domain %(domain_name)s.") %
{"pool_name": pool_name, "domain_name": domain_name})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
domain_id = self._get_protection_domain_id(domain_name)
encoded_pool_name = urllib.parse.quote(pool_name, "")
r, pool_id = self.execute_vxflexos_get_request(
url, domain_id=domain_id, encoded_pool_name=encoded_pool_name
)
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 != http_client.OK and "errorCode" in pool_id:
msg = (_("Failed to get 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("Pool id: %s.", pool_id)
return pool_id
def get_storage_pool_properties(self, domain_name, pool_name):
url = "/instances/StoragePool::%(pool_id)s"
fullname = "{}:{}".format(domain_name, pool_name)
cached_val = self.spCache.get_value(fullname)
if cached_val is not None:
return cached_val
pool_id = self._get_storage_pool_id_by_name(domain_name, pool_name)
r, response = self.execute_vxflexos_get_request(url, pool_id=pool_id)
if r.status_code != http_client.OK:
msg = (_("Failed to get pool properties from id %(pool_id)s: "
"%(err_msg)s.") %
{"pool_id": pool_id, "err_msg": response})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
self.spCache.update(fullname, response)
return response
def get_storage_pool_id(self, domain_name, pool_name):
response = self.get_storage_pool_properties(domain_name, pool_name)
if response is None:
return None
return response["id"]
def _get_verify_cert(self):
verify_cert = False
if self.verify_certificate:
verify_cert = self.certificate_path
return verify_cert
def execute_vxflexos_get_request(self, url, **url_params):
request = self.base_url + url % url_params
r = requests.get(request,
auth=(self.rest_username, self.rest_token),
verify=self._get_verify_cert())
r = self._check_response(r, request)
response = r.json()
return r, response
def execute_vxflexos_post_request(self, url, params=None, **url_params):
if not params:
params = {}
request = self.base_url + url % url_params
r = requests.post(request,
data=json.dumps(params),
headers=self._get_headers(),
auth=(self.rest_username, self.rest_token),
verify=self._get_verify_cert())
r = self._check_response(r, request, False, params)
response = None
try:
response = r.json()
except ValueError:
response = None
return r, response
def _check_response(self,
response,
request,
is_get_request=True,
params=None):
login_url = "/login"
if (response.status_code == http_client.UNAUTHORIZED or
response.status_code == http_client.FORBIDDEN):
LOG.info("Token is invalid, going to re-login and get "
"a new one.")
login_request = self.base_url + login_url
verify_cert = self._get_verify_cert()
r = requests.get(login_request,
auth=(self.rest_username, self.rest_password),
verify=verify_cert)
token = r.json()
self.rest_token = token
# Repeat request with valid token.
LOG.info("Going to perform request again %s with valid token.",
request)
if is_get_request:
response = requests.get(request,
auth=(
self.rest_username,
self.rest_token
),
verify=verify_cert)
else:
response = requests.post(request,
data=json.dumps(params),
headers=self._get_headers(),
auth=(
self.rest_username,
self.rest_token
),
verify=verify_cert)
level = logging.DEBUG
# for anything other than an OK from the REST API, log an error
if response.status_code != http_client.OK:
level = logging.ERROR
LOG.log(level,
"REST Request: %s with params %s",
request,
json.dumps(params))
LOG.log(level,
"REST Response: %s with data %s",
response.status_code,
response.text)
return response
@retry(exception.VolumeBackendAPIException)
def extend_volume(self, vol_id, new_size):
url = "/instances/Volume::%(vol_id)s/action/setVolumeSize"
round_volume_capacity = (
self.configuration.vxflexos_round_volume_capacity
)
if not round_volume_capacity and not new_size % 8 == 0:
LOG.warning("VxFlex OS only supports volumes with a granularity "
"of 8 GBs. The new volume size is: %d.",
new_size)
params = {"sizeInGB": six.text_type(new_size)}
r, response = self.execute_vxflexos_post_request(url,
params,
vol_id=vol_id)
if r.status_code != http_client.OK:
response = r.json()
msg = (_("Failed to extend volume %(vol_id)s: %(err)s.") %
{"vol_id": vol_id, "err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def _unmap_volume_before_delete(self, vol_id):
url = "/instances/Volume::%(vol_id)s/action/removeMappedSdc"
volume_is_mapped = False
try:
volume = self.query_volume(vol_id)
if volume.get("mappedSdcInfo") is not None:
volume_is_mapped = True
except exception.VolumeBackendAPIException:
LOG.info("Volume %s is not found thus is not mapped to any SDC.",
vol_id)
if volume_is_mapped:
params = {"allSdcs": ""}
LOG.info("Unmap volume from all sdcs before deletion.")
r, unused = self.execute_vxflexos_post_request(url,
params,
vol_id=vol_id)
@retry(exception.VolumeBackendAPIException)
def remove_volume(self, vol_id):
url = "/instances/Volume::%(vol_id)s/action/removeVolume"
self._unmap_volume_before_delete(vol_id)
params = {"removeMode": "ONLY_ME"}
r, response = self.execute_vxflexos_post_request(url,
params,
vol_id=vol_id)
if r.status_code != http_client.OK:
error_code = response["errorCode"]
if error_code == VOLUME_NOT_FOUND_ERROR:
LOG.warning("Ignoring error in delete volume %s: "
"Volume not found.", vol_id)
elif vol_id is None:
LOG.warning("Volume does not have provider_id thus does not "
"map to a VxFlex OS volume. "
"Allowing deletion to proceed.")
else:
msg = (_("Failed to delete volume %(vol_id)s: %(err)s.") %
{"vol_id": vol_id, "err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def is_volume_creation_safe(self, protection_domain, storage_pool):
"""Checks if volume creation is safe or not.
Using volumes with zero padding disabled can lead to existing data
being read off of a newly created volume.
"""
# if we have been told to allow unsafe volumes
if self.configuration.vxflexos_allow_non_padded_volumes:
# Enabled regardless of type, so safe to proceed
return True
try:
properties = self.get_storage_pool_properties(
protection_domain, storage_pool
)
padded = properties["zeroPaddingEnabled"]
except Exception:
msg = (_("Unable to retrieve properties for pool %s.") %
storage_pool)
raise exception.InvalidInput(reason=msg)
# zero padded storage pools are safe
if padded:
return True
# if we got here, it's unsafe
return False
def rename_volume(self, volume, name):
url = "/instances/Volume::%(id)s/action/setVolumeName"
new_name = flex_utils.id_to_base64(name)
vol_id = volume["provider_id"]
params = {"newName": new_name}
r, response = self.execute_vxflexos_post_request(url,
params,
id=vol_id)
if r.status_code != http_client.OK:
error_code = response["errorCode"]
if ((error_code == VOLUME_NOT_FOUND_ERROR or
error_code == OLD_VOLUME_NOT_FOUND_ERROR or
error_code == ILLEGAL_SYNTAX)):
LOG.info("Ignore renaming action because the volume "
"%(vol_id)s is not a VxFlex OS volume.",
{"vol_id": vol_id})
else:
msg = (_("Failed to rename volume %(vol_id)s: %(err)s.") %
{"vol_id": vol_id, "err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
else:
LOG.info("VxFlex OS volume %(vol_id)s was renamed to "
"%(new_name)s.", {"vol_id": vol_id, "new_name": new_name})

View File

@ -0,0 +1,61 @@
# Copyright (c) 2020 Dell Inc. or its subsidiaries.
# 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 base64
import binascii
from distutils import version
import math
from oslo_log import log as logging
from oslo_utils import units
LOG = logging.getLogger(__name__)
def version_gte(ver1, ver2):
return version.LooseVersion(ver1) >= version.LooseVersion(ver2)
def convert_kb_to_gib(size):
return int(math.floor(float(size) / units.Mi))
def id_to_base64(_id):
# Base64 encode the id to get a volume name less than 32 characters due
# to VxFlex OS limitation.
name = str(_id).replace("-", "")
try:
name = base64.b16decode(name.upper())
except (TypeError, binascii.Error):
pass
if isinstance(name, str):
name = name.encode()
encoded_name = base64.b64encode(name).decode()
LOG.debug("Converted id %(id)s to VxFlex OS name %(name)s.",
{"id": _id, "name": encoded_name})
return encoded_name
def round_to_num_gran(size, num=8):
"""Round size to nearest value that is multiple of `num`."""
if size % num == 0:
return size
return size + num - (size % num)
def round_down_to_num_gran(size, num=8):
"""Round size down to nearest value that is multiple of `num`."""
return size - (size % num)

View File

@ -0,0 +1,4 @@
---
features:
- |
VxFlex OS driver now supports VxFlex OS 3.5.x.