Datera driver 2.2 update
This update adds the following changes to the Datera driver - Capabilites List - Extended Volume-Type Support - Naming convention change - Volume re-type support - Volume Manage/Unmanage support Many driver settings that were originally located in StrOpts have been moved to VolumeType ExtraSpecs. DocImpact Change-Id: I469fe29950984e05b70b9399883f2bd73ec856eb
This commit is contained in:
parent
9daffb12af
commit
fff322b3d5
@ -23,8 +23,10 @@ from cinder.volume.drivers import datera
|
|||||||
from cinder.volume import volume_types
|
from cinder.volume import volume_types
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_STORAGE_NAME = datera.DEFAULT_STORAGE_NAME
|
datera.DEFAULT_SI_SLEEP = 0.01
|
||||||
DEFAULT_VOLUME_NAME = datera.DEFAULT_VOLUME_NAME
|
URL_TEMPLATES = datera.URL_TEMPLATES
|
||||||
|
OS_PREFIX = datera.OS_PREFIX
|
||||||
|
UNMANAGE_PREFIX = datera.UNMANAGE_PREFIX
|
||||||
|
|
||||||
|
|
||||||
class DateraVolumeTestCase(test.TestCase):
|
class DateraVolumeTestCase(test.TestCase):
|
||||||
@ -52,6 +54,7 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
self.driver = datera.DateraDriver(execute=mock_exec,
|
self.driver = datera.DateraDriver(execute=mock_exec,
|
||||||
configuration=self.cfg)
|
configuration=self.cfg)
|
||||||
self.driver.set_initialized()
|
self.driver.set_initialized()
|
||||||
|
self.driver.configuration.get = _config_getter
|
||||||
self.volume = _stub_volume()
|
self.volume = _stub_volume()
|
||||||
self.api_patcher = mock.patch('cinder.volume.drivers.datera.'
|
self.api_patcher = mock.patch('cinder.volume.drivers.datera.'
|
||||||
'DateraDriver._issue_api_request')
|
'DateraDriver._issue_api_request')
|
||||||
@ -74,9 +77,8 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
def _progress_api_return(mock_api):
|
def _progress_api_return(mock_api):
|
||||||
if mock_api.retry_count == 1:
|
if mock_api.retry_count == 1:
|
||||||
_bad_vol_ai = stub_single_ai.copy()
|
_bad_vol_ai = stub_single_ai.copy()
|
||||||
_bad_vol_ai['storage_instances'][
|
_bad_vol_ai['storage_instances']['storage-1'][
|
||||||
DEFAULT_STORAGE_NAME]['volumes'][DEFAULT_VOLUME_NAME][
|
'volumes']['volume-1']['op_status'] = 'unavailable'
|
||||||
'op_status'] = 'unavailable'
|
|
||||||
return _bad_vol_ai
|
return _bad_vol_ai
|
||||||
else:
|
else:
|
||||||
self.mock_api.retry_count += 1
|
self.mock_api.retry_count += 1
|
||||||
@ -259,7 +261,7 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
|
|
||||||
def test_delete_snapshot_not_found(self):
|
def test_delete_snapshot_not_found(self):
|
||||||
self.mock_api.side_effect = [stub_return_snapshots, exception.NotFound]
|
self.mock_api.side_effect = [stub_return_snapshots, exception.NotFound]
|
||||||
snapshot = _stub_snapshot(self.volume['id'])
|
snapshot = _stub_snapshot(self.volume['id'], volume_id="test")
|
||||||
self.assertIsNone(self.driver.delete_snapshot(snapshot))
|
self.assertIsNone(self.driver.delete_snapshot(snapshot))
|
||||||
|
|
||||||
def test_delete_snapshot_fails(self):
|
def test_delete_snapshot_fails(self):
|
||||||
@ -303,6 +305,73 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
self.assertRaises(exception.NotAuthorized, self.driver._login)
|
self.assertRaises(exception.NotAuthorized, self.driver._login)
|
||||||
self.assertEqual(1, self.mock_api.call_count)
|
self.assertEqual(1, self.mock_api.call_count)
|
||||||
|
|
||||||
|
def test_manage_existing(self):
|
||||||
|
TEST_NAME = {"source-name": "test-app:test-si:test-vol"}
|
||||||
|
self.mock_api.return_value = {}
|
||||||
|
self.assertIsNone(
|
||||||
|
self.driver.manage_existing(
|
||||||
|
_stub_volume(),
|
||||||
|
TEST_NAME))
|
||||||
|
self.mock_api.assert_called_once_with(
|
||||||
|
URL_TEMPLATES['ai_inst']().format(
|
||||||
|
TEST_NAME["source-name"].split(":")[0]),
|
||||||
|
method='put',
|
||||||
|
body={'name': OS_PREFIX + _stub_volume()['id']})
|
||||||
|
|
||||||
|
def test_manage_existing_wrong_ref(self):
|
||||||
|
TEST_NAME = {"source-name": "incorrect-reference"}
|
||||||
|
self.assertRaises(
|
||||||
|
exception.ManageExistingInvalidReference,
|
||||||
|
self.driver.manage_existing,
|
||||||
|
_stub_volume(),
|
||||||
|
TEST_NAME)
|
||||||
|
|
||||||
|
def test_manage_existing_get_size(self):
|
||||||
|
TEST_NAME = {"source-name": "test-app:storage-1:volume-1"}
|
||||||
|
self.mock_api.side_effect = self._generate_fake_api_request()
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.manage_existing_get_size(
|
||||||
|
_stub_volume(),
|
||||||
|
TEST_NAME), 500)
|
||||||
|
self.mock_api.assert_called_once_with(
|
||||||
|
URL_TEMPLATES['ai_inst']().format(
|
||||||
|
TEST_NAME["source-name"].split(":")[0]))
|
||||||
|
|
||||||
|
def test_manage_existing_get_size_wrong_ref(self):
|
||||||
|
TEST_NAME = {"source-name": "incorrect-reference"}
|
||||||
|
self.assertRaises(
|
||||||
|
exception.ManageExistingInvalidReference,
|
||||||
|
self.driver.manage_existing_get_size,
|
||||||
|
_stub_volume(),
|
||||||
|
TEST_NAME)
|
||||||
|
|
||||||
|
def test_get_manageable_volumes(self):
|
||||||
|
self.mock_api.return_value = non_cinder_ais
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.get_manageable_volumes(
|
||||||
|
{}, "", 10, 0, "", ""),
|
||||||
|
[{'cinder_id': None,
|
||||||
|
'extra_info': None,
|
||||||
|
'reason_not_safe': None,
|
||||||
|
'reference': {"source-name": 'test-app-inst:storage-1:volume-1'},
|
||||||
|
'safe_to_manage': True,
|
||||||
|
'size': 50},
|
||||||
|
{'cinder_id': 'c20aba21-6ef6-446b-b374-45733b4883ba',
|
||||||
|
'extra_info': None,
|
||||||
|
'reason_not_safe': None,
|
||||||
|
'reference': None,
|
||||||
|
'safe_to_manage': False,
|
||||||
|
'size': None}])
|
||||||
|
|
||||||
|
def test_unmanage(self):
|
||||||
|
self.mock_api.return_value = {}
|
||||||
|
self.assertIsNone(self.driver.unmanage(_stub_volume()))
|
||||||
|
self.mock_api.assert_called_once_with(
|
||||||
|
URL_TEMPLATES['ai_inst']().format(
|
||||||
|
OS_PREFIX + _stub_volume()['id']),
|
||||||
|
method='put',
|
||||||
|
body={'name': UNMANAGE_PREFIX + _stub_volume()['id']})
|
||||||
|
|
||||||
def _generate_fake_api_request(self, targets_exist=True):
|
def _generate_fake_api_request(self, targets_exist=True):
|
||||||
def _fake_api_request(resource_type, method='get', resource=None,
|
def _fake_api_request(resource_type, method='get', resource=None,
|
||||||
body=None, action=None, sensitive=False):
|
body=None, action=None, sensitive=False):
|
||||||
@ -318,6 +387,8 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
return stub_acl
|
return stub_acl
|
||||||
elif resource_type == 'ig_group':
|
elif resource_type == 'ig_group':
|
||||||
return stub_ig
|
return stub_ig
|
||||||
|
else:
|
||||||
|
return list(stub_app_instance.values())[0]
|
||||||
return _fake_api_request
|
return _fake_api_request
|
||||||
|
|
||||||
stub_acl = {
|
stub_acl = {
|
||||||
@ -422,6 +493,7 @@ stub_app_instance = {
|
|||||||
},
|
},
|
||||||
"creation_type": "user",
|
"creation_type": "user",
|
||||||
"descr": "c20aba21-6ef6-446b-b374-45733b4883ba__ST__storage-1",
|
"descr": "c20aba21-6ef6-446b-b374-45733b4883ba__ST__storage-1",
|
||||||
|
"op_state": "available",
|
||||||
"name": "storage-1",
|
"name": "storage-1",
|
||||||
"path": "/app_instances/c20aba21-6ef6-446b-b374-"
|
"path": "/app_instances/c20aba21-6ef6-446b-b374-"
|
||||||
"45733b4883ba/storage_instances/storage-1",
|
"45733b4883ba/storage_instances/storage-1",
|
||||||
@ -481,6 +553,148 @@ stub_return_snapshots = \
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
non_cinder_ais = {
|
||||||
|
"75bc1c69-a399-4acb-aade-3514caf13c5e": {
|
||||||
|
"admin_state": "online",
|
||||||
|
"create_mode": "normal",
|
||||||
|
"descr": "",
|
||||||
|
"health": "ok",
|
||||||
|
"id": "75bc1c69-a399-4acb-aade-3514caf13c5e",
|
||||||
|
"name": "test-app-inst",
|
||||||
|
"path": "/app_instances/75bc1c69-a399-4acb-aade-3514caf13c5e",
|
||||||
|
"snapshot_policies": {},
|
||||||
|
"snapshots": {},
|
||||||
|
"storage_instances": {
|
||||||
|
"storage-1": {
|
||||||
|
"access": {
|
||||||
|
"ips": [
|
||||||
|
"172.28.41.93"
|
||||||
|
],
|
||||||
|
"iqn": "iqn.2013-05.com.daterainc:tc:01:sn:"
|
||||||
|
"29036682e2d37b98",
|
||||||
|
"path": "/app_instances/75bc1c69-a399-4acb-aade-"
|
||||||
|
"3514caf13c5e/storage_instances/storage-1/access"
|
||||||
|
},
|
||||||
|
"access_control_mode": "deny_all",
|
||||||
|
"acl_policy": {
|
||||||
|
"initiator_groups": [],
|
||||||
|
"initiators": [],
|
||||||
|
"path": "/app_instances/75bc1c69-a399-4acb-aade-"
|
||||||
|
"3514caf13c5e/storage_instances/storage-"
|
||||||
|
"1/acl_policy"
|
||||||
|
},
|
||||||
|
"active_initiators": [],
|
||||||
|
"active_storage_nodes": [
|
||||||
|
"/storage_nodes/78b350a8-43f2-453f-a257-8df76d7406b9"
|
||||||
|
],
|
||||||
|
"admin_state": "online",
|
||||||
|
"auth": {
|
||||||
|
"initiator_pswd": "(hidden)",
|
||||||
|
"initiator_user_name": "",
|
||||||
|
"path": "/app_instances/75bc1c69-a399-4acb-aade-"
|
||||||
|
"3514caf13c5e/storage_instances/storage-1/auth",
|
||||||
|
"target_pswd": "(hidden)",
|
||||||
|
"target_user_name": "",
|
||||||
|
"type": "none"
|
||||||
|
},
|
||||||
|
"creation_type": "user",
|
||||||
|
"ip_pool": "/access_network_ip_pools/default",
|
||||||
|
"name": "storage-1",
|
||||||
|
"op_state": "available",
|
||||||
|
"path": "/app_instances/75bc1c69-a399-4acb-aade-"
|
||||||
|
"3514caf13c5e/storage_instances/storage-1",
|
||||||
|
"uuid": "6421237d-e4fc-433a-b535-148d5b6d8586",
|
||||||
|
"volumes": {
|
||||||
|
"volume-1": {
|
||||||
|
"capacity_in_use": 0,
|
||||||
|
"name": "volume-1",
|
||||||
|
"op_state": "available",
|
||||||
|
"path": "/app_instances/75bc1c69-a399-4acb-aade-"
|
||||||
|
"3514caf13c5e/storage_instances/storage-"
|
||||||
|
"1/volumes/volume-1",
|
||||||
|
"replica_count": 1,
|
||||||
|
"size": 50,
|
||||||
|
"snapshot_policies": {},
|
||||||
|
"snapshots": {},
|
||||||
|
"uuid": "e674d29c-a672-40d1-9577-abe3a504ffe9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||||
|
},
|
||||||
|
"dfdaf8d1-8976-4c13-a829-3345e03cf810": {
|
||||||
|
"admin_state": "offline",
|
||||||
|
"create_mode": "openstack",
|
||||||
|
"descr": "",
|
||||||
|
"health": "ok",
|
||||||
|
"id": "dfdaf8d1-8976-4c13-a829-3345e03cf810",
|
||||||
|
"name": "OS-c20aba21-6ef6-446b-b374-45733b4883ba",
|
||||||
|
"path": "/app_instances/dfdaf8d1-8976-4c13-a829-3345e03cf810",
|
||||||
|
"snapshot_policies": {},
|
||||||
|
"snapshots": {},
|
||||||
|
"storage_instances": {
|
||||||
|
"storage-1": {
|
||||||
|
"access": {
|
||||||
|
"ips": [
|
||||||
|
"172.28.41.57"
|
||||||
|
],
|
||||||
|
"iqn": "iqn.2013-05.com.daterainc:tc:01:sn:"
|
||||||
|
"56cd59e754ad02b6",
|
||||||
|
"path": "/app_instances/dfdaf8d1-8976-4c13-a829-"
|
||||||
|
"3345e03cf810/storage_instances/storage-1/access"
|
||||||
|
},
|
||||||
|
"access_control_mode": "deny_all",
|
||||||
|
"acl_policy": {
|
||||||
|
"initiator_groups": [],
|
||||||
|
"initiators": [],
|
||||||
|
"path": "/app_instances/dfdaf8d1-8976-4c13-a829-"
|
||||||
|
"3345e03cf810/storage_instances/storage-"
|
||||||
|
"1/acl_policy"
|
||||||
|
},
|
||||||
|
"active_initiators": [],
|
||||||
|
"active_storage_nodes": [
|
||||||
|
"/storage_nodes/78b350a8-43f2-453f-a257-8df76d7406b9"
|
||||||
|
],
|
||||||
|
"admin_state": "offline",
|
||||||
|
"auth": {
|
||||||
|
"initiator_pswd": "(hidden)",
|
||||||
|
"initiator_user_name": "",
|
||||||
|
"path": "/app_instances/dfdaf8d1-8976-4c13-a829-"
|
||||||
|
"3345e03cf810/storage_instances/storage-1/auth",
|
||||||
|
"target_pswd": "(hidden)",
|
||||||
|
"target_user_name": "",
|
||||||
|
"type": "none"
|
||||||
|
},
|
||||||
|
"creation_type": "user",
|
||||||
|
"ip_pool": "/access_network_ip_pools/default",
|
||||||
|
"name": "storage-1",
|
||||||
|
"op_state": "unavailable",
|
||||||
|
"path": "/app_instances/dfdaf8d1-8976-4c13-a829-3345e03cf810"
|
||||||
|
"/storage_instances/storage-1",
|
||||||
|
"uuid": "5620a673-9985-464e-9616-e325a50eac60",
|
||||||
|
"volumes": {
|
||||||
|
"volume-1": {
|
||||||
|
"capacity_in_use": 0,
|
||||||
|
"name": "volume-1",
|
||||||
|
"op_state": "available",
|
||||||
|
"path": "/app_instances/dfdaf8d1-8976-4c13-a829-"
|
||||||
|
"3345e03cf810/storage_instances/storage-"
|
||||||
|
"1/volumes/volume-1",
|
||||||
|
"replica_count": 1,
|
||||||
|
"size": 5,
|
||||||
|
"snapshot_policies": {},
|
||||||
|
"snapshots": {},
|
||||||
|
"uuid": "c20aba21-6ef6-446b-b374-45733b4883ba"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "c20aba21-6ef6-446b-b374-45733b4883ba"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _stub_datera_volume(*args, **kwargs):
|
def _stub_datera_volume(*args, **kwargs):
|
||||||
return {
|
return {
|
||||||
"status": "available",
|
"status": "available",
|
||||||
@ -519,3 +733,7 @@ def _stub_snapshot(*args, **kwargs):
|
|||||||
volume['display_name'] = kwargs.get('display_name', name)
|
volume['display_name'] = kwargs.get('display_name', name)
|
||||||
volume['volume_id'] = kwargs.get('volume_id', None)
|
volume['volume_id'] = kwargs.get('volume_id', None)
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
|
|
||||||
|
def _config_getter(*args, **kwargs):
|
||||||
|
return {}
|
||||||
|
@ -15,9 +15,10 @@
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import time
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import eventlet
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
@ -33,12 +34,11 @@ from cinder import interface
|
|||||||
from cinder import utils
|
from cinder import utils
|
||||||
from cinder.volume.drivers.san import san
|
from cinder.volume.drivers.san import san
|
||||||
from cinder.volume import qos_specs
|
from cinder.volume import qos_specs
|
||||||
|
from cinder.volume import utils as volutils
|
||||||
from cinder.volume import volume_types
|
from cinder.volume import volume_types
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATERA_SI_SLEEP = 4
|
|
||||||
|
|
||||||
d_opts = [
|
d_opts = [
|
||||||
cfg.StrOpt('datera_api_port',
|
cfg.StrOpt('datera_api_port',
|
||||||
default='7717',
|
default='7717',
|
||||||
@ -48,6 +48,7 @@ d_opts = [
|
|||||||
help='Datera API version.'),
|
help='Datera API version.'),
|
||||||
cfg.IntOpt('datera_num_replicas',
|
cfg.IntOpt('datera_num_replicas',
|
||||||
default='3',
|
default='3',
|
||||||
|
deprecated_for_removal=True,
|
||||||
help='Number of replicas to create of an inode.'),
|
help='Number of replicas to create of an inode.'),
|
||||||
cfg.IntOpt('datera_503_timeout',
|
cfg.IntOpt('datera_503_timeout',
|
||||||
default='120',
|
default='120',
|
||||||
@ -55,12 +56,18 @@ d_opts = [
|
|||||||
cfg.IntOpt('datera_503_interval',
|
cfg.IntOpt('datera_503_interval',
|
||||||
default='5',
|
default='5',
|
||||||
help='Interval between 503 retries'),
|
help='Interval between 503 retries'),
|
||||||
cfg.BoolOpt('datera_acl_allow_all',
|
|
||||||
default=False,
|
|
||||||
help="True to set acl 'allow_all' on volumes created"),
|
|
||||||
cfg.BoolOpt('datera_debug',
|
cfg.BoolOpt('datera_debug',
|
||||||
default=False,
|
default=False,
|
||||||
help="True to set function arg and return logging")
|
help="True to set function arg and return logging"),
|
||||||
|
cfg.BoolOpt('datera_acl_allow_all',
|
||||||
|
default=False,
|
||||||
|
deprecated_for_removal=True,
|
||||||
|
help="True to set acl 'allow_all' on volumes "
|
||||||
|
"created"),
|
||||||
|
cfg.BoolOpt('datera_debug_replica_count_override',
|
||||||
|
default=False,
|
||||||
|
help="ONLY FOR DEBUG/TESTING PURPOSES\n"
|
||||||
|
"True to set replica_count to 1")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -68,25 +75,41 @@ CONF = cfg.CONF
|
|||||||
CONF.import_opt('driver_use_ssl', 'cinder.volume.driver')
|
CONF.import_opt('driver_use_ssl', 'cinder.volume.driver')
|
||||||
CONF.register_opts(d_opts)
|
CONF.register_opts(d_opts)
|
||||||
|
|
||||||
DEFAULT_STORAGE_NAME = 'storage-1'
|
DEFAULT_SI_SLEEP = 10
|
||||||
DEFAULT_VOLUME_NAME = 'volume-1'
|
INITIATOR_GROUP_PREFIX = "IG-"
|
||||||
|
OS_PREFIX = "OS-"
|
||||||
|
UNMANAGE_PREFIX = "UNMANAGED-"
|
||||||
|
|
||||||
|
# Taken from this SO post :
|
||||||
|
# http://stackoverflow.com/a/18516125
|
||||||
|
# Using old-style string formatting because of the nature of the regex
|
||||||
|
# conflicting with new-style curly braces
|
||||||
|
UUID4_STR_RE = ("%s[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab]"
|
||||||
|
"[a-f0-9]{3}-?[a-f0-9]{12}")
|
||||||
|
UUID4_RE = re.compile(UUID4_STR_RE % OS_PREFIX)
|
||||||
|
|
||||||
# Recursive dict to assemble basic url structure for the most common
|
# Recursive dict to assemble basic url structure for the most common
|
||||||
# API URL endpoints. Most others are constructed from these
|
# API URL endpoints. Most others are constructed from these
|
||||||
# Don't use this object to get a url though
|
URL_TEMPLATES = {
|
||||||
_URL_TEMPLATES_BASE = {
|
|
||||||
'ai': lambda: 'app_instances',
|
'ai': lambda: 'app_instances',
|
||||||
'ai_inst': lambda: (_URL_TEMPLATES_BASE['ai']() + '/{}'),
|
'ai_inst': lambda: (URL_TEMPLATES['ai']() + '/{}'),
|
||||||
'si': lambda: (_URL_TEMPLATES_BASE['ai_inst']() + '/storage_instances'),
|
'si': lambda: (URL_TEMPLATES['ai_inst']() + '/storage_instances'),
|
||||||
'si_inst': lambda: ((_URL_TEMPLATES_BASE['si']() + '/{}').format(
|
'si_inst': lambda storage_name: (
|
||||||
'{}', DEFAULT_STORAGE_NAME)),
|
(URL_TEMPLATES['si']() + '/{}').format(
|
||||||
'vol': lambda: ((_URL_TEMPLATES_BASE['si_inst']() + '/volumes').format(
|
'{}', storage_name)),
|
||||||
'{}', DEFAULT_STORAGE_NAME)),
|
'vol': lambda storage_name: (
|
||||||
'vol_inst': lambda: ((_URL_TEMPLATES_BASE['vol']() + '/{}').format(
|
(URL_TEMPLATES['si_inst'](storage_name) + '/volumes')),
|
||||||
'{}', DEFAULT_VOLUME_NAME))}
|
'vol_inst': lambda storage_name, volume_name: (
|
||||||
|
(URL_TEMPLATES['vol'](storage_name) + '/{}').format(
|
||||||
|
'{}', volume_name))}
|
||||||
|
|
||||||
# Use this one since I haven't found a way to inline call lambdas
|
|
||||||
URL_TEMPLATES = {k: v() for k, v in _URL_TEMPLATES_BASE.items()}
|
def _get_name(name):
|
||||||
|
return "".join((OS_PREFIX, name))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_unmanaged(name):
|
||||||
|
return "".join((UNMANAGE_PREFIX, name))
|
||||||
|
|
||||||
|
|
||||||
def _authenticated(func):
|
def _authenticated(func):
|
||||||
@ -123,56 +146,30 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
1.1 - Look for lun-0 instead of lun-1.
|
1.1 - Look for lun-0 instead of lun-1.
|
||||||
2.0 - Update For Datera API v2
|
2.0 - Update For Datera API v2
|
||||||
2.1 - Multipath, ACL and reorg
|
2.1 - Multipath, ACL and reorg
|
||||||
|
2.2 - Capabilites List, Extended Volume-Type Support
|
||||||
|
Naming convention change,
|
||||||
|
Volume Manage/Unmanage support
|
||||||
"""
|
"""
|
||||||
VERSION = '2.1'
|
VERSION = '2.2'
|
||||||
|
|
||||||
CI_WIKI_NAME = "datera-ci"
|
CI_WIKI_NAME = "datera-ci"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DateraDriver, self).__init__(*args, **kwargs)
|
super(DateraDriver, self).__init__(*args, **kwargs)
|
||||||
self.configuration.append_config_values(d_opts)
|
self.configuration.append_config_values(d_opts)
|
||||||
self.num_replicas = self.configuration.datera_num_replicas
|
|
||||||
self.username = self.configuration.san_login
|
self.username = self.configuration.san_login
|
||||||
self.password = self.configuration.san_password
|
self.password = self.configuration.san_password
|
||||||
self.auth_token = None
|
|
||||||
self.cluster_stats = {}
|
self.cluster_stats = {}
|
||||||
self.datera_api_token = None
|
self.datera_api_token = None
|
||||||
self.retry_attempts = (int(self.configuration.datera_503_timeout /
|
|
||||||
self.configuration.datera_503_interval))
|
|
||||||
self.interval = self.configuration.datera_503_interval
|
self.interval = self.configuration.datera_503_interval
|
||||||
self.allow_all = self.configuration.datera_acl_allow_all
|
self.retry_attempts = (self.configuration.datera_503_timeout /
|
||||||
|
self.interval)
|
||||||
self.driver_prefix = str(uuid.uuid4())[:4]
|
self.driver_prefix = str(uuid.uuid4())[:4]
|
||||||
self.datera_debug = self.configuration.datera_debug
|
self.datera_debug = self.configuration.datera_debug
|
||||||
|
|
||||||
if self.datera_debug:
|
if self.datera_debug:
|
||||||
utils.setup_tracing(['method'])
|
utils.setup_tracing(['method'])
|
||||||
|
|
||||||
def _login(self):
|
|
||||||
"""Use the san_login and san_password to set self.auth_token."""
|
|
||||||
body = {
|
|
||||||
'name': self.username,
|
|
||||||
'password': self.password
|
|
||||||
}
|
|
||||||
|
|
||||||
# Unset token now, otherwise potential expired token will be sent
|
|
||||||
# along to be used for authorization when trying to login.
|
|
||||||
self.auth_token = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
LOG.debug('Getting Datera auth token.')
|
|
||||||
results = self._issue_api_request('login', 'put', body=body,
|
|
||||||
sensitive=True)
|
|
||||||
self.datera_api_token = results['key']
|
|
||||||
except exception.NotAuthorized:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
LOG.error(_LE('Logging into the Datera cluster failed. Please '
|
|
||||||
'check your username and password set in the '
|
|
||||||
'cinder.conf and start the cinder-volume '
|
|
||||||
'service again.'))
|
|
||||||
|
|
||||||
def _get_lunid(self):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def do_setup(self, context):
|
def do_setup(self, context):
|
||||||
# If we can't authenticate through the old and new method, just fail
|
# If we can't authenticate through the old and new method, just fail
|
||||||
# now.
|
# now.
|
||||||
@ -186,64 +183,65 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
self._login()
|
self._login()
|
||||||
|
|
||||||
@utils.retry(exception.VolumeDriverException, retries=3)
|
@utils.retry(exception.VolumeDriverException, retries=3)
|
||||||
def _wait_for_resource(self, id, resource_type):
|
def _wait_for_resource(self, id, resource_type, policies):
|
||||||
result = self._issue_api_request(resource_type, 'get', id)
|
result = self._issue_api_request(resource_type, 'get', id)
|
||||||
if result['storage_instances'][DEFAULT_STORAGE_NAME]['volumes'][
|
if result['storage_instances'][
|
||||||
DEFAULT_VOLUME_NAME]['op_state'] == 'available':
|
policies['default_storage_name']]['volumes'][
|
||||||
|
policies['default_volume_name']]['op_state'] == 'available':
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
raise exception.VolumeDriverException(
|
raise exception.VolumeDriverException(
|
||||||
message=_('Resource not ready.'))
|
message=_('Resource not ready.'))
|
||||||
|
|
||||||
def _create_resource(self, resource, resource_type, body):
|
def _create_resource(self, resource, resource_type, body):
|
||||||
type_id = resource.get('volume_type_id', None)
|
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
try:
|
try:
|
||||||
result = self._issue_api_request(resource_type, 'post', body=body)
|
result = self._issue_api_request(resource_type, 'post', body=body)
|
||||||
except exception.Invalid:
|
except exception.Invalid:
|
||||||
|
type_id = resource.get('volume_type_id', None)
|
||||||
if resource_type == 'volumes' and type_id:
|
if resource_type == 'volumes' and type_id:
|
||||||
LOG.error(_LE("Creation request failed. Please verify the "
|
LOG.error(_LE("Creation request failed. Please verify the "
|
||||||
"extra-specs set for your volume types are "
|
"extra-specs set for your volume types are "
|
||||||
"entered correctly."))
|
"entered correctly."))
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
|
policies = self._get_policies_for_resource(resource)
|
||||||
# Handle updating QOS Policies
|
# Handle updating QOS Policies
|
||||||
if resource_type == URL_TEMPLATES['ai']:
|
if resource_type == URL_TEMPLATES['ai']():
|
||||||
url = URL_TEMPLATES['vol_inst'] + '/performance_policy'
|
self._update_qos(resource, policies)
|
||||||
url = url.format(resource['id'])
|
if result['storage_instances'][policies['default_storage_name']][
|
||||||
if type_id is not None:
|
'volumes'][policies['default_volume_name']][
|
||||||
# Filter for just QOS policies in result. All of their keys
|
'op_state'] == 'available':
|
||||||
# should end with "max"
|
|
||||||
policies = {k: int(v) for k, v in
|
|
||||||
self._get_policies_by_volume_type(
|
|
||||||
type_id).items() if k.endswith("max")}
|
|
||||||
if policies:
|
|
||||||
self._issue_api_request(url, 'post', body=policies)
|
|
||||||
if result['storage_instances'][DEFAULT_STORAGE_NAME]['volumes'][
|
|
||||||
DEFAULT_VOLUME_NAME]['op_state'] == 'available':
|
|
||||||
return
|
return
|
||||||
self._wait_for_resource(resource['id'], resource_type)
|
self._wait_for_resource(_get_name(resource['id']),
|
||||||
|
resource_type,
|
||||||
|
policies)
|
||||||
|
|
||||||
def create_volume(self, volume):
|
def create_volume(self, volume):
|
||||||
"""Create a logical volume."""
|
"""Create a logical volume."""
|
||||||
# Generate App Instance, Storage Instance and Volume
|
# Generate App Instance, Storage Instance and Volume
|
||||||
# Volume ID will be used as the App Instance Name
|
# Volume ID will be used as the App Instance Name
|
||||||
# Storage Instance and Volumes will have standard names
|
# Storage Instance and Volumes will have standard names
|
||||||
|
policies = self._get_policies_for_resource(volume)
|
||||||
|
num_replicas = int(policies['replica_count'])
|
||||||
|
storage_name = policies['default_storage_name']
|
||||||
|
volume_name = policies['default_volume_name']
|
||||||
|
|
||||||
app_params = (
|
app_params = (
|
||||||
{
|
{
|
||||||
'create_mode': "openstack",
|
'create_mode': "openstack",
|
||||||
'uuid': str(volume['id']),
|
'uuid': str(volume['id']),
|
||||||
'name': str(volume['id']),
|
'name': _get_name(volume['id']),
|
||||||
'access_control_mode': 'deny_all',
|
'access_control_mode': 'deny_all',
|
||||||
'storage_instances': {
|
'storage_instances': {
|
||||||
DEFAULT_STORAGE_NAME: {
|
storage_name: {
|
||||||
'name': DEFAULT_STORAGE_NAME,
|
'name': storage_name,
|
||||||
'volumes': {
|
'volumes': {
|
||||||
DEFAULT_VOLUME_NAME: {
|
volume_name: {
|
||||||
'name': DEFAULT_VOLUME_NAME,
|
'name': volume_name,
|
||||||
'size': volume['size'],
|
'size': volume['size'],
|
||||||
'replica_count': self.num_replicas,
|
'replica_count': num_replicas,
|
||||||
'snapshot_policies': {
|
'snapshot_policies': {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,23 +249,26 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
self._create_resource(volume, URL_TEMPLATES['ai'], body=app_params)
|
self._create_resource(volume, URL_TEMPLATES['ai'](), body=app_params)
|
||||||
|
|
||||||
def extend_volume(self, volume, new_size):
|
def extend_volume(self, volume, new_size):
|
||||||
# Offline App Instance, if necessary
|
# Offline App Instance, if necessary
|
||||||
reonline = False
|
reonline = False
|
||||||
app_inst = self._issue_api_request(
|
app_inst = self._issue_api_request(
|
||||||
URL_TEMPLATES['ai_inst'].format(volume['id']))
|
URL_TEMPLATES['ai_inst']().format(_get_name(volume['id'])))
|
||||||
if app_inst['admin_state'] == 'online':
|
if app_inst['admin_state'] == 'online':
|
||||||
reonline = True
|
reonline = True
|
||||||
self.detach_volume(None, volume, delete_initiator=False)
|
self.detach_volume(None, volume, delete_initiator=False)
|
||||||
# Change Volume Size
|
# Change Volume Size
|
||||||
app_inst = volume['id']
|
app_inst = _get_name(volume['id'])
|
||||||
data = {
|
data = {
|
||||||
'size': new_size
|
'size': new_size
|
||||||
}
|
}
|
||||||
|
policies = self._get_policies_for_resource(volume)
|
||||||
self._issue_api_request(
|
self._issue_api_request(
|
||||||
URL_TEMPLATES['vol_inst'].format(app_inst),
|
URL_TEMPLATES['vol_inst'](
|
||||||
|
policies['default_storage_name'],
|
||||||
|
policies['default_volume_name']).format(app_inst),
|
||||||
method='put',
|
method='put',
|
||||||
body=data)
|
body=data)
|
||||||
# Online Volume, if it was online before
|
# Online Volume, if it was online before
|
||||||
@ -275,29 +276,32 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
self.create_export(None, volume, None)
|
self.create_export(None, volume, None)
|
||||||
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
src = "/" + URL_TEMPLATES['vol_inst'].format(src_vref['id'])
|
policies = self._get_policies_for_resource(volume)
|
||||||
|
src = "/" + URL_TEMPLATES['vol_inst'](
|
||||||
|
policies['default_storage_name'],
|
||||||
|
policies['default_volume_name']).format(_get_name(src_vref['id']))
|
||||||
data = {
|
data = {
|
||||||
'create_mode': 'openstack',
|
'create_mode': 'openstack',
|
||||||
'name': str(volume['id']),
|
'name': _get_name(volume['id']),
|
||||||
'uuid': str(volume['id']),
|
'uuid': str(volume['id']),
|
||||||
'clone_src': src,
|
'clone_src': src,
|
||||||
# 'access_control_mode': 'allow_all'
|
|
||||||
}
|
}
|
||||||
self._issue_api_request(URL_TEMPLATES['ai'], 'post', body=data)
|
self._issue_api_request(URL_TEMPLATES['ai'](), 'post', body=data)
|
||||||
|
|
||||||
if volume['size'] > src_vref['size']:
|
if volume['size'] > src_vref['size']:
|
||||||
self.extend_volume(volume, volume['size'])
|
self.extend_volume(volume, volume['size'])
|
||||||
|
|
||||||
def delete_volume(self, volume):
|
def delete_volume(self, volume):
|
||||||
self.detach_volume(None, volume)
|
self.detach_volume(None, volume)
|
||||||
app_inst = volume['id']
|
app_inst = _get_name(volume['id'])
|
||||||
try:
|
try:
|
||||||
self._issue_api_request(URL_TEMPLATES['ai_inst'].format(app_inst),
|
self._issue_api_request(URL_TEMPLATES['ai_inst']().format(
|
||||||
|
app_inst),
|
||||||
method='delete')
|
method='delete')
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
msg = _LI("Tried to delete volume %s, but it was not found in the "
|
msg = _LI("Tried to delete volume %s, but it was not found in the "
|
||||||
"Datera cluster. Continuing with delete.")
|
"Datera cluster. Continuing with delete.")
|
||||||
LOG.info(msg, volume['id'])
|
LOG.info(msg, _get_name(volume['id']))
|
||||||
|
|
||||||
def ensure_export(self, context, volume, connector):
|
def ensure_export(self, context, volume, connector):
|
||||||
"""Gets the associated account, retrieves CHAP info and updates."""
|
"""Gets the associated account, retrieves CHAP info and updates."""
|
||||||
@ -306,7 +310,7 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
def initialize_connection(self, volume, connector):
|
def initialize_connection(self, volume, connector):
|
||||||
# Now online the app_instance (which will online all storage_instances)
|
# Now online the app_instance (which will online all storage_instances)
|
||||||
multipath = connector.get('multipath', False)
|
multipath = connector.get('multipath', False)
|
||||||
url = URL_TEMPLATES['ai_inst'].format(volume['id'])
|
url = URL_TEMPLATES['ai_inst']().format(_get_name(volume['id']))
|
||||||
data = {
|
data = {
|
||||||
'admin_state': 'online'
|
'admin_state': 'online'
|
||||||
}
|
}
|
||||||
@ -349,20 +353,23 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
|
|
||||||
def create_export(self, context, volume, connector):
|
def create_export(self, context, volume, connector):
|
||||||
# Online volume in case it hasn't been already
|
# Online volume in case it hasn't been already
|
||||||
url = URL_TEMPLATES['ai_inst'].format(volume['id'])
|
url = URL_TEMPLATES['ai_inst']().format(_get_name(volume['id']))
|
||||||
data = {
|
data = {
|
||||||
'admin_state': 'online'
|
'admin_state': 'online'
|
||||||
}
|
}
|
||||||
self._issue_api_request(url, method='put', body=data)
|
self._issue_api_request(url, method='put', body=data)
|
||||||
# Check if we've already setup everything for this volume
|
# Check if we've already setup everything for this volume
|
||||||
url = (URL_TEMPLATES['si'].format(volume['id']))
|
url = (URL_TEMPLATES['si']().format(_get_name(volume['id'])))
|
||||||
storage_instances = self._issue_api_request(url)
|
storage_instances = self._issue_api_request(url)
|
||||||
# Handle adding initiator to product if necessary
|
# Handle adding initiator to product if necessary
|
||||||
# Then add initiator to ACL
|
# Then add initiator to ACL
|
||||||
if connector and connector.get('initiator') and not self.allow_all:
|
policies = self._get_policies_for_resource(volume)
|
||||||
|
if (connector and
|
||||||
|
connector.get('initiator') and
|
||||||
|
not policies['acl_allow_all']):
|
||||||
initiator_name = "OpenStack_{}_{}".format(
|
initiator_name = "OpenStack_{}_{}".format(
|
||||||
self.driver_prefix, str(uuid.uuid4())[:4])
|
self.driver_prefix, str(uuid.uuid4())[:4])
|
||||||
initiator_group = 'IG-' + volume['id']
|
initiator_group = INITIATOR_GROUP_PREFIX + volume['id']
|
||||||
found = False
|
found = False
|
||||||
initiator = connector['initiator']
|
initiator = connector['initiator']
|
||||||
current_initiators = self._issue_api_request('initiators')
|
current_initiators = self._issue_api_request('initiators')
|
||||||
@ -390,24 +397,31 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
conflict_ok=True)
|
conflict_ok=True)
|
||||||
# Create ACL with initiator group as reference for each
|
# Create ACL with initiator group as reference for each
|
||||||
# storage_instance in app_instance
|
# storage_instance in app_instance
|
||||||
# TODO(_alastor_) We need to avoid changing the ACLs if the
|
# TODO(_alastor_): We need to avoid changing the ACLs if the
|
||||||
# template already specifies an ACL policy.
|
# template already specifies an ACL policy.
|
||||||
for si_name in storage_instances.keys():
|
for si_name in storage_instances.keys():
|
||||||
acl_url = (URL_TEMPLATES['si'] + "/{}/acl_policy").format(
|
acl_url = (URL_TEMPLATES['si']() + "/{}/acl_policy").format(
|
||||||
volume['id'], si_name)
|
_get_name(volume['id']), si_name)
|
||||||
data = {'initiator_groups': [initiator_group_path]}
|
data = {'initiator_groups': [initiator_group_path]}
|
||||||
self._issue_api_request(acl_url,
|
self._issue_api_request(acl_url,
|
||||||
method="put",
|
method="put",
|
||||||
body=data)
|
body=data)
|
||||||
|
|
||||||
if connector and connector.get('ip'):
|
if connector and connector.get('ip'):
|
||||||
# Determine IP Pool from IP and update storage_instance
|
|
||||||
try:
|
try:
|
||||||
|
# Case where volume_type has non default IP Pool info
|
||||||
|
if policies['ip_pool'] != 'default':
|
||||||
|
initiator_ip_pool_path = self._issue_api_request(
|
||||||
|
"access_network_ip_pools/{}".format(
|
||||||
|
policies['ip_pool']))['path']
|
||||||
|
# Fallback to trying reasonable IP based guess
|
||||||
|
else:
|
||||||
initiator_ip_pool_path = self._get_ip_pool_for_string_ip(
|
initiator_ip_pool_path = self._get_ip_pool_for_string_ip(
|
||||||
connector['ip'])
|
connector['ip'])
|
||||||
|
|
||||||
ip_pool_url = URL_TEMPLATES['si_inst'].format(
|
ip_pool_url = URL_TEMPLATES['si_inst'](
|
||||||
volume['id'])
|
policies['default_storage_name']).format(
|
||||||
|
_get_name(volume['id']))
|
||||||
ip_pool_data = {'ip_pool': initiator_ip_pool_path}
|
ip_pool_data = {'ip_pool': initiator_ip_pool_path}
|
||||||
self._issue_api_request(ip_pool_url,
|
self._issue_api_request(ip_pool_url,
|
||||||
method="put",
|
method="put",
|
||||||
@ -415,13 +429,12 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
except exception.DateraAPIException:
|
except exception.DateraAPIException:
|
||||||
# Datera product 1.0 support
|
# Datera product 1.0 support
|
||||||
pass
|
pass
|
||||||
# Some versions of Datera software require more time to make the
|
|
||||||
# ISCSI lun available, but don't report that it's unavailable. We
|
# Check to ensure we're ready for go-time
|
||||||
# can remove this when we deprecate those versions
|
self._si_poll(volume, policies)
|
||||||
time.sleep(DATERA_SI_SLEEP)
|
|
||||||
|
|
||||||
def detach_volume(self, context, volume, attachment=None):
|
def detach_volume(self, context, volume, attachment=None):
|
||||||
url = URL_TEMPLATES['ai_inst'].format(volume['id'])
|
url = URL_TEMPLATES['ai_inst']().format(_get_name(volume['id']))
|
||||||
data = {
|
data = {
|
||||||
'admin_state': 'offline',
|
'admin_state': 'offline',
|
||||||
'force': True
|
'force': True
|
||||||
@ -432,7 +445,7 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
msg = _LI("Tried to detach volume %s, but it was not found in the "
|
msg = _LI("Tried to detach volume %s, but it was not found in the "
|
||||||
"Datera cluster. Continuing with detach.")
|
"Datera cluster. Continuing with detach.")
|
||||||
LOG.info(msg, volume['id'])
|
LOG.info(msg, volume['id'])
|
||||||
# TODO(_alastor_) Make acl cleaning multi-attach aware
|
# TODO(_alastor_): Make acl cleaning multi-attach aware
|
||||||
self._clean_acl(volume)
|
self._clean_acl(volume)
|
||||||
|
|
||||||
def _check_for_acl(self, initiator_path):
|
def _check_for_acl(self, initiator_path):
|
||||||
@ -449,8 +462,10 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _clean_acl(self, volume):
|
def _clean_acl(self, volume):
|
||||||
acl_url = (URL_TEMPLATES["si_inst"] + "/acl_policy").format(
|
policies = self._get_policies_for_resource(volume)
|
||||||
volume['id'])
|
acl_url = (URL_TEMPLATES["si_inst"](
|
||||||
|
policies['default_storage_name']) + "/acl_policy").format(
|
||||||
|
_get_name(volume['id']))
|
||||||
try:
|
try:
|
||||||
initiator_group = self._issue_api_request(acl_url)[
|
initiator_group = self._issue_api_request(acl_url)[
|
||||||
'initiator_groups'][0]
|
'initiator_groups'][0]
|
||||||
@ -470,8 +485,11 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
volume)
|
volume)
|
||||||
|
|
||||||
def create_snapshot(self, snapshot):
|
def create_snapshot(self, snapshot):
|
||||||
url_template = URL_TEMPLATES['vol_inst'] + '/snapshots'
|
policies = self._get_policies_for_resource(snapshot)
|
||||||
url = url_template.format(snapshot['volume_id'])
|
url_template = URL_TEMPLATES['vol_inst'](
|
||||||
|
policies['default_storage_name'],
|
||||||
|
policies['default_volume_name']) + '/snapshots'
|
||||||
|
url = url_template.format(_get_name(snapshot['volume_id']))
|
||||||
|
|
||||||
snap_params = {
|
snap_params = {
|
||||||
'uuid': snapshot['id'],
|
'uuid': snapshot['id'],
|
||||||
@ -479,8 +497,11 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
self._issue_api_request(url, method='post', body=snap_params)
|
self._issue_api_request(url, method='post', body=snap_params)
|
||||||
|
|
||||||
def delete_snapshot(self, snapshot):
|
def delete_snapshot(self, snapshot):
|
||||||
snap_temp = URL_TEMPLATES['vol_inst'] + '/snapshots'
|
policies = self._get_policies_for_resource(snapshot)
|
||||||
snapu = snap_temp.format(snapshot['volume_id'])
|
snap_temp = URL_TEMPLATES['vol_inst'](
|
||||||
|
policies['default_storage_name'],
|
||||||
|
policies['default_volume_name']) + '/snapshots'
|
||||||
|
snapu = snap_temp.format(_get_name(snapshot['volume_id']))
|
||||||
snapshots = self._issue_api_request(snapu, method='get')
|
snapshots = self._issue_api_request(snapu, method='get')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -495,11 +516,14 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
msg = _LI("Tried to delete snapshot %s, but was not found in "
|
msg = _LI("Tried to delete snapshot %s, but was not found in "
|
||||||
"Datera cluster. Continuing with delete.")
|
"Datera cluster. Continuing with delete.")
|
||||||
LOG.info(msg, snapshot['id'])
|
LOG.info(msg, _get_name(snapshot['id']))
|
||||||
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
def create_volume_from_snapshot(self, volume, snapshot):
|
||||||
snap_temp = URL_TEMPLATES['vol_inst'] + '/snapshots'
|
policies = self._get_policies_for_resource(snapshot)
|
||||||
snapu = snap_temp.format(snapshot['volume_id'])
|
snap_temp = URL_TEMPLATES['vol_inst'](
|
||||||
|
policies['default_storage_name'],
|
||||||
|
policies['default_volume_name']) + '/snapshots'
|
||||||
|
snapu = snap_temp.format(_get_name(snapshot['volume_id']))
|
||||||
snapshots = self._issue_api_request(snapu, method='get')
|
snapshots = self._issue_api_request(snapu, method='get')
|
||||||
for ts, snap in snapshots.items():
|
for ts, snap in snapshots.items():
|
||||||
if snap['uuid'] == snapshot['id']:
|
if snap['uuid'] == snapshot['id']:
|
||||||
@ -508,19 +532,183 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
else:
|
else:
|
||||||
raise exception.NotFound
|
raise exception.NotFound
|
||||||
|
|
||||||
src = "/" + (snap_temp + '/{}').format(snapshot['volume_id'], found_ts)
|
src = "/" + (snap_temp + '/{}').format(
|
||||||
|
_get_name(snapshot['volume_id']), found_ts)
|
||||||
app_params = (
|
app_params = (
|
||||||
{
|
{
|
||||||
'create_mode': 'openstack',
|
'create_mode': 'openstack',
|
||||||
'uuid': str(volume['id']),
|
'uuid': str(volume['id']),
|
||||||
'name': str(volume['id']),
|
'name': _get_name(volume['id']),
|
||||||
'clone_src': src,
|
'clone_src': src,
|
||||||
})
|
})
|
||||||
self._issue_api_request(
|
self._issue_api_request(
|
||||||
URL_TEMPLATES['ai'],
|
URL_TEMPLATES['ai'](),
|
||||||
method='post',
|
method='post',
|
||||||
body=app_params)
|
body=app_params)
|
||||||
|
|
||||||
|
def manage_existing(self, volume, existing_ref):
|
||||||
|
"""Manage an existing volume on the Datera backend
|
||||||
|
|
||||||
|
The existing_ref must be either the current name or Datera UUID of
|
||||||
|
an app_instance on the Datera backend in a colon separated list with
|
||||||
|
the storage instance name and volume name. This means only
|
||||||
|
single storage instances and single volumes are supported for
|
||||||
|
managing by cinder.
|
||||||
|
|
||||||
|
Eg.
|
||||||
|
|
||||||
|
existing_ref['source-name'] == app_inst_name:storage_inst_name:vol_name
|
||||||
|
|
||||||
|
:param volume: Cinder volume to manage
|
||||||
|
:param existing_ref: Driver-specific information used to identify a
|
||||||
|
volume
|
||||||
|
"""
|
||||||
|
existing_ref = existing_ref['source-name']
|
||||||
|
if existing_ref.count(":") != 2:
|
||||||
|
raise exception.ManageExistingInvalidReference(
|
||||||
|
_("existing_ref argument must be of this format:"
|
||||||
|
"app_inst_name:storage_inst_name:vol_name"))
|
||||||
|
app_inst_name = existing_ref.split(":")[0]
|
||||||
|
LOG.debug("Managing existing Datera volume %(volume)s. "
|
||||||
|
"Changing name to %(existing)s",
|
||||||
|
existing=existing_ref, volume=_get_name(volume['id']))
|
||||||
|
data = {'name': _get_name(volume['id'])}
|
||||||
|
self._issue_api_request(URL_TEMPLATES['ai_inst']().format(
|
||||||
|
app_inst_name), method='put', body=data)
|
||||||
|
|
||||||
|
def manage_existing_get_size(self, volume, existing_ref):
|
||||||
|
"""Get the size of an unmanaged volume on the Datera backend
|
||||||
|
|
||||||
|
The existing_ref must be either the current name or Datera UUID of
|
||||||
|
an app_instance on the Datera backend in a colon separated list with
|
||||||
|
the storage instance name and volume name. This means only
|
||||||
|
single storage instances and single volumes are supported for
|
||||||
|
managing by cinder.
|
||||||
|
|
||||||
|
Eg.
|
||||||
|
|
||||||
|
existing_ref == app_inst_name:storage_inst_name:vol_name
|
||||||
|
|
||||||
|
:param volume: Cinder volume to manage
|
||||||
|
:param existing_ref: Driver-specific information used to identify a
|
||||||
|
volume on the Datera backend
|
||||||
|
"""
|
||||||
|
existing_ref = existing_ref['source-name']
|
||||||
|
if existing_ref.count(":") != 2:
|
||||||
|
raise exception.ManageExistingInvalidReference(
|
||||||
|
_("existing_ref argument must be of this format:"
|
||||||
|
"app_inst_name:storage_inst_name:vol_name"))
|
||||||
|
app_inst_name, si_name, vol_name = existing_ref.split(":")
|
||||||
|
app_inst = self._issue_api_request(
|
||||||
|
URL_TEMPLATES['ai_inst']().format(app_inst_name))
|
||||||
|
return self._get_size(volume, app_inst, si_name, vol_name)
|
||||||
|
|
||||||
|
def _get_size(self, volume, app_inst=None, si_name=None, vol_name=None):
|
||||||
|
"""Helper method for getting the size of a backend object
|
||||||
|
|
||||||
|
If app_inst is provided, we'll just parse the dict to get
|
||||||
|
the size instead of making a separate http request
|
||||||
|
"""
|
||||||
|
policies = self._get_policies_for_resource(volume)
|
||||||
|
si_name = si_name if si_name else policies['default_storage_name']
|
||||||
|
vol_name = vol_name if vol_name else policies['default_volume_name']
|
||||||
|
if not app_inst:
|
||||||
|
vol_url = URL_TEMPLATES['ai_inst']().format(
|
||||||
|
_get_name(volume['id']))
|
||||||
|
app_inst = self._issue_api_request(vol_url)
|
||||||
|
size = app_inst[
|
||||||
|
'storage_instances'][si_name]['volumes'][vol_name]['size']
|
||||||
|
return size
|
||||||
|
|
||||||
|
def get_manageable_volumes(self, cinder_volumes, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
"""List volumes on the backend available for management by Cinder.
|
||||||
|
|
||||||
|
Returns a list of dictionaries, each specifying a volume in the host,
|
||||||
|
with the following keys:
|
||||||
|
- reference (dictionary): The reference for a volume, which can be
|
||||||
|
passed to "manage_existing".
|
||||||
|
- size (int): The size of the volume according to the storage
|
||||||
|
backend, rounded up to the nearest GB.
|
||||||
|
- safe_to_manage (boolean): Whether or not this volume is safe to
|
||||||
|
manage according to the storage backend. For example, is the volume
|
||||||
|
in use or invalid for any reason.
|
||||||
|
- reason_not_safe (string): If safe_to_manage is False, the reason why.
|
||||||
|
- cinder_id (string): If already managed, provide the Cinder ID.
|
||||||
|
- extra_info (string): Any extra information to return to the user
|
||||||
|
|
||||||
|
:param cinder_volumes: A list of volumes in this host that Cinder
|
||||||
|
currently manages, used to determine if
|
||||||
|
a volume is manageable or not.
|
||||||
|
:param marker: The last item of the previous page; we return the
|
||||||
|
next results after this value (after sorting)
|
||||||
|
:param limit: Maximum number of items to return
|
||||||
|
:param offset: Number of items to skip after marker
|
||||||
|
:param sort_keys: List of keys to sort results by (valid keys are
|
||||||
|
'identifier' and 'size')
|
||||||
|
:param sort_dirs: List of directions to sort by, corresponding to
|
||||||
|
sort_keys (valid directions are 'asc' and 'desc')
|
||||||
|
"""
|
||||||
|
LOG.debug("Listing manageable Datera volumes")
|
||||||
|
app_instances = self._issue_api_request(URL_TEMPLATES['ai']()).values()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
cinder_volume_ids = [vol['id'] for vol in cinder_volumes]
|
||||||
|
|
||||||
|
for ai in app_instances:
|
||||||
|
ai_name = ai['name']
|
||||||
|
reference = None
|
||||||
|
size = None
|
||||||
|
safe_to_manage = False
|
||||||
|
reason_not_safe = None
|
||||||
|
cinder_id = None
|
||||||
|
extra_info = None
|
||||||
|
if re.match(UUID4_RE, ai_name):
|
||||||
|
cinder_id = ai_name.lstrip(OS_PREFIX)
|
||||||
|
if (not cinder_id and
|
||||||
|
ai_name.lstrip(OS_PREFIX) not in cinder_volume_ids):
|
||||||
|
safe_to_manage = self._is_manageable(ai)
|
||||||
|
if safe_to_manage:
|
||||||
|
si = list(ai['storage_instances'].values())[0]
|
||||||
|
si_name = si['name']
|
||||||
|
vol = list(si['volumes'].values())[0]
|
||||||
|
vol_name = vol['name']
|
||||||
|
size = vol['size']
|
||||||
|
reference = {"source-name": "{}:{}:{}".format(
|
||||||
|
ai_name, si_name, vol_name)}
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'reference': reference,
|
||||||
|
'size': size,
|
||||||
|
'safe_to_manage': safe_to_manage,
|
||||||
|
'reason_not_safe': reason_not_safe,
|
||||||
|
'cinder_id': cinder_id,
|
||||||
|
'extra_info': extra_info})
|
||||||
|
|
||||||
|
page_results = volutils.paginate_entries_list(
|
||||||
|
results, marker, limit, offset, sort_keys, sort_dirs)
|
||||||
|
|
||||||
|
return page_results
|
||||||
|
|
||||||
|
def _is_manageable(self, app_inst):
|
||||||
|
if len(app_inst['storage_instances']) == 1:
|
||||||
|
si = list(app_inst['storage_instances'].values())[0]
|
||||||
|
if len(si['volumes']) == 1:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unmanage(self, volume):
|
||||||
|
"""Unmanage a currently managed volume in Cinder
|
||||||
|
|
||||||
|
:param volume: Cinder volume to unmanage
|
||||||
|
"""
|
||||||
|
LOG.debug("Unmanaging Cinder volume %s. Changing name to %s",
|
||||||
|
volume['id'], _get_unmanaged(volume['id']))
|
||||||
|
data = {'name': _get_unmanaged(volume['id'])}
|
||||||
|
self._issue_api_request(URL_TEMPLATES['ai_inst']().format(
|
||||||
|
_get_name(volume['id'])), method='put', body=data)
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
def get_volume_stats(self, refresh=False):
|
||||||
"""Get volume stats.
|
"""Get volume stats.
|
||||||
|
|
||||||
@ -558,17 +746,199 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
|
|
||||||
self.cluster_stats = stats
|
self.cluster_stats = stats
|
||||||
|
|
||||||
def _get_policies_by_volume_type(self, type_id):
|
def _login(self):
|
||||||
|
"""Use the san_login and san_password to set token."""
|
||||||
|
body = {
|
||||||
|
'name': self.username,
|
||||||
|
'password': self.password
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unset token now, otherwise potential expired token will be sent
|
||||||
|
# along to be used for authorization when trying to login.
|
||||||
|
|
||||||
|
try:
|
||||||
|
LOG.debug('Getting Datera auth token.')
|
||||||
|
results = self._issue_api_request('login', 'put', body=body,
|
||||||
|
sensitive=True)
|
||||||
|
self.datera_api_token = results['key']
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error(_LE('Logging into the Datera cluster failed. Please '
|
||||||
|
'check your username and password set in the '
|
||||||
|
'cinder.conf and start the cinder-volume '
|
||||||
|
'service again.'))
|
||||||
|
|
||||||
|
def _get_lunid(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _init_vendor_properties(self):
|
||||||
|
"""Create a dictionary of vendor unique properties.
|
||||||
|
|
||||||
|
This method creates a dictionary of vendor unique properties
|
||||||
|
and returns both created dictionary and vendor name.
|
||||||
|
Returned vendor name is used to check for name of vendor
|
||||||
|
unique properties.
|
||||||
|
|
||||||
|
- Vendor name shouldn't include colon(:) because of the separator
|
||||||
|
and it is automatically replaced by underscore(_).
|
||||||
|
ex. abc:d -> abc_d
|
||||||
|
- Vendor prefix is equal to vendor name.
|
||||||
|
ex. abcd
|
||||||
|
- Vendor unique properties must start with vendor prefix + ':'.
|
||||||
|
ex. abcd:maxIOPS
|
||||||
|
|
||||||
|
Each backend driver needs to override this method to expose
|
||||||
|
its own properties using _set_property() like this:
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"vendorPrefix:specific_property",
|
||||||
|
"Title of property",
|
||||||
|
_("Description of property"),
|
||||||
|
"type")
|
||||||
|
|
||||||
|
: return dictionary of vendor unique properties
|
||||||
|
: return vendor name
|
||||||
|
|
||||||
|
prefix: DF --> Datera Fabric
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {}
|
||||||
|
|
||||||
|
if self.configuration.get('datera_debug_replica_count_override'):
|
||||||
|
replica_count = 1
|
||||||
|
else:
|
||||||
|
replica_count = 3
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:replica_count",
|
||||||
|
"Datera Volume Replica Count",
|
||||||
|
_("Specifies number of replicas for each volume. Can only be "
|
||||||
|
"increased once volume is created"),
|
||||||
|
"integer",
|
||||||
|
minimum=1,
|
||||||
|
default=replica_count)
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:acl_allow_all",
|
||||||
|
"Datera ACL Allow All",
|
||||||
|
_("True to set acl 'allow_all' on volumes created. Cannot be "
|
||||||
|
"changed on volume once set"),
|
||||||
|
"boolean",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:ip_pool",
|
||||||
|
"Datera IP Pool",
|
||||||
|
_("Specifies IP pool to use for volume"),
|
||||||
|
"string",
|
||||||
|
default="default")
|
||||||
|
|
||||||
|
# ###### QoS Settings ###### #
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:read_bandwidth_max",
|
||||||
|
"Datera QoS Max Bandwidth Read",
|
||||||
|
_("Max read bandwidth setting for volume qos, "
|
||||||
|
"use 0 for unlimited"),
|
||||||
|
"integer",
|
||||||
|
minimum=0,
|
||||||
|
default=0)
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:default_storage_name",
|
||||||
|
"Datera Default Storage Instance Name",
|
||||||
|
_("The name to use for storage instances created"),
|
||||||
|
"string",
|
||||||
|
default="storage-1")
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:default_volume_name",
|
||||||
|
"Datera Default Volume Name",
|
||||||
|
_("The name to use for volumes created"),
|
||||||
|
"string",
|
||||||
|
default="volume-1")
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:write_bandwidth_max",
|
||||||
|
"Datera QoS Max Bandwidth Write",
|
||||||
|
_("Max write bandwidth setting for volume qos, "
|
||||||
|
"use 0 for unlimited"),
|
||||||
|
"integer",
|
||||||
|
minimum=0,
|
||||||
|
default=0)
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:total_bandwidth_max",
|
||||||
|
"Datera QoS Max Bandwidth Total",
|
||||||
|
_("Max total bandwidth setting for volume qos, "
|
||||||
|
"use 0 for unlimited"),
|
||||||
|
"integer",
|
||||||
|
minimum=0,
|
||||||
|
default=0)
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:read_iops_max",
|
||||||
|
"Datera QoS Max iops Read",
|
||||||
|
_("Max read iops setting for volume qos, "
|
||||||
|
"use 0 for unlimited"),
|
||||||
|
"integer",
|
||||||
|
minimum=0,
|
||||||
|
default=0)
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:write_iops_max",
|
||||||
|
"Datera QoS Max IOPS Write",
|
||||||
|
_("Max write iops setting for volume qos, "
|
||||||
|
"use 0 for unlimited"),
|
||||||
|
"integer",
|
||||||
|
minimum=0,
|
||||||
|
default=0)
|
||||||
|
|
||||||
|
self._set_property(
|
||||||
|
properties,
|
||||||
|
"DF:total_iops_max",
|
||||||
|
"Datera QoS Max IOPS Total",
|
||||||
|
_("Max total iops setting for volume qos, "
|
||||||
|
"use 0 for unlimited"),
|
||||||
|
"integer",
|
||||||
|
minimum=0,
|
||||||
|
default=0)
|
||||||
|
# ###### End QoS Settings ###### #
|
||||||
|
|
||||||
|
return properties, 'DF'
|
||||||
|
|
||||||
|
def _get_policies_for_resource(self, resource):
|
||||||
"""Get extra_specs and qos_specs of a volume_type.
|
"""Get extra_specs and qos_specs of a volume_type.
|
||||||
|
|
||||||
This fetches the scoped keys from the volume type. Anything set from
|
This fetches the scoped keys from the volume type. Anything set from
|
||||||
qos_specs will override key/values set from extra_specs.
|
qos_specs will override key/values set from extra_specs.
|
||||||
"""
|
"""
|
||||||
|
type_id = resource.get('volume_type_id', None)
|
||||||
|
# Handle case of volume with no type. We still want the
|
||||||
|
# specified defaults from above
|
||||||
|
if type_id:
|
||||||
ctxt = context.get_admin_context()
|
ctxt = context.get_admin_context()
|
||||||
volume_type = volume_types.get_volume_type(ctxt, type_id)
|
volume_type = volume_types.get_volume_type(ctxt, type_id)
|
||||||
specs = volume_type.get('extra_specs')
|
specs = volume_type.get('extra_specs')
|
||||||
|
else:
|
||||||
|
volume_type = None
|
||||||
|
specs = {}
|
||||||
|
|
||||||
policies = {}
|
# Set defaults:
|
||||||
|
policies = {k.lstrip('DF:'): str(v['default']) for (k, v)
|
||||||
|
in self._init_vendor_properties()[0].items()}
|
||||||
|
|
||||||
|
if volume_type:
|
||||||
|
# Populate updated value
|
||||||
for key, value in specs.items():
|
for key, value in specs.items():
|
||||||
if ':' in key:
|
if ':' in key:
|
||||||
fields = key.split(':')
|
fields = key.split(':')
|
||||||
@ -580,8 +950,54 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
qos_kvs = qos_specs.get_qos_specs(ctxt, qos_specs_id)['specs']
|
qos_kvs = qos_specs.get_qos_specs(ctxt, qos_specs_id)['specs']
|
||||||
if qos_kvs:
|
if qos_kvs:
|
||||||
policies.update(qos_kvs)
|
policies.update(qos_kvs)
|
||||||
|
# Cast everything except booleans int that can be cast
|
||||||
|
for k, v in policies.items():
|
||||||
|
# Handle String Boolean case
|
||||||
|
if v == 'True' or v == 'False':
|
||||||
|
policies[k] = policies[k] == 'True'
|
||||||
|
continue
|
||||||
|
# Int cast
|
||||||
|
try:
|
||||||
|
policies[k] = int(v)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
return policies
|
return policies
|
||||||
|
|
||||||
|
def _si_poll(self, volume, policies):
|
||||||
|
# Initial 4 second sleep required for some Datera versions
|
||||||
|
eventlet.sleep(DEFAULT_SI_SLEEP)
|
||||||
|
TIMEOUT = 10
|
||||||
|
retry = 0
|
||||||
|
check_url = URL_TEMPLATES['si_inst'](
|
||||||
|
policies['default_storage_name']).format(_get_name(volume['id']))
|
||||||
|
poll = True
|
||||||
|
while poll and not retry >= TIMEOUT:
|
||||||
|
retry += 1
|
||||||
|
si = self._issue_api_request(check_url)
|
||||||
|
if si['op_state'] == 'available':
|
||||||
|
poll = False
|
||||||
|
else:
|
||||||
|
eventlet.sleep(1)
|
||||||
|
if retry >= TIMEOUT:
|
||||||
|
raise exception.VolumeDriverException(
|
||||||
|
message=_('Resource not ready.'))
|
||||||
|
|
||||||
|
def _update_qos(self, resource, policies):
|
||||||
|
url = URL_TEMPLATES['vol_inst'](
|
||||||
|
policies['default_storage_name'],
|
||||||
|
policies['default_volume_name']) + '/performance_policy'
|
||||||
|
url = url.format(_get_name(resource['id']))
|
||||||
|
type_id = resource.get('volume_type_id', None)
|
||||||
|
if type_id is not None:
|
||||||
|
# Filter for just QOS policies in result. All of their keys
|
||||||
|
# should end with "max"
|
||||||
|
fpolicies = {k: int(v) for k, v in
|
||||||
|
policies.items() if k.endswith("max")}
|
||||||
|
# Filter all 0 values from being passed
|
||||||
|
fpolicies = dict(filter(lambda _v: _v[1] > 0, fpolicies.items()))
|
||||||
|
if fpolicies:
|
||||||
|
self._issue_api_request(url, 'post', body=fpolicies)
|
||||||
|
|
||||||
def _get_ip_pool_for_string_ip(self, ip):
|
def _get_ip_pool_for_string_ip(self, ip):
|
||||||
"""Takes a string ipaddress and return the ip_pool API object dict """
|
"""Takes a string ipaddress and return the ip_pool API object dict """
|
||||||
pool = 'default'
|
pool = 'default'
|
||||||
@ -648,7 +1064,7 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
current_retry = 0
|
current_retry = 0
|
||||||
while current_retry <= self.retry_attempts:
|
while current_retry <= self.retry_attempts:
|
||||||
LOG.debug("Datera 503 response, trying request again")
|
LOG.debug("Datera 503 response, trying request again")
|
||||||
time.sleep(self.interval)
|
eventlet.sleep(self.interval)
|
||||||
resp = self._request(connection_string,
|
resp = self._request(connection_string,
|
||||||
method,
|
method,
|
||||||
payload,
|
payload,
|
||||||
@ -662,16 +1078,13 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
self._raise_response(response)
|
self._raise_response(response)
|
||||||
|
|
||||||
@_authenticated
|
@_authenticated
|
||||||
def _issue_api_request(self, resource_type, method='get', resource=None,
|
def _issue_api_request(self, resource_url, method='get', body=None,
|
||||||
body=None, action=None, sensitive=False,
|
sensitive=False, conflict_ok=False):
|
||||||
conflict_ok=False):
|
|
||||||
"""All API requests to Datera cluster go through this method.
|
"""All API requests to Datera cluster go through this method.
|
||||||
|
|
||||||
:param resource_type: the type of the resource
|
:param resource_url: the url of the resource
|
||||||
:param method: the request verb
|
:param method: the request verb
|
||||||
:param resource: the identifier of the resource
|
|
||||||
:param body: a dict with options for the action_type
|
:param body: a dict with options for the action_type
|
||||||
:param action: the action to perform
|
|
||||||
:returns: a dict of the response from the Datera cluster
|
:returns: a dict of the response from the Datera cluster
|
||||||
"""
|
"""
|
||||||
host = self.configuration.san_ip
|
host = self.configuration.san_ip
|
||||||
@ -682,7 +1095,8 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
payload = json.dumps(body, ensure_ascii=False)
|
payload = json.dumps(body, ensure_ascii=False)
|
||||||
payload.encode('utf-8')
|
payload.encode('utf-8')
|
||||||
|
|
||||||
header = {'Content-Type': 'application/json; charset=utf-8'}
|
header = {'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Datera-Driver': 'OpenStack-Cinder-{}'.format(self.VERSION)}
|
||||||
|
|
||||||
protocol = 'http'
|
protocol = 'http'
|
||||||
if self.configuration.driver_use_ssl:
|
if self.configuration.driver_use_ssl:
|
||||||
@ -700,12 +1114,7 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
cert_data = (client_cert, client_cert_key)
|
cert_data = (client_cert, client_cert_key)
|
||||||
|
|
||||||
connection_string = '%s://%s:%s/v%s/%s' % (protocol, host, port,
|
connection_string = '%s://%s:%s/v%s/%s' % (protocol, host, port,
|
||||||
api_version, resource_type)
|
api_version, resource_url)
|
||||||
|
|
||||||
if resource is not None:
|
|
||||||
connection_string += '/%s' % resource
|
|
||||||
if action is not None:
|
|
||||||
connection_string += '/%s' % action
|
|
||||||
|
|
||||||
response = self._request(connection_string,
|
response = self._request(connection_string,
|
||||||
method,
|
method,
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Capabilites List for Datera Volume Drivers
|
||||||
|
- Extended Volume-Type Support for Datera Volume Drivers
|
||||||
|
- Naming convention change for Datera Volume Drivers
|
||||||
|
- Volume Manage/Unmanage support for Datera Volume Drivers
|
||||||
|
- BoolOpt datera_debug_override_num_replicas was added for Datera Volume
|
||||||
|
Drivers
|
||||||
|
deprecations:
|
||||||
|
- datera_num_replicas IntOpt was changed to a VolumeType
|
||||||
|
Extra Spec -- DF:replica_count
|
||||||
|
- datera_acl_allow_all BoolOpt was changed to a VolumeType
|
||||||
|
Extra Spec -- DF:acl_allow_all
|
Loading…
x
Reference in New Issue
Block a user