Merge "Updating Datera DataFabric Driver to v2 of Datera DataFabric API"
This commit is contained in:
commit
1a8361ed32
@ -23,7 +23,12 @@ 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
|
||||||
|
DEFAULT_VOLUME_NAME = datera.DEFAULT_VOLUME_NAME
|
||||||
|
|
||||||
|
|
||||||
class DateraVolumeTestCase(test.TestCase):
|
class DateraVolumeTestCase(test.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DateraVolumeTestCase, self).setUp()
|
super(DateraVolumeTestCase, self).setUp()
|
||||||
|
|
||||||
@ -51,17 +56,7 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
self.addCleanup(self.api_patcher.stop)
|
self.addCleanup(self.api_patcher.stop)
|
||||||
|
|
||||||
def test_volume_create_success(self):
|
def test_volume_create_success(self):
|
||||||
self.mock_api.return_value = {
|
self.mock_api.return_value = stub_single_ai
|
||||||
u'status': u'available',
|
|
||||||
u'name': u'volume-00000001',
|
|
||||||
u'parent': u'00000000-0000-0000-0000-000000000000',
|
|
||||||
u'uuid': u'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
u'snapshots': {},
|
|
||||||
u'targets': {},
|
|
||||||
u'num_replicas': u'2',
|
|
||||||
u'sub_type': u'IS_ORIGINAL',
|
|
||||||
u'size': u'1073741824'
|
|
||||||
}
|
|
||||||
self.assertIsNone(self.driver.create_volume(self.volume))
|
self.assertIsNone(self.driver.create_volume(self.volume))
|
||||||
|
|
||||||
def test_volume_create_fails(self):
|
def test_volume_create_fails(self):
|
||||||
@ -74,31 +69,14 @@ 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:
|
||||||
return {
|
_bad_vol_ai = stub_single_ai.copy()
|
||||||
u'status': u'unavailable',
|
_bad_vol_ai['storage_instances'][
|
||||||
u'name': u'test',
|
DEFAULT_STORAGE_NAME]['volumes'][DEFAULT_VOLUME_NAME][
|
||||||
u'parent': u'00000000-0000-0000-0000-000000000000',
|
'op_status'] = 'unavailable'
|
||||||
u'uuid': u'9c1666fe-4f1a-4891-b33d-e710549527fe',
|
return _bad_vol_ai
|
||||||
u'snapshots': {},
|
|
||||||
u'targets': {},
|
|
||||||
u'num_replicas': u'2',
|
|
||||||
u'sub_type': u'IS_ORIGINAL',
|
|
||||||
u'size': u'1073741824'
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
self.mock_api.retry_count += 1
|
self.mock_api.retry_count += 1
|
||||||
return {
|
return stub_single_ai
|
||||||
u'status': u'available',
|
|
||||||
u'name': u'test',
|
|
||||||
u'parent': u'00000000-0000-0000-0000-000000000000',
|
|
||||||
u'uuid': u'9c1666fe-4f1a-4891-b33d-e710549527fe',
|
|
||||||
u'snapshots': {},
|
|
||||||
u'targets': {},
|
|
||||||
u'num_replicas': u'2',
|
|
||||||
u'sub_type': u'IS_ORIGINAL',
|
|
||||||
u'size': u'1073741824'
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mock_api.retry_count = 0
|
self.mock_api.retry_count = 0
|
||||||
self.mock_api.return_value = _progress_api_return(self.mock_api)
|
self.mock_api.return_value = _progress_api_return(self.mock_api)
|
||||||
self.assertEqual(1, self.mock_api.retry_count)
|
self.assertEqual(1, self.mock_api.retry_count)
|
||||||
@ -106,18 +84,7 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
|
|
||||||
@mock.patch.object(volume_types, 'get_volume_type')
|
@mock.patch.object(volume_types, 'get_volume_type')
|
||||||
def test_create_volume_with_extra_specs(self, mock_get_type):
|
def test_create_volume_with_extra_specs(self, mock_get_type):
|
||||||
self.mock_api.return_value = {
|
self.mock_api.return_value = stub_single_ai
|
||||||
u'status': u'available',
|
|
||||||
u'name': u'volume-00000001',
|
|
||||||
u'parent': u'00000000-0000-0000-0000-000000000000',
|
|
||||||
u'uuid': u'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
u'snapshots': {},
|
|
||||||
u'targets': {},
|
|
||||||
u'num_replicas': u'2',
|
|
||||||
u'sub_type': u'IS_ORIGINAL',
|
|
||||||
u'size': u'1073741824'
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_get_type.return_value = {
|
mock_get_type.return_value = {
|
||||||
'name': u'The Best',
|
'name': u'The Best',
|
||||||
'qos_specs_id': None,
|
'qos_specs_id': None,
|
||||||
@ -140,33 +107,10 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
volume_type_id='dffb4a83-b8fb-4c19-9f8c-713bb75db3b1'
|
volume_type_id='dffb4a83-b8fb-4c19-9f8c-713bb75db3b1'
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_body = {
|
|
||||||
u'max_iops_read': u'2000',
|
|
||||||
'numReplicas': '2',
|
|
||||||
'uuid': u'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
'size': '1073741824',
|
|
||||||
u'max_iops_write': u'4000',
|
|
||||||
u'max_iops_total': u'4000',
|
|
||||||
'name': u'volume-00000001'
|
|
||||||
}
|
|
||||||
|
|
||||||
self.assertIsNone(self.driver.create_volume(mock_volume))
|
self.assertIsNone(self.driver.create_volume(mock_volume))
|
||||||
self.mock_api.assert_called_once_with('volumes', 'post',
|
|
||||||
body=assert_body)
|
|
||||||
self.assertTrue(mock_get_type.called)
|
self.assertTrue(mock_get_type.called)
|
||||||
|
|
||||||
def test_create_cloned_volume_success(self):
|
def test_create_cloned_volume_success(self):
|
||||||
self.mock_api.return_value = {
|
|
||||||
'status': 'available',
|
|
||||||
'uuid': 'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
'size': '1073741824',
|
|
||||||
'name': 'volume-00000001',
|
|
||||||
'parent': '7f91abfa-7964-41ed-88fc-207c3a290b4f',
|
|
||||||
'snapshots': {},
|
|
||||||
'targets': {},
|
|
||||||
'numReplicas': '2',
|
|
||||||
'subType': 'IS_CLONE'
|
|
||||||
}
|
|
||||||
source_volume = _stub_volume(
|
source_volume = _stub_volume(
|
||||||
id='7f91abfa-7964-41ed-88fc-207c3a290b4f',
|
id='7f91abfa-7964-41ed-88fc-207c3a290b4f',
|
||||||
display_name='foo'
|
display_name='foo'
|
||||||
@ -185,15 +129,6 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
source_volume)
|
source_volume)
|
||||||
|
|
||||||
def test_delete_volume_success(self):
|
def test_delete_volume_success(self):
|
||||||
self.mock_api.return_value = {
|
|
||||||
'uuid': 'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
'size': '1073741824',
|
|
||||||
'name': 'volume-00000001',
|
|
||||||
'parent': '00000000-0000-0000-0000-000000000000',
|
|
||||||
'numReplicas': '2',
|
|
||||||
'subType': 'IS_ORIGINAL',
|
|
||||||
'target': None
|
|
||||||
}
|
|
||||||
self.assertIsNone(self.driver.delete_volume(self.volume))
|
self.assertIsNone(self.driver.delete_volume(self.volume))
|
||||||
|
|
||||||
def test_delete_volume_not_found(self):
|
def test_delete_volume_not_found(self):
|
||||||
@ -209,36 +144,38 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
self.mock_api.side_effect = self._generate_fake_api_request()
|
self.mock_api.side_effect = self._generate_fake_api_request()
|
||||||
ctxt = context.get_admin_context()
|
ctxt = context.get_admin_context()
|
||||||
expected = {
|
expected = {
|
||||||
'provider_location': '172.28.121.10:3260 iqn.2013-05.com.daterain'
|
'provider_location': '172.28.94.11:3260 iqn.2013-05.com.daterainc'
|
||||||
'c::01:sn:fc372bc0490b2dbe 0'
|
':c20aba21-6ef6-446b-b374-45733b4883ba--ST'
|
||||||
}
|
'--storage-1:01:sn:34e5b20fbadd3abb 0'}
|
||||||
|
|
||||||
self.assertEqual(expected, self.driver.ensure_export(ctxt,
|
self.assertEqual(expected, self.driver.ensure_export(ctxt,
|
||||||
self.volume))
|
self.volume,
|
||||||
|
None))
|
||||||
|
|
||||||
def test_ensure_export_fails(self):
|
def test_ensure_export_fails(self):
|
||||||
self.mock_api.side_effect = exception.DateraAPIException
|
self.mock_api.side_effect = exception.DateraAPIException
|
||||||
ctxt = context.get_admin_context()
|
ctxt = context.get_admin_context()
|
||||||
self.assertRaises(exception.DateraAPIException,
|
self.assertRaises(exception.DateraAPIException,
|
||||||
self.driver.ensure_export, ctxt, self.volume)
|
self.driver.ensure_export, ctxt, self.volume, None)
|
||||||
|
|
||||||
def test_create_export_target_does_not_exist_success(self):
|
def test_create_export_target_does_not_exist_success(self):
|
||||||
self.mock_api.side_effect = self._generate_fake_api_request(
|
self.mock_api.side_effect = self._generate_fake_api_request(
|
||||||
targets_exist=False)
|
targets_exist=False)
|
||||||
ctxt = context.get_admin_context()
|
ctxt = context.get_admin_context()
|
||||||
expected = {
|
expected = {
|
||||||
'provider_location': '172.28.121.10:3260 iqn.2013-05.com.daterainc'
|
'provider_location': '172.28.94.11:3260 iqn.2013-05.com.daterainc'
|
||||||
'::01:sn:fc372bc0490b2dbe 0'
|
':c20aba21-6ef6-446b-b374-45733b4883ba--ST'
|
||||||
}
|
'--storage-1:01:sn:34e5b20fbadd3abb 0'}
|
||||||
|
|
||||||
self.assertEqual(expected, self.driver.create_export(ctxt,
|
self.assertEqual(expected, self.driver.create_export(ctxt,
|
||||||
self.volume,
|
self.volume,
|
||||||
{}))
|
None))
|
||||||
|
|
||||||
def test_create_export_fails(self):
|
def test_create_export_fails(self):
|
||||||
self.mock_api.side_effect = exception.DateraAPIException
|
self.mock_api.side_effect = exception.DateraAPIException
|
||||||
ctxt = context.get_admin_context()
|
ctxt = context.get_admin_context()
|
||||||
self.assertRaises(exception.DateraAPIException,
|
self.assertRaises(exception.DateraAPIException,
|
||||||
self.driver.create_export, ctxt, self.volume, {})
|
self.driver.create_export, ctxt, self.volume, None)
|
||||||
|
|
||||||
def test_detach_volume_success(self):
|
def test_detach_volume_success(self):
|
||||||
self.mock_api.return_value = {}
|
self.mock_api.return_value = {}
|
||||||
@ -260,17 +197,6 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
self.assertIsNone(self.driver.detach_volume(ctxt, volume))
|
self.assertIsNone(self.driver.detach_volume(ctxt, volume))
|
||||||
|
|
||||||
def test_create_snapshot_success(self):
|
def test_create_snapshot_success(self):
|
||||||
self.mock_api.return_value = {
|
|
||||||
u'status': u'available',
|
|
||||||
u'uuid': u'0bb34f0c-fea4-48e0-bf96-591120ac7e3c',
|
|
||||||
u'parent': u'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
u'subType': u'IS_SNAPSHOT',
|
|
||||||
u'snapshots': {},
|
|
||||||
u'targets': {},
|
|
||||||
u'numReplicas': 2,
|
|
||||||
u'size': u'1073741824',
|
|
||||||
u'name': u'snapshot-00000001'
|
|
||||||
}
|
|
||||||
snapshot = _stub_snapshot(volume_id=self.volume['id'])
|
snapshot = _stub_snapshot(volume_id=self.volume['id'])
|
||||||
self.assertIsNone(self.driver.create_snapshot(snapshot))
|
self.assertIsNone(self.driver.create_snapshot(snapshot))
|
||||||
|
|
||||||
@ -281,19 +207,11 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
self.driver.create_snapshot, snapshot)
|
self.driver.create_snapshot, snapshot)
|
||||||
|
|
||||||
def test_delete_snapshot_success(self):
|
def test_delete_snapshot_success(self):
|
||||||
self.mock_api.return_value = {
|
|
||||||
u'uuid': u'0bb34f0c-fea4-48e0-bf96-591120ac7e3c',
|
|
||||||
u'parent': u'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
u'subType': u'IS_SNAPSHOT',
|
|
||||||
u'numReplicas': 2,
|
|
||||||
u'size': u'1073741824',
|
|
||||||
u'name': u'snapshot-00000001'
|
|
||||||
}
|
|
||||||
snapshot = _stub_snapshot(volume_id=self.volume['id'])
|
snapshot = _stub_snapshot(volume_id=self.volume['id'])
|
||||||
self.assertIsNone(self.driver.delete_snapshot(snapshot))
|
self.assertIsNone(self.driver.delete_snapshot(snapshot))
|
||||||
|
|
||||||
def test_delete_snapshot_not_found(self):
|
def test_delete_snapshot_not_found(self):
|
||||||
self.mock_api.side_effect = exception.NotFound
|
self.mock_api.side_effect = [stub_return_snapshots, exception.NotFound]
|
||||||
snapshot = _stub_snapshot(self.volume['id'])
|
snapshot = _stub_snapshot(self.volume['id'])
|
||||||
self.assertIsNone(self.driver.delete_snapshot(snapshot))
|
self.assertIsNone(self.driver.delete_snapshot(snapshot))
|
||||||
|
|
||||||
@ -304,18 +222,8 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
self.driver.delete_snapshot, snapshot)
|
self.driver.delete_snapshot, snapshot)
|
||||||
|
|
||||||
def test_create_volume_from_snapshot_success(self):
|
def test_create_volume_from_snapshot_success(self):
|
||||||
self.mock_api.return_value = {
|
|
||||||
u'status': u'available',
|
|
||||||
u'uuid': u'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
u'parent': u'0bb34f0c-fea4-48e0-bf96-591120ac7e3c',
|
|
||||||
u'snapshots': {},
|
|
||||||
u'targets': {},
|
|
||||||
u'subType': u'IS_ORIGINAL',
|
|
||||||
u'numReplicas': 2,
|
|
||||||
u'size': u'1073741824',
|
|
||||||
u'name': u'volume-00000001'
|
|
||||||
}
|
|
||||||
snapshot = _stub_snapshot(volume_id=self.volume['id'])
|
snapshot = _stub_snapshot(volume_id=self.volume['id'])
|
||||||
|
self.mock_api.side_effect = [stub_return_snapshots, None]
|
||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
self.driver.create_volume_from_snapshot(self.volume, snapshot))
|
self.driver.create_volume_from_snapshot(self.volume, snapshot))
|
||||||
|
|
||||||
@ -327,14 +235,6 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
snapshot)
|
snapshot)
|
||||||
|
|
||||||
def test_extend_volume_success(self):
|
def test_extend_volume_success(self):
|
||||||
self.mock_api.return_value = {
|
|
||||||
u'uuid': u'c20aba21-6ef6-446b-b374-45733b4883ba',
|
|
||||||
u'parent': u'00000000-0000-0000-0000-000000000000',
|
|
||||||
u'subType': u'IS_ORIGINAL',
|
|
||||||
u'numReplicas': 2,
|
|
||||||
u'size': u'2147483648',
|
|
||||||
u'name': u'volume-00000001'
|
|
||||||
}
|
|
||||||
volume = _stub_volume(size=1)
|
volume = _stub_volume(size=1)
|
||||||
self.assertIsNone(self.driver.extend_volume(volume, 2))
|
self.assertIsNone(self.driver.extend_volume(volume, 2))
|
||||||
|
|
||||||
@ -357,87 +257,163 @@ class DateraVolumeTestCase(test.TestCase):
|
|||||||
self.assertEqual(1, self.mock_api.call_count)
|
self.assertEqual(1, self.mock_api.call_count)
|
||||||
|
|
||||||
def _generate_fake_api_request(self, targets_exist=True):
|
def _generate_fake_api_request(self, targets_exist=True):
|
||||||
fake_volume = None
|
|
||||||
if not targets_exist:
|
|
||||||
fake_volume = _stub_datera_volume(targets={})
|
|
||||||
else:
|
|
||||||
fake_volume = _stub_datera_volume()
|
|
||||||
|
|
||||||
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):
|
||||||
if resource_type == 'volumes' and action is None:
|
if resource_type.split('/')[-1] == 'storage-1':
|
||||||
return fake_volume
|
|
||||||
elif resource_type == 'volume' and action == 'export':
|
|
||||||
return stub_create_export
|
|
||||||
elif resource_type == 'export_configs':
|
|
||||||
return stub_get_export
|
return stub_get_export
|
||||||
|
elif resource_type == 'app_instances':
|
||||||
|
return stub_single_ai
|
||||||
|
elif (resource_type.split('/')[-1] ==
|
||||||
|
'c20aba21-6ef6-446b-b374-45733b4883ba'):
|
||||||
|
return stub_app_instance[
|
||||||
|
'c20aba21-6ef6-446b-b374-45733b4883ba']
|
||||||
return _fake_api_request
|
return _fake_api_request
|
||||||
|
|
||||||
|
|
||||||
stub_create_export = {
|
stub_create_export = {
|
||||||
u'_ipColl': [u'172.28.121.10', u'172.28.120.10'],
|
"_ipColl": ["172.28.121.10", "172.28.120.10"],
|
||||||
u'active_initiators': [],
|
"acls": {},
|
||||||
u'activeServers': [u'4594953e-f97f-e111-ad85-001e6738c0f0'],
|
"activeServers": {"4594953e-f97f-e111-ad85-001e6738c0f0": "1"},
|
||||||
u'admin_state': u'online',
|
"ctype": "TC_BLOCK_ISCSI",
|
||||||
u'atype': u'none',
|
"endpointsExt1": {
|
||||||
u'creation_type': u'system_explicit',
|
"4594953e-f97f-e111-ad85-001e6738c0f0": {
|
||||||
u'endpoint_addrs': [u'172.30.128.2'],
|
"ipHigh": 0,
|
||||||
u'endpoint_idents': [u'iqn.2013-05.com.daterainc::01:sn:fc372bc0490b2dbe'],
|
"ipLow": "192421036",
|
||||||
u'initiators': [],
|
"ipStr": "172.28.120.11",
|
||||||
u'name': u'OS-a8b4d666',
|
"ipV": 4,
|
||||||
u'server_allocation': u'TS_ALLOC_COMPLETED',
|
"name": "",
|
||||||
|
"network": 24
|
||||||
u'servers': [u'4594953e-f97f-e111-ad85-001e6738c0f0'],
|
}
|
||||||
u'targetIds': {
|
},
|
||||||
u'4594953e-f97f-e111-ad85-001e6738c0f0': {
|
"endpointsExt2": {
|
||||||
u'ids': [{
|
"4594953e-f97f-e111-ad85-001e6738c0f0": {
|
||||||
u'dev': None,
|
"ipHigh": 0,
|
||||||
u'id': u'iqn.2013-05.com.daterainc::01:sn:fc372bc0490b2dbe'
|
"ipLow": "192486572",
|
||||||
|
"ipStr": "172.28.121.11",
|
||||||
|
"ipV": 4,
|
||||||
|
"name": "",
|
||||||
|
"network": 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inodes": {"c20aba21-6ef6-446b-b374-45733b4883ba": "1"},
|
||||||
|
"name": "",
|
||||||
|
"networkPort": 0,
|
||||||
|
"serverAllocation": "TS_ALLOC_COMPLETED",
|
||||||
|
"servers": {"4594953e-f97f-e111-ad85-001e6738c0f0": "1"},
|
||||||
|
"targetAllocation": "TS_ALLOC_COMPLETED",
|
||||||
|
"targetIds": {
|
||||||
|
"4594953e-f97f-e111-ad85-001e6738c0f0": {
|
||||||
|
"ids": [{
|
||||||
|
"dev": None,
|
||||||
|
"id": "iqn.2013-05.com.daterainc::01:sn:fc372bc0490b2dbe"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"typeName": "TargetIscsiConfig",
|
||||||
u'target_allocation': u'TS_ALLOC_COMPLETED',
|
"uuid": "7071efd7-9f22-4996-8f68-47e9ab19d0fd"
|
||||||
u'type': u'iscsi',
|
|
||||||
u'uuid': u'7071efd7-9f22-4996-8f68-47e9ab19d0fd',
|
|
||||||
u'volumes': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stub_get_export = {
|
|
||||||
"uuid": "744e1bd8-d741-4919-86cd-806037d98c8a",
|
stub_app_instance = {
|
||||||
"active_initiators": [],
|
"c20aba21-6ef6-446b-b374-45733b4883ba": {
|
||||||
"active_servers": [
|
|
||||||
"472764aa-584b-4c1d-a7b7-e50cf7f5518f"
|
|
||||||
],
|
|
||||||
"endpoint_addrs": [
|
|
||||||
"172.28.121.10",
|
|
||||||
"172.28.120.10"
|
|
||||||
],
|
|
||||||
"endpoint_idents": [
|
|
||||||
"iqn.2013-05.com.daterainc::01:sn:fc372bc0490b2dbe"
|
|
||||||
],
|
|
||||||
"initiators": [],
|
|
||||||
"servers": [
|
|
||||||
"472764aa-584b-4c1d-a7b7-e50cf7f5518f"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"10305aa4-1343-4363-86fe-f49eb421a48c"
|
|
||||||
],
|
|
||||||
"type": "iscsi",
|
|
||||||
"creation_type": "system_explicit",
|
|
||||||
"server_allocation": "TS_ALLOC_COMPLETED",
|
|
||||||
"admin_state": "online",
|
"admin_state": "online",
|
||||||
"target_allocation": "TS_ALLOC_COMPLETED",
|
"clone_src": {},
|
||||||
"atype": "none",
|
"create_mode": "openstack",
|
||||||
"name": "OS-10305aa4",
|
"descr": "",
|
||||||
"targetIds": {
|
"health": "ok",
|
||||||
"472764aa-584b-4c1d-a7b7-e50cf7f5518f": {
|
"name": "c20aba21-6ef6-446b-b374-45733b4883ba",
|
||||||
"ids": [{
|
"path": "/app_instances/c20aba21-6ef6-446b-b374-45733b4883ba",
|
||||||
"dev": "",
|
"storage_instances": {
|
||||||
"id": ("iqn.2013-05.com.daterainc::01:sn:fc372bc0490b2dbe")
|
"storage-1": {
|
||||||
}]
|
"access": {
|
||||||
|
"ips": [
|
||||||
|
"172.28.94.11"
|
||||||
|
],
|
||||||
|
"iqn": "iqn.2013-05.com.daterainc:c20aba21-6ef6-446b-"
|
||||||
|
"b374-45733b4883ba--ST--storage-1:01:sn:"
|
||||||
|
"34e5b20fbadd3abb",
|
||||||
|
"path": "/app_instances/c20aba21-6ef6-446b-b374"
|
||||||
|
"-45733b4883ba/storage_instances/storage-1/access"
|
||||||
|
},
|
||||||
|
"access_control": {
|
||||||
|
"initiator_groups": [],
|
||||||
|
"initiators": [],
|
||||||
|
"path": "/app_instances/c20aba21-6ef6-446b-b374-"
|
||||||
|
"45733b4883ba/storage_instances/storage-1"
|
||||||
|
"/access_control"
|
||||||
|
},
|
||||||
|
"access_control_mode": "allow_all",
|
||||||
|
"active_initiators": [],
|
||||||
|
"active_storage_nodes": [
|
||||||
|
"/storage_nodes/1c4feac4-17c7-478b-8928-c76e8ec80b72"
|
||||||
|
],
|
||||||
|
"admin_state": "online",
|
||||||
|
"auth": {
|
||||||
|
"initiator_pswd": "",
|
||||||
|
"initiator_user_name": "",
|
||||||
|
"path": "/app_instances/c20aba21-6ef6-446b-b374-"
|
||||||
|
"45733b4883ba/storage_instances/storage-1/auth",
|
||||||
|
"target_pswd": "",
|
||||||
|
"target_user_name": "",
|
||||||
|
"type": "none"
|
||||||
|
},
|
||||||
|
"creation_type": "user",
|
||||||
|
"descr": "c20aba21-6ef6-446b-b374-45733b4883ba__ST__storage-1",
|
||||||
|
"name": "storage-1",
|
||||||
|
"path": "/app_instances/c20aba21-6ef6-446b-b374-"
|
||||||
|
"45733b4883ba/storage_instances/storage-1",
|
||||||
|
"uuid": "b9897b84-149f-43c7-b19c-27d6af8fa815",
|
||||||
|
"volumes": {
|
||||||
|
"volume-1": {
|
||||||
|
"capacity_in_use": 0,
|
||||||
|
"name": "volume-1",
|
||||||
|
"op_state": "available",
|
||||||
|
"path": "/app_instances/c20aba21-6ef6-446b-b374-"
|
||||||
|
"45733b4883ba/storage_instances/storage-1"
|
||||||
|
"/volumes/volume-1",
|
||||||
|
"replica_count": 3,
|
||||||
|
"size": 500,
|
||||||
|
"snapshot_policies": {},
|
||||||
|
"snapshots": {
|
||||||
|
"1445384931.322468627": {
|
||||||
|
"op_state": "available",
|
||||||
|
"path": "/app_instances/c20aba21-6ef6-446b"
|
||||||
|
"-b374-45733b4883ba/storage_instances"
|
||||||
|
"/storage-1/volumes/volume-1/snapshots"
|
||||||
|
"/1445384931.322468627",
|
||||||
|
"uuid": "0bb34f0c-fea4-48e0-bf96-591120ac7e3c"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"uuid": "c20aba21-6ef6-446b-b374-45733b4883ba"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "c20aba21-6ef6-446b-b374-45733b4883ba"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stub_get_export = stub_app_instance[
|
||||||
|
'c20aba21-6ef6-446b-b374-45733b4883ba']['storage_instances']['storage-1']
|
||||||
|
|
||||||
|
stub_single_ai = stub_app_instance['c20aba21-6ef6-446b-b374-45733b4883ba']
|
||||||
|
|
||||||
|
stub_return_snapshots = \
|
||||||
|
{
|
||||||
|
"1446076293.118600738": {
|
||||||
|
"op_state": "available",
|
||||||
|
"path": "/app_instances/c20aba21-6ef6-446b-b374-45733b4883ba"
|
||||||
|
"/storage_instances/storage-1/volumes/volume-1/snapshots/"
|
||||||
|
"1446076293.118600738",
|
||||||
|
"uuid": "0bb34f0c-fea4-48e0-bf96-591120ac7e3c"
|
||||||
|
},
|
||||||
|
"1446076384.00607846": {
|
||||||
|
"op_state": "available",
|
||||||
|
"path": "/app_instances/c20aba21-6ef6-446b-b374-45733b4883ba"
|
||||||
|
"/storage_instances/storage-1/volumes/volume-1/snapshots/"
|
||||||
|
"1446076384.00607846",
|
||||||
|
"uuid": "25b4b959-c30a-45f2-a90c-84a40f34f0a1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright 2015 Datera
|
# Copyright 2016 Datera
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -17,7 +17,6 @@ import json
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_log import versionutils
|
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
from oslo_utils import units
|
from oslo_utils import units
|
||||||
import requests
|
import requests
|
||||||
@ -25,7 +24,7 @@ import six
|
|||||||
|
|
||||||
from cinder import context
|
from cinder import context
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _, _LE, _LI, _LW
|
from cinder.i18n import _, _LE, _LI
|
||||||
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
|
||||||
@ -34,28 +33,25 @@ from cinder.volume import volume_types
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
d_opts = [
|
d_opts = [
|
||||||
cfg.StrOpt('datera_api_token',
|
|
||||||
help='DEPRECATED: This will be removed in the Liberty release. '
|
|
||||||
'Use san_login and san_password instead. This directly '
|
|
||||||
'sets the Datera API token.'),
|
|
||||||
cfg.StrOpt('datera_api_port',
|
cfg.StrOpt('datera_api_port',
|
||||||
default='7717',
|
default='7717',
|
||||||
help='Datera API port.'),
|
help='Datera API port.'),
|
||||||
cfg.StrOpt('datera_api_version',
|
cfg.StrOpt('datera_api_version',
|
||||||
default='1',
|
default='2',
|
||||||
help='Datera API version.'),
|
help='Datera API version.'),
|
||||||
cfg.StrOpt('datera_num_replicas',
|
cfg.StrOpt('datera_num_replicas',
|
||||||
default='3',
|
default='1',
|
||||||
help='Number of replicas to create of an inode.')
|
help='Number of replicas to create of an inode.')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.import_opt('driver_client_cert_key', 'cinder.volume.driver')
|
|
||||||
CONF.import_opt('driver_client_cert', 'cinder.volume.driver')
|
|
||||||
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_VOLUME_NAME = 'volume-1'
|
||||||
|
|
||||||
|
|
||||||
def _authenticated(func):
|
def _authenticated(func):
|
||||||
"""Ensure the driver is authenticated to make a request.
|
"""Ensure the driver is authenticated to make a request.
|
||||||
@ -63,6 +59,7 @@ def _authenticated(func):
|
|||||||
In do_setup() we fetch an auth token and store it. If that expires when
|
In do_setup() we fetch an auth token and store it. If that expires when
|
||||||
we do API request, we'll fetch a new one.
|
we do API request, we'll fetch a new one.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def func_wrapper(self, *args, **kwargs):
|
def func_wrapper(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
@ -80,13 +77,15 @@ def _authenticated(func):
|
|||||||
|
|
||||||
|
|
||||||
class DateraDriver(san.SanISCSIDriver):
|
class DateraDriver(san.SanISCSIDriver):
|
||||||
|
|
||||||
"""The OpenStack Datera Driver
|
"""The OpenStack Datera Driver
|
||||||
|
|
||||||
Version history:
|
Version history:
|
||||||
1.0 - Initial driver
|
1.0 - Initial driver
|
||||||
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
|
||||||
"""
|
"""
|
||||||
VERSION = '1.1'
|
VERSION = '2.0'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DateraDriver, self).__init__(*args, **kwargs)
|
super(DateraDriver, self).__init__(*args, **kwargs)
|
||||||
@ -96,202 +95,7 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
self.password = self.configuration.san_password
|
self.password = self.configuration.san_password
|
||||||
self.auth_token = None
|
self.auth_token = None
|
||||||
self.cluster_stats = {}
|
self.cluster_stats = {}
|
||||||
|
self.datera_api_token = None
|
||||||
def do_setup(self, context):
|
|
||||||
# If any of the deprecated options are set, we'll warn the operator to
|
|
||||||
# use the new authentication method.
|
|
||||||
DEPRECATED_OPTS = [
|
|
||||||
self.configuration.driver_client_cert_key,
|
|
||||||
self.configuration.driver_client_cert,
|
|
||||||
self.configuration.datera_api_token
|
|
||||||
]
|
|
||||||
|
|
||||||
if any(DEPRECATED_OPTS):
|
|
||||||
msg = _LW("Client cert verification and datera_api_token are "
|
|
||||||
"deprecated in the Datera driver, and will be removed "
|
|
||||||
"in the Liberty release. Please set the san_login and "
|
|
||||||
"san_password in your cinder.conf instead.")
|
|
||||||
versionutils.report_deprecated_feature(LOG, msg)
|
|
||||||
return
|
|
||||||
|
|
||||||
# If we can't authenticate through the old and new method, just fail
|
|
||||||
# now.
|
|
||||||
if not all([self.username, self.password]):
|
|
||||||
msg = _("san_login and/or san_password is not set for Datera "
|
|
||||||
"driver in the cinder.conf. Set this information and "
|
|
||||||
"start the cinder-volume service again.")
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.InvalidInput(msg)
|
|
||||||
|
|
||||||
self._login()
|
|
||||||
|
|
||||||
@utils.retry(exception.VolumeDriverException, retries=3)
|
|
||||||
def _wait_for_resource(self, id, resource_type):
|
|
||||||
result = self._issue_api_request(resource_type, 'get', id)
|
|
||||||
if result['status'] == 'available':
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
raise exception.VolumeDriverException(message=
|
|
||||||
_('Resource not ready.'))
|
|
||||||
|
|
||||||
def _create_resource(self, resource, resource_type, body):
|
|
||||||
type_id = resource.get('volume_type_id', None)
|
|
||||||
if resource_type == 'volumes':
|
|
||||||
if type_id is not None:
|
|
||||||
policies = self._get_policies_by_volume_type(type_id)
|
|
||||||
if policies:
|
|
||||||
body.update(policies)
|
|
||||||
|
|
||||||
result = None
|
|
||||||
try:
|
|
||||||
result = self._issue_api_request(resource_type, 'post', body=body)
|
|
||||||
except exception.Invalid:
|
|
||||||
if resource_type == 'volumes' and type_id:
|
|
||||||
LOG.error(_LE("Creation request failed. Please verify the "
|
|
||||||
"extra-specs set for your volume types are "
|
|
||||||
"entered correctly."))
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
if result['status'] == 'available':
|
|
||||||
return
|
|
||||||
self._wait_for_resource(resource['id'], resource_type)
|
|
||||||
|
|
||||||
def create_volume(self, volume):
|
|
||||||
"""Create a logical volume."""
|
|
||||||
body = {
|
|
||||||
'name': volume['display_name'] or volume['id'],
|
|
||||||
'size': str(volume['size'] * units.Gi),
|
|
||||||
'uuid': volume['id'],
|
|
||||||
'numReplicas': self.num_replicas
|
|
||||||
}
|
|
||||||
self._create_resource(volume, 'volumes', body)
|
|
||||||
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
|
||||||
body = {
|
|
||||||
'name': volume['display_name'] or volume['id'],
|
|
||||||
'uuid': volume['id'],
|
|
||||||
'clone_uuid': src_vref['id'],
|
|
||||||
'numReplicas': self.num_replicas
|
|
||||||
}
|
|
||||||
self._create_resource(volume, 'volumes', body)
|
|
||||||
|
|
||||||
def delete_volume(self, volume):
|
|
||||||
try:
|
|
||||||
self._issue_api_request('volumes', 'delete', volume['id'])
|
|
||||||
except exception.NotFound:
|
|
||||||
LOG.info(_LI("Tried to delete volume %s, but it was not found in "
|
|
||||||
"the Datera cluster. Continuing with delete."),
|
|
||||||
volume['id'])
|
|
||||||
|
|
||||||
def _do_export(self, context, volume):
|
|
||||||
"""Gets the associated account, retrieves CHAP info and updates."""
|
|
||||||
portal = None
|
|
||||||
iqn = None
|
|
||||||
datera_volume = self._issue_api_request('volumes',
|
|
||||||
resource=volume['id'])
|
|
||||||
if len(datera_volume['targets']) == 0:
|
|
||||||
export = self._issue_api_request(
|
|
||||||
'volumes', action='export', method='post',
|
|
||||||
body={'ctype': 'TC_BLOCK_ISCSI'}, resource=volume['id'])
|
|
||||||
|
|
||||||
portal = "%s:3260" % export['endpoint_addrs'][0]
|
|
||||||
|
|
||||||
iqn = export['endpoint_idents'][0]
|
|
||||||
else:
|
|
||||||
export = self._issue_api_request(
|
|
||||||
'export_configs',
|
|
||||||
resource=datera_volume['targets'][0]
|
|
||||||
)
|
|
||||||
portal = export['endpoint_addrs'][0] + ':3260'
|
|
||||||
iqn = export['endpoint_idents'][0]
|
|
||||||
|
|
||||||
provider_location = '%s %s %s' % (portal, iqn, 0)
|
|
||||||
return {'provider_location': provider_location}
|
|
||||||
|
|
||||||
def ensure_export(self, context, volume):
|
|
||||||
return self._do_export(context, volume)
|
|
||||||
|
|
||||||
def create_export(self, context, volume, connector):
|
|
||||||
return self._do_export(context, volume)
|
|
||||||
|
|
||||||
def detach_volume(self, context, volume, attachment=None):
|
|
||||||
try:
|
|
||||||
self._issue_api_request('volumes', 'delete', resource=volume['id'],
|
|
||||||
action='export')
|
|
||||||
except exception.NotFound:
|
|
||||||
LOG.info(_LI("Tried to delete export for volume %s, but it was "
|
|
||||||
"not found in the Datera cluster. Continuing with "
|
|
||||||
"volume detach"), volume['id'])
|
|
||||||
|
|
||||||
def delete_snapshot(self, snapshot):
|
|
||||||
try:
|
|
||||||
self._issue_api_request('snapshots', 'delete', snapshot['id'])
|
|
||||||
except exception.NotFound:
|
|
||||||
LOG.info(_LI("Tried to delete snapshot %s, but was not found in "
|
|
||||||
"Datera cluster. Continuing with delete."),
|
|
||||||
snapshot['id'])
|
|
||||||
|
|
||||||
def create_snapshot(self, snapshot):
|
|
||||||
body = {
|
|
||||||
'uuid': snapshot['id'],
|
|
||||||
'parentUUID': snapshot['volume_id']
|
|
||||||
}
|
|
||||||
self._create_resource(snapshot, 'snapshots', body)
|
|
||||||
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
|
||||||
body = {
|
|
||||||
'name': volume['display_name'] or volume['id'],
|
|
||||||
'uuid': volume['id'],
|
|
||||||
'snapshot_uuid': snapshot['id'],
|
|
||||||
'numReplicas': self.num_replicas
|
|
||||||
}
|
|
||||||
self._create_resource(volume, 'volumes', body)
|
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
|
||||||
"""Get volume stats.
|
|
||||||
|
|
||||||
If 'refresh' is True, run update first.
|
|
||||||
The name is a bit misleading as
|
|
||||||
the majority of the data here is cluster
|
|
||||||
data.
|
|
||||||
"""
|
|
||||||
if refresh:
|
|
||||||
try:
|
|
||||||
self._update_cluster_stats()
|
|
||||||
except exception.DateraAPIException:
|
|
||||||
LOG.error(_LE('Failed to get updated stats from Datera '
|
|
||||||
'cluster.'))
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self.cluster_stats
|
|
||||||
|
|
||||||
def extend_volume(self, volume, new_size):
|
|
||||||
body = {
|
|
||||||
'size': str(new_size * units.Gi)
|
|
||||||
}
|
|
||||||
self._issue_api_request('volumes', 'put', body=body,
|
|
||||||
resource=volume['id'])
|
|
||||||
|
|
||||||
def _update_cluster_stats(self):
|
|
||||||
LOG.debug("Updating cluster stats info.")
|
|
||||||
|
|
||||||
results = self._issue_api_request('cluster')
|
|
||||||
|
|
||||||
if 'uuid' not in results:
|
|
||||||
LOG.error(_LE('Failed to get updated stats from Datera Cluster.'))
|
|
||||||
|
|
||||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
|
||||||
stats = {
|
|
||||||
'volume_backend_name': backend_name or 'Datera',
|
|
||||||
'vendor_name': 'Datera',
|
|
||||||
'driver_version': self.VERSION,
|
|
||||||
'storage_protocol': 'iSCSI',
|
|
||||||
'total_capacity_gb': int(results['totalRawSpace']),
|
|
||||||
'free_capacity_gb': int(results['availableSpace']),
|
|
||||||
'reserved_percentage': 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cluster_stats = stats
|
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
"""Use the san_login and san_password to set self.auth_token."""
|
"""Use the san_login and san_password to set self.auth_token."""
|
||||||
@ -306,9 +110,9 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
LOG.debug('Getting Datera auth token.')
|
LOG.debug('Getting Datera auth token.')
|
||||||
results = self._issue_api_request('login', 'post', body=body,
|
results = self._issue_api_request('login', 'put', body=body,
|
||||||
sensitive=True)
|
sensitive=True)
|
||||||
self.auth_token = results['key']
|
self.datera_api_token = results['key']
|
||||||
except exception.NotAuthorized:
|
except exception.NotAuthorized:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.error(_LE('Logging into the Datera cluster failed. Please '
|
LOG.error(_LE('Logging into the Datera cluster failed. Please '
|
||||||
@ -316,6 +120,275 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
'cinder.conf and start the cinder-volume '
|
'cinder.conf and start the cinder-volume '
|
||||||
'service again.'))
|
'service again.'))
|
||||||
|
|
||||||
|
def _get_lunid(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def do_setup(self, context):
|
||||||
|
# If we can't authenticate through the old and new method, just fail
|
||||||
|
# now.
|
||||||
|
if not all([self.username, self.password]):
|
||||||
|
msg = _("san_login and/or san_password is not set for Datera "
|
||||||
|
"driver in the cinder.conf. Set this information and "
|
||||||
|
"start the cinder-volume service again.")
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.InvalidInput(msg)
|
||||||
|
|
||||||
|
self._login()
|
||||||
|
|
||||||
|
@utils.retry(exception.VolumeDriverException, retries=3)
|
||||||
|
def _wait_for_resource(self, id, resource_type):
|
||||||
|
result = self._issue_api_request(resource_type, 'get', id)
|
||||||
|
if result['storage_instances'][DEFAULT_STORAGE_NAME]['volumes'][
|
||||||
|
DEFAULT_VOLUME_NAME]['op_state'] == 'available':
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise exception.VolumeDriverException(
|
||||||
|
message=_('Resource not ready.'))
|
||||||
|
|
||||||
|
def _create_resource(self, resource, resource_type, body):
|
||||||
|
type_id = resource.get('volume_type_id', None)
|
||||||
|
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
|
result = self._issue_api_request(resource_type, 'post', body=body)
|
||||||
|
except exception.Invalid:
|
||||||
|
if resource_type == 'volumes' and type_id:
|
||||||
|
LOG.error(_LE("Creation request failed. Please verify the "
|
||||||
|
"extra-specs set for your volume types are "
|
||||||
|
"entered correctly."))
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# Handle updating QOS Policies
|
||||||
|
if resource_type == 'app_instances':
|
||||||
|
url = ('app_instances/{}/storage_instances/{}/volumes/{'
|
||||||
|
'}/performance_policy')
|
||||||
|
url = url.format(
|
||||||
|
resource['id'],
|
||||||
|
DEFAULT_STORAGE_NAME,
|
||||||
|
DEFAULT_VOLUME_NAME)
|
||||||
|
if type_id is not None:
|
||||||
|
policies = self._get_policies_by_volume_type(type_id)
|
||||||
|
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
|
||||||
|
self._wait_for_resource(resource['id'], resource_type)
|
||||||
|
|
||||||
|
def create_volume(self, volume):
|
||||||
|
"""Create a logical volume."""
|
||||||
|
# Generate App Instance, Storage Instance and Volume
|
||||||
|
# Volume ID will be used as the App Instance Name
|
||||||
|
# Storage Instance and Volumes will have standard names
|
||||||
|
app_params = (
|
||||||
|
{
|
||||||
|
'create_mode': "openstack",
|
||||||
|
'uuid': str(volume['id']),
|
||||||
|
'name': str(volume['id']),
|
||||||
|
'access_control_mode': 'allow_all',
|
||||||
|
'storage_instances': {
|
||||||
|
DEFAULT_STORAGE_NAME: {
|
||||||
|
'name': DEFAULT_STORAGE_NAME,
|
||||||
|
'volumes': {
|
||||||
|
DEFAULT_VOLUME_NAME: {
|
||||||
|
'name': DEFAULT_VOLUME_NAME,
|
||||||
|
'size': volume['size'],
|
||||||
|
'replica_count': int(self.num_replicas),
|
||||||
|
'snapshot_policies': {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self._create_resource(volume, 'app_instances', body=app_params)
|
||||||
|
|
||||||
|
def extend_volume(self, volume, new_size):
|
||||||
|
# Offline App Instance, if necessary
|
||||||
|
reonline = False
|
||||||
|
app_inst = self._issue_api_request(
|
||||||
|
"app_instances/{}".format(volume['id']))
|
||||||
|
if app_inst['admin_state'] == 'online':
|
||||||
|
reonline = True
|
||||||
|
self.detach_volume(None, volume)
|
||||||
|
# Change Volume Size
|
||||||
|
app_inst = volume['id']
|
||||||
|
storage_inst = DEFAULT_STORAGE_NAME
|
||||||
|
data = {
|
||||||
|
'size': new_size
|
||||||
|
}
|
||||||
|
self._issue_api_request(
|
||||||
|
'app_instances/{}/storage_instances/{}/volumes/{}'.format(
|
||||||
|
app_inst, storage_inst, DEFAULT_VOLUME_NAME),
|
||||||
|
method='put', body=data)
|
||||||
|
# Online Volume, if it was online before
|
||||||
|
if reonline:
|
||||||
|
self.create_export(None, volume)
|
||||||
|
|
||||||
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
|
clone_src_template = ("/app_instances/{}/storage_instances/{"
|
||||||
|
"}/volumes/{}")
|
||||||
|
src = clone_src_template.format(src_vref['id'], DEFAULT_STORAGE_NAME,
|
||||||
|
DEFAULT_VOLUME_NAME)
|
||||||
|
data = {
|
||||||
|
'create_mode': 'openstack',
|
||||||
|
'name': str(volume['id']),
|
||||||
|
'uuid': str(volume['id']),
|
||||||
|
'clone_src': src,
|
||||||
|
'access_control_mode': 'allow_all'
|
||||||
|
}
|
||||||
|
self._issue_api_request('app_instances', 'post', body=data)
|
||||||
|
|
||||||
|
def delete_volume(self, volume):
|
||||||
|
self.detach_volume(None, volume)
|
||||||
|
app_inst = volume['id']
|
||||||
|
try:
|
||||||
|
self._issue_api_request('app_instances/{}'.format(app_inst),
|
||||||
|
method='delete')
|
||||||
|
except exception.NotFound:
|
||||||
|
msg = _LI("Tried to delete volume %s, but it was not found in the "
|
||||||
|
"Datera cluster. Continuing with delete.")
|
||||||
|
LOG.info(msg, volume['id'])
|
||||||
|
|
||||||
|
def ensure_export(self, context, volume, connector):
|
||||||
|
"""Gets the associated account, retrieves CHAP info and updates."""
|
||||||
|
return self.create_export(context, volume, connector)
|
||||||
|
|
||||||
|
def create_export(self, context, volume, connector):
|
||||||
|
url = "app_instances/{}".format(volume['id'])
|
||||||
|
data = {
|
||||||
|
'admin_state': 'online'
|
||||||
|
}
|
||||||
|
app_inst = self._issue_api_request(url, method='put', body=data)
|
||||||
|
storage_instance = app_inst['storage_instances'][
|
||||||
|
DEFAULT_STORAGE_NAME]
|
||||||
|
|
||||||
|
portal = storage_instance['access']['ips'][0] + ':3260'
|
||||||
|
iqn = storage_instance['access']['iqn']
|
||||||
|
|
||||||
|
# Portal, IQN, LUNID
|
||||||
|
provider_location = '%s %s %s' % (portal, iqn, self._get_lunid())
|
||||||
|
return {'provider_location': provider_location}
|
||||||
|
|
||||||
|
def detach_volume(self, context, volume, attachment=None):
|
||||||
|
url = "app_instances/{}".format(volume['id'])
|
||||||
|
data = {
|
||||||
|
'admin_state': 'offline',
|
||||||
|
'force': True
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self._issue_api_request(url, method='put', body=data)
|
||||||
|
except exception.NotFound:
|
||||||
|
msg = _LI("Tried to detach volume %s, but it was not found in the "
|
||||||
|
"Datera cluster. Continuing with detach.")
|
||||||
|
LOG.info(msg, volume['id'])
|
||||||
|
|
||||||
|
def create_snapshot(self, snapshot):
|
||||||
|
url_template = ('app_instances/{}/storage_instances/{}/volumes/{'
|
||||||
|
'}/snapshots')
|
||||||
|
url = url_template.format(snapshot['volume_id'],
|
||||||
|
DEFAULT_STORAGE_NAME,
|
||||||
|
DEFAULT_VOLUME_NAME)
|
||||||
|
|
||||||
|
snap_params = {
|
||||||
|
'uuid': snapshot['id'],
|
||||||
|
}
|
||||||
|
self._issue_api_request(url, method='post', body=snap_params)
|
||||||
|
|
||||||
|
def delete_snapshot(self, snapshot):
|
||||||
|
snap_temp = ('app_instances/{}/storage_instances/{}/volumes/{'
|
||||||
|
'}/snapshots')
|
||||||
|
snapu = snap_temp.format(snapshot['volume_id'],
|
||||||
|
DEFAULT_STORAGE_NAME,
|
||||||
|
DEFAULT_VOLUME_NAME)
|
||||||
|
|
||||||
|
snapshots = self._issue_api_request(snapu, method='get')
|
||||||
|
|
||||||
|
try:
|
||||||
|
for ts, snap in snapshots.items():
|
||||||
|
if snap['uuid'] == snapshot['id']:
|
||||||
|
url_template = snapu + '/{}'
|
||||||
|
url = url_template.format(ts)
|
||||||
|
self._issue_api_request(url, method='delete')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise exception.NotFound
|
||||||
|
except exception.NotFound:
|
||||||
|
msg = _LI("Tried to delete snapshot %s, but was not found in "
|
||||||
|
"Datera cluster. Continuing with delete.")
|
||||||
|
LOG.info(msg, snapshot['id'])
|
||||||
|
|
||||||
|
def create_volume_from_snapshot(self, volume, snapshot):
|
||||||
|
snap_temp = ('app_instances/{}/storage_instances/{}/volumes/{'
|
||||||
|
'}/snapshots')
|
||||||
|
snapu = snap_temp.format(snapshot['volume_id'],
|
||||||
|
DEFAULT_STORAGE_NAME,
|
||||||
|
DEFAULT_VOLUME_NAME)
|
||||||
|
|
||||||
|
snapshots = self._issue_api_request(snapu, method='get')
|
||||||
|
for ts, snap in snapshots.items():
|
||||||
|
if snap['uuid'] == snapshot['id']:
|
||||||
|
found_ts = ts
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise exception.NotFound
|
||||||
|
|
||||||
|
src = ('/app_instances/{}/storage_instances/{}/volumes/{'
|
||||||
|
'}/snapshots/{}'.format(
|
||||||
|
snapshot['volume_id'],
|
||||||
|
DEFAULT_STORAGE_NAME,
|
||||||
|
DEFAULT_VOLUME_NAME,
|
||||||
|
found_ts))
|
||||||
|
app_params = (
|
||||||
|
{
|
||||||
|
'create_mode': 'openstack',
|
||||||
|
'uuid': str(volume['id']),
|
||||||
|
'name': str(volume['id']),
|
||||||
|
'clone_src': src,
|
||||||
|
'access_control_mode': 'allow_all'
|
||||||
|
})
|
||||||
|
self._issue_api_request(
|
||||||
|
'app_instances',
|
||||||
|
method='post',
|
||||||
|
body=app_params)
|
||||||
|
|
||||||
|
def get_volume_stats(self, refresh=False):
|
||||||
|
"""Get volume stats.
|
||||||
|
|
||||||
|
If 'refresh' is True, run update first.
|
||||||
|
The name is a bit misleading as
|
||||||
|
the majority of the data here is cluster
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
if refresh or not self.cluster_stats:
|
||||||
|
try:
|
||||||
|
self._update_cluster_stats()
|
||||||
|
except exception.DateraAPIException:
|
||||||
|
LOG.error(_LE('Failed to get updated stats from Datera '
|
||||||
|
'cluster.'))
|
||||||
|
return self.cluster_stats
|
||||||
|
|
||||||
|
def _update_cluster_stats(self):
|
||||||
|
LOG.debug("Updating cluster stats info.")
|
||||||
|
|
||||||
|
results = self._issue_api_request('system')
|
||||||
|
|
||||||
|
if 'uuid' not in results:
|
||||||
|
LOG.error(_LE('Failed to get updated stats from Datera Cluster.'))
|
||||||
|
|
||||||
|
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||||
|
stats = {
|
||||||
|
'volume_backend_name': backend_name or 'Datera',
|
||||||
|
'vendor_name': 'Datera',
|
||||||
|
'driver_version': self.VERSION,
|
||||||
|
'storage_protocol': 'iSCSI',
|
||||||
|
'total_capacity_gb': int(results['total_capacity']) / units.Gi,
|
||||||
|
'free_capacity_gb': int(results['available_capacity']) / units.Gi,
|
||||||
|
'reserved_percentage': 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cluster_stats = stats
|
||||||
|
|
||||||
def _get_policies_by_volume_type(self, type_id):
|
def _get_policies_by_volume_type(self, type_id):
|
||||||
"""Get extra_specs and qos_specs of a volume_type.
|
"""Get extra_specs and qos_specs of a volume_type.
|
||||||
|
|
||||||
@ -354,7 +427,7 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
"""
|
"""
|
||||||
host = self.configuration.san_ip
|
host = self.configuration.san_ip
|
||||||
port = self.configuration.datera_api_port
|
port = self.configuration.datera_api_port
|
||||||
api_token = self.configuration.datera_api_token
|
api_token = self.datera_api_token
|
||||||
api_version = self.configuration.datera_api_version
|
api_version = self.configuration.datera_api_version
|
||||||
|
|
||||||
payload = json.dumps(body, ensure_ascii=False)
|
payload = json.dumps(body, ensure_ascii=False)
|
||||||
@ -363,10 +436,7 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
if not sensitive:
|
if not sensitive:
|
||||||
LOG.debug("Payload for Datera API call: %s", payload)
|
LOG.debug("Payload for Datera API call: %s", payload)
|
||||||
|
|
||||||
header = {
|
header = {'Content-Type': 'application/json; charset=utf-8'}
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
|
||||||
'auth-token': self.auth_token
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol = 'http'
|
protocol = 'http'
|
||||||
if self.configuration.driver_use_ssl:
|
if self.configuration.driver_use_ssl:
|
||||||
@ -399,8 +469,10 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
data=payload, headers=header,
|
data=payload, headers=header,
|
||||||
verify=False, cert=cert_data)
|
verify=False, cert=cert_data)
|
||||||
except requests.exceptions.RequestException as ex:
|
except requests.exceptions.RequestException as ex:
|
||||||
msg = _('Failed to make a request to Datera cluster endpoint due '
|
msg = _(
|
||||||
'to the following reason: %s') % six.text_type(ex.message)
|
'Failed to make a request to Datera cluster endpoint due '
|
||||||
|
'to the following reason: %s') % six.text_type(
|
||||||
|
ex.message)
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.DateraAPIException(msg)
|
raise exception.DateraAPIException(msg)
|
||||||
|
|
||||||
@ -409,6 +481,12 @@ class DateraDriver(san.SanISCSIDriver):
|
|||||||
LOG.debug("Results of Datera API call: %s", data)
|
LOG.debug("Results of Datera API call: %s", data)
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
|
LOG.debug(("Datera Response URL: %s\n"
|
||||||
|
"Datera Response Payload: %s\n"
|
||||||
|
"Response Object: %s\n"),
|
||||||
|
response.url,
|
||||||
|
payload,
|
||||||
|
vars(response))
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
raise exception.NotFound(data['message'])
|
raise exception.NotFound(data['message'])
|
||||||
elif response.status_code in [403, 401]:
|
elif response.status_code in [403, 401]:
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- All Datera DataFabric backed volume-types will now use
|
||||||
|
API version 2 with Datera DataFabric
|
||||||
|
upgrade:
|
||||||
|
- Users of the Datera Cinder driver are now required to use
|
||||||
|
Datera DataFabric version 1.0+. Versions before 1.0 will
|
||||||
|
not be able to utilize this new driver since they still
|
||||||
|
function on v1 of the Datera DataFabric API
|
||||||
|
deprecations:
|
||||||
|
- datera_api_token -- this has been replaced by
|
||||||
|
san_login and san_password
|
Loading…
Reference in New Issue
Block a user