JovianDSS: add certs and snapshot restore
Added support of authenticity verification through self-signed certificates for JovianDSS data storage. Added support of revert to snapshot functionality. Expanded unit-test coverage for JovianDSS driver. Change-Id: If0444fe479750dd79f3d3c3eb83b9d5c3e14053c Implements: bp jdss-add-cert-and-snapshot-revert
This commit is contained in:
parent
a1f567e3b3
commit
d501d1a880
@ -27,7 +27,6 @@ from cinder.volume.drivers.open_e import iscsi
|
||||
from cinder.volume.drivers.open_e.jovian_common import exception as jexc
|
||||
from cinder.volume.drivers.open_e.jovian_common import jdss_common as jcom
|
||||
|
||||
|
||||
UUID_1 = '12345678-1234-1234-1234-000000000001'
|
||||
UUID_2 = '12345678-1234-1234-1234-000000000002'
|
||||
UUID_3 = '12345678-1234-1234-1234-000000000003'
|
||||
@ -36,7 +35,7 @@ UUID_4 = '12345678-1234-1234-1234-000000000004'
|
||||
CONFIG_OK = {
|
||||
'san_hosts': ['192.168.0.2'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'https',
|
||||
'driver_use_ssl': 'true',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'jovian_user': 'admin',
|
||||
@ -53,7 +52,7 @@ CONFIG_OK = {
|
||||
CONFIG_BLOCK_SIZE = {
|
||||
'san_hosts': ['192.168.0.2'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'https',
|
||||
'driver_use_ssl': 'true',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'jovian_user': 'admin',
|
||||
@ -67,10 +66,27 @@ CONFIG_BLOCK_SIZE = {
|
||||
'jovian_block_size': '64K'
|
||||
}
|
||||
|
||||
CONFIG_BAD_BLOCK_SIZE = {
|
||||
'san_hosts': ['192.168.0.2'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'true',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'jovian_user': 'admin',
|
||||
'jovian_password': 'password',
|
||||
'jovian_ignore_tpath': [],
|
||||
'target_port': 3260,
|
||||
'jovian_pool': 'Pool-0',
|
||||
'target_prefix': 'iqn.2020-04.com.open-e.cinder:',
|
||||
'chap_password_len': 12,
|
||||
'san_thin_provision': False,
|
||||
'jovian_block_size': '61K'
|
||||
}
|
||||
|
||||
CONFIG_BACKEND_NAME = {
|
||||
'san_hosts': ['192.168.0.2'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'https',
|
||||
'driver_use_ssl': 'true',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'jovian_user': 'admin',
|
||||
@ -89,7 +105,7 @@ CONFIG_BACKEND_NAME = {
|
||||
CONFIG_MULTI_HOST = {
|
||||
'san_hosts': ['192.168.0.2', '192.168.0.3'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'https',
|
||||
'driver_use_ssl': 'true',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'jovian_user': 'admin',
|
||||
@ -168,10 +184,6 @@ def get_jdss_exceptions():
|
||||
return out
|
||||
|
||||
|
||||
def fake_safe_get(value):
|
||||
return CONFIG_OK[value]
|
||||
|
||||
|
||||
class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
|
||||
def get_driver(self, config):
|
||||
@ -179,10 +191,15 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
|
||||
cfg = mock.Mock()
|
||||
cfg.append_config_values.return_value = None
|
||||
cfg.safe_get = lambda val: config[val]
|
||||
cfg.get = lambda val, default: config.get(val, default)
|
||||
|
||||
jdssd = iscsi.JovianISCSIDriver()
|
||||
|
||||
jdssd.configuration = cfg
|
||||
lib_to_patch = ('cinder.volume.drivers.open_e.jovian_common.rest.'
|
||||
'JovianRESTAPI')
|
||||
with mock.patch(lib_to_patch) as ra:
|
||||
ra.is_pool_exists.return_value = True
|
||||
jdssd.do_setup(ctx)
|
||||
jdssd.ra = mock.Mock()
|
||||
return jdssd, ctx
|
||||
@ -195,6 +212,39 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
for p in patches:
|
||||
p.stop()
|
||||
|
||||
def test_check_for_setup_error(self):
|
||||
|
||||
cfg = mock.Mock()
|
||||
cfg.append_config_values.return_value = None
|
||||
|
||||
jdssd = iscsi.JovianISCSIDriver()
|
||||
jdssd.configuration = cfg
|
||||
|
||||
jdssd.ra = mock.Mock()
|
||||
|
||||
# No IP
|
||||
jdssd.ra.is_pool_exists.return_value = True
|
||||
jdssd.jovian_hosts = []
|
||||
jdssd.block_size = ['64K']
|
||||
|
||||
self.assertRaises(exception.VolumeDriverException,
|
||||
jdssd.check_for_setup_error)
|
||||
|
||||
# No pool detected
|
||||
jdssd.ra.is_pool_exists.return_value = False
|
||||
jdssd.jovian_hosts = ['192.168.0.2']
|
||||
jdssd.block_size = ['64K']
|
||||
|
||||
self.assertRaises(exception.VolumeDriverException,
|
||||
jdssd.check_for_setup_error)
|
||||
# Bad block size
|
||||
jdssd.ra.is_pool_exists.return_value = True
|
||||
jdssd.jovian_hosts = ['192.168.0.2', '192.168.0.3']
|
||||
jdssd.block_size = ['61K']
|
||||
|
||||
self.assertRaises(exception.InvalidConfigurationValue,
|
||||
jdssd.check_for_setup_error)
|
||||
|
||||
def test_get_provider_location(self):
|
||||
jdssd, ctx = self.get_driver(CONFIG_OK)
|
||||
host = CONFIG_OK["san_hosts"][0]
|
||||
@ -351,17 +401,18 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
SNAPSHOTS_EMPTY,
|
||||
SNAPSHOTS_EMPTY]
|
||||
|
||||
fake_gc = mock.Mock()
|
||||
fake_hide_object = mock.Mock()
|
||||
gc = mock.patch.object(jdssd, "_gc_delete", new=fake_gc)
|
||||
gc.start()
|
||||
hide = mock.patch.object(jdssd, "_hide_object", new=fake_hide_object)
|
||||
hide.start()
|
||||
patches = [mock.patch.object(jdssd, "_gc_delete"),
|
||||
mock.patch.object(jdssd, "_hide_object")]
|
||||
|
||||
self.start_patches(patches)
|
||||
|
||||
jdssd._cascade_volume_delete(o_vname, o_snaps)
|
||||
|
||||
jdssd._hide_object.assert_called_once_with(o_vname)
|
||||
hide.stop()
|
||||
jdssd._gc_delete.assert_not_called()
|
||||
gc.stop()
|
||||
|
||||
self.stop_patches(patches)
|
||||
|
||||
delete_snapshot_expected = [
|
||||
mock.call(o_vname,
|
||||
SNAPSHOTS_CASCADE_2[0]["name"],
|
||||
@ -399,17 +450,17 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
mock.call(SNAPSHOTS_CASCADE_1[1]["name"]),
|
||||
mock.call(o_vname)]
|
||||
|
||||
fake_gc = mock.Mock()
|
||||
fake_hide_object = mock.Mock()
|
||||
gc = mock.patch.object(jdssd, "_gc_delete", new=fake_gc)
|
||||
gc.start()
|
||||
hide = mock.patch.object(jdssd, "_hide_object", new=fake_hide_object)
|
||||
hide.start()
|
||||
patches = [mock.patch.object(jdssd, "_gc_delete"),
|
||||
mock.patch.object(jdssd, "_hide_object")]
|
||||
|
||||
self.start_patches(patches)
|
||||
|
||||
jdssd._cascade_volume_delete(o_vname, o_snaps)
|
||||
jdssd._hide_object.assert_has_calls(hide_object_expected)
|
||||
hide.stop()
|
||||
jdssd._gc_delete.assert_not_called()
|
||||
gc.stop()
|
||||
|
||||
self.stop_patches(patches)
|
||||
|
||||
jdssd.ra.get_snapshots.assert_has_calls(get_snapshots)
|
||||
|
||||
delete_snapshot_expected = [
|
||||
@ -522,7 +573,8 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
jdssd._gc_delete(jcom.vname(UUID_1))
|
||||
|
||||
jdssd._delete_back_recursively.assert_not_called()
|
||||
jdssd.ra.delete_lun.assert_called_once_with(jcom.vname(UUID_1))
|
||||
jdssd.ra.delete_lun.assert_called_once_with(jcom.vname(UUID_1),
|
||||
force_umount=True)
|
||||
|
||||
self.stop_patches(patches)
|
||||
|
||||
@ -655,6 +707,133 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
except Exception as err:
|
||||
self.assertIsInstance(err, exception.VolumeBackendAPIException)
|
||||
|
||||
def test_revert_to_snapshot(self):
|
||||
|
||||
jdssd, ctx = self.get_driver(CONFIG_OK)
|
||||
vol = fake_volume.fake_volume_obj(ctx)
|
||||
vol.id = UUID_1
|
||||
snap = fake_snapshot.fake_snapshot_obj(ctx)
|
||||
snap.id = UUID_2
|
||||
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
|
||||
get_lun_resp_1 = {'vscan': None,
|
||||
'full_name': 'Pool-0/' + UUID_1,
|
||||
'userrefs': None,
|
||||
'primarycache': 'all',
|
||||
'logbias': 'latency',
|
||||
'creation': '1591543140',
|
||||
'sync': 'always',
|
||||
'is_clone': False,
|
||||
'dedup': 'off',
|
||||
'sharenfs': None,
|
||||
'receive_resume_token': None,
|
||||
'volsize': '2147483648'}
|
||||
|
||||
get_lun_resp_2 = {'vscan': None,
|
||||
'full_name': 'Pool-0/' + UUID_1,
|
||||
'userrefs': None,
|
||||
'primarycache': 'all',
|
||||
'logbias': 'latency',
|
||||
'creation': '1591543140',
|
||||
'sync': 'always',
|
||||
'is_clone': False,
|
||||
'dedup': 'off',
|
||||
'sharenfs': None,
|
||||
'receive_resume_token': None,
|
||||
'volsize': '1073741824'}
|
||||
|
||||
jdssd.ra.get_lun.side_effect = [get_lun_resp_1, get_lun_resp_2]
|
||||
|
||||
get_lun_expected = [mock.call(vname), mock.call(vname)]
|
||||
|
||||
jdssd.revert_to_snapshot(ctx, vol, snap)
|
||||
|
||||
jdssd.ra.get_lun.assert_has_calls(get_lun_expected)
|
||||
|
||||
jdssd.ra.rollback_volume_to_snapshot.assert_called_once_with(vname,
|
||||
sname)
|
||||
jdssd.ra.extend_lun(vname, '2147483648')
|
||||
|
||||
def test_revert_to_snapshot_exception(self):
|
||||
|
||||
jdssd, ctx = self.get_driver(CONFIG_OK)
|
||||
vol = fake_volume.fake_volume_obj(ctx)
|
||||
vol.id = UUID_1
|
||||
snap = fake_snapshot.fake_snapshot_obj(ctx)
|
||||
snap.id = UUID_2
|
||||
|
||||
vname = jcom.vname(UUID_1)
|
||||
|
||||
get_lun_resp_no_size = {'vscan': None,
|
||||
'full_name': 'Pool-0/' + vname,
|
||||
'userrefs': None,
|
||||
'primarycache': 'all',
|
||||
'logbias': 'latency',
|
||||
'creation': '1591543140',
|
||||
'sync': 'always',
|
||||
'is_clone': False,
|
||||
'dedup': 'off',
|
||||
'sharenfs': None,
|
||||
'receive_resume_token': None,
|
||||
'volsize': None}
|
||||
|
||||
get_lun_resp_1 = {'vscan': None,
|
||||
'full_name': 'Pool-0/' + vname,
|
||||
'userrefs': None,
|
||||
'primarycache': 'all',
|
||||
'logbias': 'latency',
|
||||
'creation': '1591543140',
|
||||
'sync': 'always',
|
||||
'is_clone': False,
|
||||
'dedup': 'off',
|
||||
'sharenfs': None,
|
||||
'receive_resume_token': None,
|
||||
'volsize': '2147483648'}
|
||||
|
||||
get_lun_resp_2 = {'vscan': None,
|
||||
'full_name': 'Pool-0/' + vname,
|
||||
'userrefs': None,
|
||||
'primarycache': 'all',
|
||||
'logbias': 'latency',
|
||||
'creation': '1591543140',
|
||||
'sync': 'always',
|
||||
'is_clone': False,
|
||||
'dedup': 'off',
|
||||
'sharenfs': None,
|
||||
'receive_resume_token': None,
|
||||
'volsize': '1073741824'}
|
||||
|
||||
jdssd.ra.get_lun.side_effect = [get_lun_resp_no_size, get_lun_resp_2]
|
||||
|
||||
self.assertRaises(exception.VolumeDriverException,
|
||||
jdssd.revert_to_snapshot,
|
||||
ctx,
|
||||
vol,
|
||||
snap)
|
||||
|
||||
jdssd.ra.get_lun.side_effect = [get_lun_resp_1, get_lun_resp_2]
|
||||
|
||||
jdssd.ra.rollback_volume_to_snapshot.side_effect = [
|
||||
jexc.JDSSResourceNotFoundException(res=vname)]
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
jdssd.revert_to_snapshot,
|
||||
ctx,
|
||||
vol,
|
||||
snap)
|
||||
|
||||
jdssd.ra.get_lun.side_effect = [get_lun_resp_1,
|
||||
jexc.JDSSException("some_error")]
|
||||
|
||||
jdssd.ra.rollback_volume_to_snapshot.side_effect = [
|
||||
jexc.JDSSResourceNotFoundException(res=vname)]
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
jdssd.revert_to_snapshot,
|
||||
ctx,
|
||||
vol,
|
||||
snap)
|
||||
|
||||
def test_clone_object(self):
|
||||
jdssd, ctx = self.get_driver(CONFIG_OK)
|
||||
origin = jcom.vname(UUID_1)
|
||||
@ -1103,7 +1282,7 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
location_info = 'JovianISCSIDriver:192.168.0.2:Pool-0'
|
||||
correct_out = {
|
||||
'vendor_name': 'Open-E',
|
||||
'driver_version': "1.0.0",
|
||||
'driver_version': "1.0.1",
|
||||
'storage_protocol': 'iSCSI',
|
||||
'total_capacity_gb': 100,
|
||||
'free_capacity_gb': 50,
|
||||
@ -1311,6 +1490,7 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
jdssd._create_target_volume.assert_called_once_with(vol)
|
||||
|
||||
jdssd.ra.is_target_lun.assert_not_called()
|
||||
self.stop_patches(patches)
|
||||
|
||||
def test_remove_target_volume(self):
|
||||
|
||||
@ -1454,7 +1634,6 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': properties,
|
||||
}
|
||||
jdssd.ra.activate_target.return_value = None
|
||||
|
||||
ret = jdssd.initialize_connection(vol, connector)
|
||||
|
||||
|
@ -26,11 +26,12 @@ from cinder.volume.drivers.open_e.jovian_common import rest
|
||||
|
||||
UUID_1 = '12345678-1234-1234-1234-000000000001'
|
||||
UUID_2 = '12345678-1234-1234-1234-000000000002'
|
||||
UUID_3 = '12345678-1234-1234-1234-000000000003'
|
||||
|
||||
CONFIG_OK = {
|
||||
'san_hosts': ['192.168.0.2'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'https',
|
||||
'driver_use_ssl': 'true',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'san_login': 'admin',
|
||||
@ -45,10 +46,6 @@ CONFIG_OK = {
|
||||
}
|
||||
|
||||
|
||||
def fake_safe_get(value):
|
||||
return CONFIG_OK[value]
|
||||
|
||||
|
||||
class TestOpenEJovianRESTAPI(test.TestCase):
|
||||
|
||||
def get_rest(self, config):
|
||||
@ -57,19 +54,11 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
||||
cfg = mock.Mock()
|
||||
cfg.append_config_values.return_value = None
|
||||
cfg.safe_get = lambda val: config[val]
|
||||
cfg.get = lambda val, default: config[val]
|
||||
jdssr = rest.JovianRESTAPI(cfg)
|
||||
cfg.get = lambda val, default: config.get(val, default)
|
||||
jdssr = rest.JovianRESTAPI(config)
|
||||
jdssr.rproxy = mock.Mock()
|
||||
return jdssr, ctx
|
||||
|
||||
def start_patches(self, patches):
|
||||
for p in patches:
|
||||
p.start()
|
||||
|
||||
def stop_patches(self, patches):
|
||||
for p in patches:
|
||||
p.stop()
|
||||
|
||||
def test_get_active_host(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
@ -114,7 +103,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
resp = {'data': [{
|
||||
'vscan': None,
|
||||
'full_name': 'pool-0/' + UUID_1,
|
||||
'full_name': 'Pool-0/' + UUID_1,
|
||||
'userrefs': None,
|
||||
'primarycache': 'all',
|
||||
'logbias': 'latency',
|
||||
@ -148,7 +137,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
resp = {'data': {
|
||||
'vscan': None,
|
||||
'full_name': 'pool-0/' + jcom.vname(UUID_1),
|
||||
'full_name': 'Pool-0/' + jcom.vname(UUID_1),
|
||||
'userrefs': None,
|
||||
'primarycache': 'all',
|
||||
'logbias': 'latency',
|
||||
@ -231,7 +220,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
resp = {'data': {
|
||||
"vscan": None,
|
||||
"full_name": "pool-0/" + jcom.vname(UUID_1),
|
||||
"full_name": "Pool-0/" + jcom.vname(UUID_1),
|
||||
"userrefs": None,
|
||||
"primarycache": "all",
|
||||
"logbias": "latency",
|
||||
@ -268,7 +257,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
||||
def test_get_lun(self):
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
resp = {'data': {"vscan": None,
|
||||
"full_name": "pool-0/v_" + UUID_1,
|
||||
"full_name": "Pool-0/v_" + UUID_1,
|
||||
"userrefs": None,
|
||||
"primarycache": "all",
|
||||
"logbias": "latency",
|
||||
@ -995,3 +984,498 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
||||
self.assertRaises(jexc.JDSSException,
|
||||
jrest.detach_target_vol, tname, vname)
|
||||
jrest.rproxy.pool_request.assert_has_calls(detach_target_vol_expected)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
|
||||
data = {'name': jcom.sname(UUID_2)}
|
||||
resp = {'data': data,
|
||||
'error': None,
|
||||
'code': 201}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
self.assertIsNone(jrest.create_snapshot(vname, sname))
|
||||
|
||||
def test_create_snapshot_exception(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
|
||||
addr = '/volumes/{vol}/snapshots'.format(vol=vname)
|
||||
req = {'snapshot_name': sname}
|
||||
|
||||
url = ('http://192.168.0.2:82/api/v3/pools/Pool-0/volumes/{vol}/'
|
||||
'snapshots').format(vol=UUID_1)
|
||||
resp = {'data': None,
|
||||
'error': {
|
||||
'class': "zfslib.zfsapi.resources.ZfsResourceError",
|
||||
'errno': 1,
|
||||
'message': ('Zfs resource: Pool-0/{vol} not found in '
|
||||
'this collection.'.format(vol=vname)),
|
||||
"url": url},
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
create_snapshot_expected = [
|
||||
mock.call('POST', addr, json_data=req)]
|
||||
|
||||
self.assertRaises(jexc.JDSSVolumeNotFoundException,
|
||||
jrest.create_snapshot,
|
||||
vname,
|
||||
sname)
|
||||
|
||||
# snapshot exists
|
||||
resp = {'data': None,
|
||||
'error': {
|
||||
'class': "zfslib.zfsapi.resources.ZfsResourceError",
|
||||
'errno': 5,
|
||||
'message': 'Resource Pool-0/{vol}@{snap} already exists.',
|
||||
'url': url},
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
create_snapshot_expected += [mock.call('POST', addr, json_data=req)]
|
||||
self.assertRaises(jexc.JDSSSnapshotExistsException,
|
||||
jrest.create_snapshot,
|
||||
vname,
|
||||
sname)
|
||||
|
||||
# error unknown
|
||||
err = {"class": "some test error",
|
||||
"message": "test error message",
|
||||
"url": url,
|
||||
"errno": 123}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
create_snapshot_expected += [mock.call('POST', addr, json_data=req)]
|
||||
self.assertRaises(jexc.JDSSException,
|
||||
jrest.create_snapshot,
|
||||
vname,
|
||||
sname)
|
||||
jrest.rproxy.pool_request.assert_has_calls(create_snapshot_expected)
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
cname = jcom.vname(UUID_3)
|
||||
|
||||
addr = '/volumes/{vol}/clone'.format(vol=vname)
|
||||
jbody = {
|
||||
'name': cname,
|
||||
'snapshot': sname,
|
||||
'sparse': False
|
||||
}
|
||||
|
||||
data = {
|
||||
"origin": "Pool-0/{vol}@{snap}".format(vol=vname, snap=sname),
|
||||
"is_clone": True,
|
||||
"full_name": "Pool-0/{}".format(cname),
|
||||
"name": cname
|
||||
}
|
||||
|
||||
resp = {'data': data,
|
||||
'error': None,
|
||||
'code': 201}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
create_volume_from_snapshot_expected = [
|
||||
mock.call('POST', addr, json_data=jbody)]
|
||||
self.assertIsNone(jrest.create_volume_from_snapshot(cname,
|
||||
sname,
|
||||
vname))
|
||||
|
||||
jrest.rproxy.pool_request.assert_has_calls(
|
||||
create_volume_from_snapshot_expected)
|
||||
|
||||
def test_create_volume_from_snapshot_exception(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
cname = jcom.vname(UUID_3)
|
||||
|
||||
addr = '/volumes/{vol}/clone'.format(vol=vname)
|
||||
jbody = {
|
||||
'name': cname,
|
||||
'snapshot': sname,
|
||||
'sparse': False
|
||||
}
|
||||
|
||||
# volume DNE
|
||||
url = ('http://192.168.0.2:82/api/v3/pools/Pool-0/volumes/{vol}/'
|
||||
'clone').format(vol=UUID_1)
|
||||
resp = {'data': None,
|
||||
'error': {
|
||||
'class': "zfslib.zfsapi.resources.ZfsResourceError",
|
||||
'errno': 1,
|
||||
'message': ('Zfs resource: Pool-0/{vol} not found in '
|
||||
'this collection.'.format(vol=vname)),
|
||||
"url": url},
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
create_volume_from_snapshot_expected = [
|
||||
mock.call('POST', addr, json_data=jbody)]
|
||||
|
||||
self.assertRaises(jexc.JDSSResourceNotFoundException,
|
||||
jrest.create_volume_from_snapshot,
|
||||
cname,
|
||||
sname,
|
||||
vname)
|
||||
|
||||
# clone exists
|
||||
resp = {'data': None,
|
||||
'error': {
|
||||
"class": "zfslib.wrap.zfs.ZfsCmdError",
|
||||
"errno": 100,
|
||||
"message": ("cannot create 'Pool-0/{}': "
|
||||
"dataset already exists").format(vname),
|
||||
'url': url},
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
create_volume_from_snapshot_expected += [
|
||||
mock.call('POST', addr, json_data=jbody)]
|
||||
self.assertRaises(jexc.JDSSResourceExistsException,
|
||||
jrest.create_volume_from_snapshot,
|
||||
cname,
|
||||
sname,
|
||||
vname)
|
||||
|
||||
# error unknown
|
||||
err = {"class": "some test error",
|
||||
"message": "test error message",
|
||||
"url": url,
|
||||
"errno": 123}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
create_volume_from_snapshot_expected += [
|
||||
mock.call('POST', addr, json_data=jbody)]
|
||||
self.assertRaises(jexc.JDSSException,
|
||||
jrest.create_volume_from_snapshot,
|
||||
cname,
|
||||
sname,
|
||||
vname)
|
||||
jrest.rproxy.pool_request.assert_has_calls(
|
||||
create_volume_from_snapshot_expected)
|
||||
|
||||
def test_rollback_volume_to_snapshot(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
|
||||
req = ('/volumes/{vol}/snapshots/'
|
||||
'{snap}/rollback').format(vol=vname, snap=sname)
|
||||
|
||||
resp = {'data': None,
|
||||
'error': None,
|
||||
'code': 200}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
rollback_volume_to_snapshot_expected = [
|
||||
mock.call('POST', req)]
|
||||
self.assertIsNone(jrest.rollback_volume_to_snapshot(vname, sname))
|
||||
|
||||
jrest.rproxy.pool_request.assert_has_calls(
|
||||
rollback_volume_to_snapshot_expected)
|
||||
|
||||
def test_rollback_volume_to_snapshot_exception(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
|
||||
req = ('/volumes/{vol}/snapshots/'
|
||||
'{snap}/rollback').format(vol=vname,
|
||||
snap=sname)
|
||||
|
||||
# volume DNE
|
||||
msg = ('Zfs resource: Pool-0/{vname}'
|
||||
' not found in this collection.').format(vname=vname)
|
||||
|
||||
url = ('http://192.168.0.2:82/api/v3/pools/Pool-0/volumes/{vol}/'
|
||||
'snapshots/{snap}/rollback').format(vol=vname, snap=sname)
|
||||
err = {"class": "zfslib.zfsapi.resources.ZfsResourceError",
|
||||
"message": msg,
|
||||
"url": url,
|
||||
"errno": 123}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
rollback_volume_to_snapshot_expected = [
|
||||
mock.call('POST', req)]
|
||||
self.assertRaises(jexc.JDSSException,
|
||||
jrest.rollback_volume_to_snapshot,
|
||||
vname,
|
||||
sname)
|
||||
jrest.rproxy.pool_request.assert_has_calls(
|
||||
rollback_volume_to_snapshot_expected)
|
||||
|
||||
# error unknown
|
||||
err = {"class": "some test error",
|
||||
"message": "test error message",
|
||||
"url": url,
|
||||
"errno": 123}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
rollback_volume_to_snapshot_expected += [
|
||||
mock.call('POST', req)]
|
||||
self.assertRaises(jexc.JDSSException,
|
||||
jrest.rollback_volume_to_snapshot,
|
||||
vname,
|
||||
sname)
|
||||
jrest.rproxy.pool_request.assert_has_calls(
|
||||
rollback_volume_to_snapshot_expected)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
|
||||
addr = '/volumes/{vol}/snapshots/{snap}'.format(vol=vname, snap=sname)
|
||||
|
||||
jbody = {
|
||||
'recursively_children': True,
|
||||
'recursively_dependents': True,
|
||||
'force_umount': True
|
||||
}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': None,
|
||||
'code': 204}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
delete_snapshot_expected = [mock.call('DELETE', addr)]
|
||||
self.assertIsNone(jrest.delete_snapshot(vname, sname))
|
||||
|
||||
delete_snapshot_expected += [
|
||||
mock.call('DELETE', addr, json_data=jbody)]
|
||||
self.assertIsNone(jrest.delete_snapshot(vname,
|
||||
sname,
|
||||
recursively_children=True,
|
||||
recursively_dependents=True,
|
||||
force_umount=True))
|
||||
|
||||
jrest.rproxy.pool_request.assert_has_calls(delete_snapshot_expected)
|
||||
|
||||
def test_delete_snapshot_exception(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
sname = jcom.sname(UUID_2)
|
||||
cname = jcom.sname(UUID_3)
|
||||
|
||||
addr = '/volumes/{vol}/snapshots/{snap}'.format(vol=vname, snap=sname)
|
||||
|
||||
# snapshot busy
|
||||
url = ('http://192.168.0.2:82/api/v3/pools/Pool-0/volumes/{vol}/'
|
||||
'snapshots/{snap}').format(vol=vname, snap=sname)
|
||||
msg = ('cannot destroy "Pool-0/{vol}@{snap}": snapshot has dependent '
|
||||
'clones use "-R" to destroy the following datasets: '
|
||||
'Pool-0/{clone}').format(vol=vname, snap=sname, clone=cname)
|
||||
err = {'class': 'zfslib.wrap.zfs.ZfsCmdError',
|
||||
'message': msg,
|
||||
'url': url,
|
||||
'errno': 1000}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
delete_snapshot_expected = [
|
||||
mock.call('DELETE', addr)]
|
||||
|
||||
self.assertRaises(jexc.JDSSSnapshotIsBusyException,
|
||||
jrest.delete_snapshot,
|
||||
vname,
|
||||
sname)
|
||||
|
||||
# error unknown
|
||||
err = {"class": "some test error",
|
||||
"message": "test error message",
|
||||
"url": url,
|
||||
"errno": 123}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
delete_snapshot_expected += [mock.call('DELETE', addr)]
|
||||
self.assertRaises(jexc.JDSSException,
|
||||
jrest.delete_snapshot, vname, sname)
|
||||
|
||||
jrest.rproxy.pool_request.assert_has_calls(delete_snapshot_expected)
|
||||
|
||||
def test_get_snapshots(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
|
||||
addr = '/volumes/{vol}/snapshots'.format(vol=vname)
|
||||
|
||||
data = {"results": 2,
|
||||
"entries": {"referenced": "65536",
|
||||
"name": jcom.sname(UUID_2),
|
||||
"defer_destroy": "off",
|
||||
"userrefs": "0",
|
||||
"primarycache": "all",
|
||||
"type": "snapshot",
|
||||
"creation": "2015-5-27 16:8:35",
|
||||
"refcompressratio": "1.00x",
|
||||
"compressratio": "1.00x",
|
||||
"written": "65536",
|
||||
"used": "0",
|
||||
"clones": "",
|
||||
"mlslabel": "none",
|
||||
"secondarycache": "all"}}
|
||||
|
||||
resp = {'data': data,
|
||||
'error': None,
|
||||
'code': 200}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
get_snapshots_expected = [mock.call('GET', addr)]
|
||||
self.assertEqual(data['entries'], jrest.get_snapshots(vname))
|
||||
|
||||
jrest.rproxy.pool_request.assert_has_calls(get_snapshots_expected)
|
||||
|
||||
def test_get_snapshots_exception(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
vname = jcom.vname(UUID_1)
|
||||
|
||||
addr = '/volumes/{vol}/snapshots'.format(vol=vname)
|
||||
|
||||
url = ('http://192.168.0.2:82/api/v3/pools/Pool-0/volumes/{vol}/'
|
||||
'snapshots').format(vol=vname)
|
||||
|
||||
err = {"class": "zfslib.zfsapi.resources.ZfsResourceError",
|
||||
"message": ('Zfs resource: Pool-0/{vol} not found in '
|
||||
'this collection.').format(vol=vname),
|
||||
"url": url,
|
||||
"errno": 1}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
get_snapshots_expected = [mock.call('GET', addr)]
|
||||
self.assertRaises(jexc.JDSSResourceNotFoundException,
|
||||
jrest.get_snapshots,
|
||||
vname)
|
||||
|
||||
# error unknown
|
||||
err = {"class": "some test error",
|
||||
"message": "test error message",
|
||||
"url": url,
|
||||
"errno": 123}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
get_snapshots_expected += [
|
||||
mock.call('GET', addr)]
|
||||
self.assertRaises(jexc.JDSSException, jrest.get_snapshots, vname)
|
||||
|
||||
jrest.rproxy.pool_request.assert_has_calls(get_snapshots_expected)
|
||||
|
||||
def test_get_pool_stats(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
|
||||
addr = ''
|
||||
|
||||
data = {"available": "950040707072",
|
||||
"status": 26,
|
||||
"name": "Pool-0",
|
||||
"scan": None,
|
||||
"encryption": {"enabled": False},
|
||||
"iostats": {
|
||||
"read": "0",
|
||||
"write": "0",
|
||||
"chksum": "0"},
|
||||
"vdevs": [{"name": "wwn-0x5000cca3a8cddb2f",
|
||||
"iostats": {"read": "0",
|
||||
"write": "0",
|
||||
"chksum": "0"},
|
||||
"disks": [{"origin": "local",
|
||||
"led": "off",
|
||||
"name": "sdc",
|
||||
"iostats": {"read": "0",
|
||||
"write": "0",
|
||||
"chksum": "0"},
|
||||
"health": "ONLINE",
|
||||
"sn": "JPW9K0N20ZGXWE",
|
||||
"path": None,
|
||||
"model": "Hitachi HUA72201",
|
||||
"id": "wwn-0x5000cca3a8cddb2f",
|
||||
"size": 1000204886016}],
|
||||
"health": "ONLINE",
|
||||
"vdev_replacings": [],
|
||||
"vdev_spares": [],
|
||||
"type": ""}],
|
||||
"health": "ONLINE",
|
||||
"operation": "none",
|
||||
"id": "12413634663904564349",
|
||||
"size": "996432412672"}
|
||||
|
||||
resp = {'data': data,
|
||||
'error': None,
|
||||
'code': 200}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
get_pool_stats_expected = [mock.call('GET', addr)]
|
||||
self.assertEqual(data, jrest.get_pool_stats())
|
||||
|
||||
jrest.rproxy.pool_request.assert_has_calls(get_pool_stats_expected)
|
||||
|
||||
def test_get_pool_stats_exception(self):
|
||||
|
||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||
|
||||
addr = ''
|
||||
|
||||
url = 'http://192.168.0.2:82/api/v3/pools/Pool-0/'
|
||||
|
||||
err = {'class': 'zfslib.zfsapi.zpool.ZpoolError',
|
||||
'message': "Given zpool 'Pool-0' doesn't exists.",
|
||||
"url": url,
|
||||
"errno": 1}
|
||||
|
||||
resp = {'data': None,
|
||||
'error': err,
|
||||
'code': 500}
|
||||
|
||||
jrest.rproxy.pool_request.return_value = resp
|
||||
get_pool_stats_expected = [mock.call('GET', addr)]
|
||||
self.assertRaises(jexc.JDSSException, jrest.get_pool_stats)
|
||||
|
||||
jrest.rproxy.pool_request.assert_has_calls(get_pool_stats_expected)
|
||||
|
325
cinder/tests/unit/volume/drivers/open_e/test_rest_proxy.py
Normal file
325
cinder/tests/unit/volume/drivers/open_e/test_rest_proxy.py
Normal file
@ -0,0 +1,325 @@
|
||||
# Copyright (c) 2020 Open-E, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
import requests
|
||||
|
||||
from cinder import exception
|
||||
from cinder.tests.unit import test
|
||||
from cinder.volume.drivers.open_e.jovian_common import exception as jexc
|
||||
from cinder.volume.drivers.open_e.jovian_common import rest_proxy
|
||||
|
||||
UUID_1 = '12345678-1234-1234-1234-000000000001'
|
||||
UUID_2 = '12345678-1234-1234-1234-000000000002'
|
||||
UUID_3 = '12345678-1234-1234-1234-000000000003'
|
||||
|
||||
CONFIG_OK = {
|
||||
'san_hosts': ['192.168.0.2'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'true',
|
||||
'driver_ssl_cert_verify': True,
|
||||
'driver_ssl_cert_path': '/etc/cinder/joviandss.crt',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'san_login': 'admin',
|
||||
'san_password': 'password',
|
||||
'jovian_ignore_tpath': [],
|
||||
'target_port': 3260,
|
||||
'jovian_pool': 'Pool-0',
|
||||
'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:',
|
||||
'chap_password_len': 12,
|
||||
'san_thin_provision': False,
|
||||
'jovian_block_size': '128K'
|
||||
|
||||
}
|
||||
|
||||
CONFIG_BAD_IP = {
|
||||
'san_hosts': ['asd'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'true',
|
||||
'driver_ssl_cert_verify': True,
|
||||
'driver_ssl_cert_path': '/etc/cinder/joviandss.crt',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'san_login': 'admin',
|
||||
'san_password': 'password',
|
||||
'jovian_ignore_tpath': [],
|
||||
'target_port': 3260,
|
||||
'jovian_pool': 'Pool-0',
|
||||
'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:',
|
||||
'chap_password_len': 12,
|
||||
'san_thin_provision': False,
|
||||
'jovian_block_size': '128K'
|
||||
|
||||
}
|
||||
|
||||
CONFIG_MULTIHOST = {
|
||||
'san_hosts': ['192.168.0.2', '192.168.0.3', '192.168.0.4'],
|
||||
'san_api_port': 82,
|
||||
'driver_use_ssl': 'true',
|
||||
'driver_ssl_cert_verify': True,
|
||||
'driver_ssl_cert_path': '/etc/cinder/joviandss.crt',
|
||||
'jovian_rest_send_repeats': 3,
|
||||
'jovian_recovery_delay': 60,
|
||||
'san_login': 'admin',
|
||||
'san_password': 'password',
|
||||
'jovian_ignore_tpath': [],
|
||||
'target_port': 3260,
|
||||
'jovian_pool': 'Pool-0',
|
||||
'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:',
|
||||
'chap_password_len': 12,
|
||||
'san_thin_provision': False,
|
||||
'jovian_block_size': '128K'
|
||||
|
||||
}
|
||||
|
||||
|
||||
class TestOpenEJovianRESTProxy(test.TestCase):
|
||||
|
||||
def start_patches(self, patches):
|
||||
for p in patches:
|
||||
p.start()
|
||||
|
||||
def stop_patches(self, patches):
|
||||
for p in patches:
|
||||
p.stop()
|
||||
|
||||
def test_init(self):
|
||||
|
||||
self.assertRaises(exception.InvalidConfigurationValue,
|
||||
rest_proxy.JovianRESTProxy,
|
||||
CONFIG_BAD_IP)
|
||||
|
||||
def test_get_base_url(self):
|
||||
|
||||
proxy = rest_proxy.JovianRESTProxy(CONFIG_OK)
|
||||
|
||||
url = proxy._get_base_url()
|
||||
|
||||
exp = '{proto}://{host}:{port}/api/v3'.format(
|
||||
proto='https',
|
||||
host='192.168.0.2',
|
||||
port='82')
|
||||
self.assertEqual(exp, url)
|
||||
|
||||
def test_next_host(self):
|
||||
|
||||
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
|
||||
|
||||
self.assertEqual(0, proxy.active_host)
|
||||
proxy._next_host()
|
||||
|
||||
self.assertEqual(1, proxy.active_host)
|
||||
proxy._next_host()
|
||||
|
||||
self.assertEqual(2, proxy.active_host)
|
||||
proxy._next_host()
|
||||
|
||||
self.assertEqual(0, proxy.active_host)
|
||||
|
||||
def test_request(self):
|
||||
|
||||
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
|
||||
|
||||
patches = [
|
||||
mock.patch.object(requests, "Request", return_value="request"),
|
||||
mock.patch.object(proxy.session,
|
||||
"prepare_request",
|
||||
return_value="out_data"),
|
||||
mock.patch.object(proxy, "_send", return_value="out_data")]
|
||||
|
||||
addr = 'https://192.168.0.2:82/api/v3/pools/Pool-0'
|
||||
|
||||
self.start_patches(patches)
|
||||
proxy.request('GET', '/pools/Pool-0')
|
||||
|
||||
requests.Request.assert_called_once_with('GET', addr)
|
||||
self.stop_patches(patches)
|
||||
|
||||
def test_request_host_failure(self):
|
||||
|
||||
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
|
||||
|
||||
patches = [
|
||||
mock.patch.object(requests, "Request", return_value="request"),
|
||||
mock.patch.object(proxy.session,
|
||||
"prepare_request",
|
||||
return_value="out_data"),
|
||||
mock.patch.object(proxy, "_send", return_value="out_data")]
|
||||
|
||||
request_expected = [
|
||||
mock.call('GET',
|
||||
'https://192.168.0.2:82/api/v3/pools/Pool-0'),
|
||||
mock.call('GET',
|
||||
'https://192.168.0.3:82/api/v3/pools/Pool-0'),
|
||||
mock.call('GET',
|
||||
'https://192.168.0.4:82/api/v3/pools/Pool-0')]
|
||||
|
||||
self.start_patches(patches)
|
||||
|
||||
proxy._send.side_effect = [
|
||||
requests.exceptions.ConnectionError(),
|
||||
requests.exceptions.ConnectionError(),
|
||||
"out_data"]
|
||||
|
||||
proxy.request('GET', '/pools/Pool-0')
|
||||
self.assertEqual(2, proxy.active_host)
|
||||
requests.Request.assert_has_calls(request_expected)
|
||||
|
||||
self.stop_patches(patches)
|
||||
|
||||
def test_pool_request(self):
|
||||
|
||||
proxy = rest_proxy.JovianRESTProxy(CONFIG_OK)
|
||||
|
||||
patches = [mock.patch.object(proxy, "request")]
|
||||
|
||||
req = '/pools/Pool-0/volumes'
|
||||
|
||||
self.start_patches(patches)
|
||||
proxy.pool_request('GET', '/volumes')
|
||||
|
||||
proxy.request.assert_called_once_with('GET', req, json_data=None)
|
||||
self.stop_patches(patches)
|
||||
|
||||
def test_send(self):
|
||||
|
||||
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
|
||||
|
||||
json_data = {"data": [{"available": "949998694400",
|
||||
"status": 26,
|
||||
"name": "Pool-0",
|
||||
"scan": None,
|
||||
"encryption": {"enabled": False},
|
||||
"iostats": {"read": "0",
|
||||
"write": "0",
|
||||
"chksum": "0"},
|
||||
"vdevs": [{}],
|
||||
"health": "ONLINE",
|
||||
"operation": "none",
|
||||
"id": "12413634663904564349",
|
||||
"size": "996432412672"}],
|
||||
"error": None}
|
||||
session_ret = mock.Mock()
|
||||
session_ret.text = json.dumps(json_data)
|
||||
session_ret.status_code = 200
|
||||
patches = [mock.patch.object(proxy.session,
|
||||
"send",
|
||||
return_value=session_ret)]
|
||||
|
||||
pr = 'prepared_request'
|
||||
|
||||
self.start_patches(patches)
|
||||
ret = proxy._send(pr)
|
||||
|
||||
proxy.session.send.assert_called_once_with(pr)
|
||||
|
||||
self.assertEqual(0, proxy.active_host)
|
||||
|
||||
self.assertEqual(200, ret['code'])
|
||||
self.assertEqual(json_data['data'], ret['data'])
|
||||
self.assertEqual(json_data['error'], ret['error'])
|
||||
self.stop_patches(patches)
|
||||
|
||||
def test_send_connection_error(self):
|
||||
|
||||
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
|
||||
|
||||
json_data = {"data": None,
|
||||
"error": None}
|
||||
|
||||
session_ret = mock.Mock()
|
||||
session_ret.text = json.dumps(json_data)
|
||||
session_ret.status_code = 200
|
||||
patches = [mock.patch.object(proxy.session, "send")]
|
||||
|
||||
pr = 'prepared_request'
|
||||
|
||||
self.start_patches(patches)
|
||||
|
||||
side_effect = [requests.exceptions.ConnectionError()] * 4
|
||||
side_effect += [session_ret]
|
||||
|
||||
proxy.session.send.side_effect = side_effect
|
||||
|
||||
send_expected = [mock.call(pr)] * 4
|
||||
|
||||
ret = proxy._send(pr)
|
||||
|
||||
proxy.session.send.assert_has_calls(send_expected)
|
||||
|
||||
self.assertEqual(0, proxy.active_host)
|
||||
|
||||
self.assertEqual(200, ret['code'])
|
||||
self.assertEqual(json_data['data'], ret['data'])
|
||||
self.assertEqual(json_data['error'], ret['error'])
|
||||
self.stop_patches(patches)
|
||||
|
||||
def test_send_mixed_error(self):
|
||||
|
||||
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
|
||||
|
||||
json_data = {"data": None,
|
||||
"error": None}
|
||||
|
||||
session_ret = mock.Mock()
|
||||
session_ret.text = json.dumps(json_data)
|
||||
session_ret.status_code = 200
|
||||
patches = [mock.patch.object(proxy.session, "send")]
|
||||
|
||||
pr = 'prepared_request'
|
||||
|
||||
self.start_patches(patches)
|
||||
|
||||
side_effect = [requests.exceptions.ConnectionError()] * 4
|
||||
side_effect += [jexc.JDSSOSException()] * 4
|
||||
side_effect += [session_ret]
|
||||
|
||||
proxy.session.send.side_effect = side_effect
|
||||
|
||||
send_expected = [mock.call(pr)] * 7
|
||||
|
||||
self.assertRaises(jexc.JDSSOSException, proxy._send, pr)
|
||||
|
||||
proxy.session.send.assert_has_calls(send_expected)
|
||||
|
||||
self.assertEqual(0, proxy.active_host)
|
||||
|
||||
def test_handle_500(self):
|
||||
|
||||
error = {"class": "exceptions.OSError",
|
||||
"errno": 17,
|
||||
"message": ""}
|
||||
|
||||
json_data = {"data": None,
|
||||
"error": error}
|
||||
|
||||
session_ret = mock.Mock()
|
||||
session_ret.text = json.dumps(json_data)
|
||||
session_ret.status_code = 500
|
||||
|
||||
self.assertRaises(jexc.JDSSOSException,
|
||||
rest_proxy.JovianRESTProxy._handle_500,
|
||||
session_ret)
|
||||
|
||||
session_ret.status_code = 200
|
||||
json_data = {"data": None,
|
||||
"error": None}
|
||||
|
||||
session_ret.text = json.dumps(json_data)
|
||||
self.assertIsNone(rest_proxy.JovianRESTProxy._handle_500(session_ret))
|
@ -43,23 +43,25 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
.. code-block:: none
|
||||
|
||||
1.0.0 - Open-E JovianDSS driver with basic functionality
|
||||
1.0.1 - Added certificate support
|
||||
Added revert to snapshot support
|
||||
"""
|
||||
|
||||
# ThirdPartySystems wiki page
|
||||
CI_WIKI_NAME = "Open-E_JovianDSS_CI"
|
||||
VERSION = "1.0.0"
|
||||
VERSION = "1.0.1"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(JovianISCSIDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
self._stats = None
|
||||
self._pool = 'Pool-0'
|
||||
self.jovian_iscsi_target_portal_port = "3260"
|
||||
self.jovian_target_prefix = 'iqn.2020-04.com.open-e.cinder:'
|
||||
self.jovian_chap_pass_len = 12
|
||||
self.jovian_sparse = False
|
||||
self.jovian_ignore_tpath = None
|
||||
self.jovian_hosts = None
|
||||
self._pool = 'Pool-0'
|
||||
self.ra = None
|
||||
|
||||
@property
|
||||
@ -67,7 +69,8 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
"""Return backend name."""
|
||||
backend_name = None
|
||||
if self.configuration:
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
backend_name = self.configuration.get('volume_backend_name',
|
||||
'JovianDSS')
|
||||
if not backend_name:
|
||||
backend_name = self.__class__.__name__
|
||||
return backend_name
|
||||
@ -82,26 +85,30 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
options.jdss_volume_opts)
|
||||
self.configuration.append_config_values(san.san_opts)
|
||||
|
||||
self._pool = self.configuration.safe_get('jovian_pool')
|
||||
self.jovian_iscsi_target_portal_port = self.configuration.safe_get(
|
||||
'target_port')
|
||||
self._pool = self.configuration.get('jovian_pool', 'Pool-0')
|
||||
self.jovian_iscsi_target_portal_port = self.configuration.get(
|
||||
'target_port', 3260)
|
||||
|
||||
self.jovian_target_prefix = self.configuration.safe_get(
|
||||
'target_prefix')
|
||||
self.jovian_chap_pass_len = self.configuration.safe_get(
|
||||
'chap_password_len')
|
||||
self.jovian_target_prefix = self.configuration.get(
|
||||
'target_prefix',
|
||||
'iqn.2020-04.com.open-e.cinder:')
|
||||
self.jovian_chap_pass_len = self.configuration.get(
|
||||
'chap_password_len', 12)
|
||||
self.block_size = (
|
||||
self.configuration.safe_get('jovian_block_size'))
|
||||
self.configuration.get('jovian_block_size', '64K'))
|
||||
self.jovian_sparse = (
|
||||
self.configuration.safe_get('san_thin_provision'))
|
||||
self.configuration.get('san_thin_provision', True))
|
||||
self.jovian_ignore_tpath = self.configuration.get(
|
||||
'jovian_ignore_tpath', None)
|
||||
self.jovian_hosts = self.configuration.safe_get(
|
||||
'san_hosts')
|
||||
self.jovian_hosts = self.configuration.get(
|
||||
'san_hosts', [])
|
||||
|
||||
self.ra = rest.JovianRESTAPI(self.configuration)
|
||||
|
||||
self.check_for_setup_error()
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Verify that the pool exists."""
|
||||
"""Check for setup error."""
|
||||
if len(self.jovian_hosts) == 0:
|
||||
msg = _("No hosts provided in configuration")
|
||||
raise exception.VolumeDriverException(msg)
|
||||
@ -110,6 +117,12 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
msg = (_("Unable to identify pool %s") % self._pool)
|
||||
raise exception.VolumeDriverException(msg)
|
||||
|
||||
valid_bsize = ['32K', '64K', '128K', '256K', '512K', '1M']
|
||||
if self.block_size not in valid_bsize:
|
||||
raise exception.InvalidConfigurationValue(
|
||||
value=self.block_size,
|
||||
option='jovian_block_size')
|
||||
|
||||
def _get_target_name(self, volume_name):
|
||||
"""Return iSCSI target name to access volume."""
|
||||
return '%s%s' % (self.jovian_target_prefix, volume_name)
|
||||
@ -290,14 +303,14 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
jcom.origin_snapshot(vol['origin']))
|
||||
else:
|
||||
try:
|
||||
self.ra.delete_lun(vname)
|
||||
self.ra.delete_lun(vname, force_umount=True)
|
||||
except jexc.JDSSRESTException as err:
|
||||
LOG.debug(
|
||||
"Unable to delete physical volume %(volume)s "
|
||||
"with error %(err)s.", {
|
||||
"volume": vname,
|
||||
"err": err})
|
||||
raise exception.SnapshotIsBusy(err)
|
||||
raise exception.VolumeIsBusy(err)
|
||||
|
||||
def _delete_back_recursively(self, opvname, opsname):
|
||||
"""Deletes snapshot by removing its oldest removable parent
|
||||
@ -391,6 +404,46 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
raise exception.VolumeBackendAPIException(
|
||||
(_('Failed to extend volume %s.'), volume.id))
|
||||
|
||||
def revert_to_snapshot(self, context, volume, snapshot):
|
||||
"""Revert volume to snapshot.
|
||||
|
||||
Note: the revert process should not change the volume's
|
||||
current size, that means if the driver shrank
|
||||
the volume during the process, it should extend the
|
||||
volume internally.
|
||||
"""
|
||||
vname = jcom.vname(volume.id)
|
||||
sname = jcom.sname(snapshot.id)
|
||||
LOG.debug('reverting %(vname)s to %(sname)s', {
|
||||
"vname": vname,
|
||||
"sname": sname})
|
||||
|
||||
vsize = None
|
||||
try:
|
||||
vsize = self.ra.get_lun(vname).get('volsize')
|
||||
except jexc.JDSSResourceNotFoundException:
|
||||
raise exception.VolumeNotFound(volume_id=volume.id)
|
||||
except jexc.JDSSException as err:
|
||||
raise exception.VolumeBackendAPIException(err)
|
||||
|
||||
if vsize is None:
|
||||
raise exception.VolumeDriverException(
|
||||
_("unable to identify volume size"))
|
||||
|
||||
try:
|
||||
self.ra.rollback_volume_to_snapshot(vname, sname)
|
||||
except jexc.JDSSException as err:
|
||||
raise exception.VolumeBackendAPIException(err.message)
|
||||
|
||||
try:
|
||||
rvsize = self.ra.get_lun(vname).get('volsize')
|
||||
if rvsize != vsize:
|
||||
self.ra.extend_lun(vname, vsize)
|
||||
except jexc.JDSSResourceNotFoundException:
|
||||
raise exception.VolumeNotFound(volume_id=volume.id)
|
||||
except jexc.JDSSException as err:
|
||||
raise exception.VolumeBackendAPIException(err)
|
||||
|
||||
def _clone_object(self, oname, coname):
|
||||
"""Creates a clone of specified object
|
||||
|
||||
@ -430,7 +483,7 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
coname,
|
||||
oname,
|
||||
sparse=self.jovian_sparse)
|
||||
except jexc.JDSSVolumeExistsException:
|
||||
except jexc.JDSSResourceExistsException:
|
||||
raise exception.Duplicate()
|
||||
except jexc.JDSSException as err:
|
||||
try:
|
||||
@ -671,7 +724,7 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
free_capacity = math.floor(int(pool_stats["available"]) / o_units.Gi)
|
||||
|
||||
reserved_percentage = (
|
||||
self.configuration.safe_get('reserved_percentage'))
|
||||
self.configuration.get('reserved_percentage', 0))
|
||||
|
||||
if total_capacity is None:
|
||||
total_capacity = 'unknown'
|
||||
@ -784,7 +837,7 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
||||
auth = volume.provider_auth
|
||||
|
||||
if not auth:
|
||||
msg = _("Volume {} is missing provider_auth") % volume.id
|
||||
msg = _("Volume %s is missing provider_auth") % volume.id
|
||||
raise exception.VolumeDriverException(msg)
|
||||
|
||||
(__, auth_username, auth_secret) = auth.split()
|
||||
|
@ -80,3 +80,9 @@ class JDSSSnapshotIsBusyException(JDSSResourceIsBusyException):
|
||||
"""Snapshot have dependent clones"""
|
||||
|
||||
message = _("JDSS snapshot %(snapshot)s is busy.")
|
||||
|
||||
|
||||
class JDSSOSException(JDSSException):
|
||||
"""Storage internal system error"""
|
||||
|
||||
message = _("JDSS internal system error %(message)s.")
|
||||
|
@ -20,7 +20,6 @@ import re
|
||||
from oslo_log import log as logging
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.volume.drivers.open_e.jovian_common import exception as jexc
|
||||
from cinder.volume.drivers.open_e.jovian_common import rest_proxy
|
||||
|
||||
@ -32,9 +31,7 @@ class JovianRESTAPI(object):
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
self.target_p = config.get('iscsi_target_prefix',
|
||||
'iqn.2020-04.com.open-e.cinder:')
|
||||
self.pool = config.safe_get('jovian_pool')
|
||||
self.pool = config.get('jovian_pool', 'Pool-0')
|
||||
self.rproxy = rest_proxy.JovianRESTProxy(config)
|
||||
|
||||
self.resource_dne_msg = (
|
||||
@ -48,7 +45,7 @@ class JovianRESTAPI(object):
|
||||
code = resp.get('code', 'Unknown')
|
||||
msg = resp.get('message', 'Unknown')
|
||||
|
||||
reason = ("Request to {url} failed with code:%{code} "
|
||||
reason = ("Request to {url} failed with code: {code} "
|
||||
"of type:{eclass} reason:{message}")
|
||||
reason = reason.format(eclass=eclass,
|
||||
code=code,
|
||||
@ -638,12 +635,12 @@ class JovianRESTAPI(object):
|
||||
|
||||
if resp["code"] == 500:
|
||||
if resp["error"]:
|
||||
if resp["error"]["errno"] == 1:
|
||||
raise jexc.JDSSVolumeNotFoundException(
|
||||
volume=volume_name)
|
||||
if resp["error"]["errno"] == 5:
|
||||
raise jexc.JDSSSnapshotExistsException(
|
||||
snapshot=snapshot_name)
|
||||
if resp["error"]["errno"] == 1:
|
||||
raise jexc.JDSSVolumeNotFoundException(
|
||||
volume=volume_name)
|
||||
|
||||
self._general_error(req, resp)
|
||||
|
||||
@ -682,34 +679,42 @@ class JovianRESTAPI(object):
|
||||
if resp["error"]["errno"] == 100:
|
||||
raise jexc.JDSSVolumeExistsException(
|
||||
volume=volume_name)
|
||||
args = {"vol": volume_name, "e": resp['error']['message']}
|
||||
msg = _('Failed to create volume %(vol)s, err: %(e)s') % args
|
||||
raise jexc.JDSSRESTException(msg)
|
||||
if resp["error"]["errno"] == 1:
|
||||
raise jexc.JDSSResourceNotFoundException(
|
||||
res="{vol}@{snap}".format(vol=original_vol_name,
|
||||
snap=snapshot_name))
|
||||
|
||||
raise jexc.JDSSRESTException('unable to create volume')
|
||||
self._general_error(req, resp)
|
||||
|
||||
def is_snapshot(self, volume_name, snapshot_name):
|
||||
"""is_snapshots.
|
||||
def rollback_volume_to_snapshot(self, volume_name, snapshot_name):
|
||||
"""Rollback volume to its snapshot
|
||||
|
||||
GET
|
||||
/volumes/<string:volumename>/snapshots/<string:snapshotname>/clones
|
||||
|
||||
:param volume_name: that snapshot belongs to
|
||||
:return: bool
|
||||
POST /volumes/<volume_name>/snapshots/<snapshot_name>/rollback
|
||||
:param volume_name: volume that is going to be restored
|
||||
:param snapshot_name: snapshot of a volume above
|
||||
:return:
|
||||
"""
|
||||
req = '/volumes/' + volume_name + '/snapshots/' + snapshot_name + \
|
||||
'/clones'
|
||||
req = ('/volumes/{vol}/snapshots/'
|
||||
'{snap}/rollback').format(vol=volume_name,
|
||||
snap=snapshot_name)
|
||||
|
||||
LOG.debug("check if snapshot %(snap)s of volume %(vol)s exists",
|
||||
{'snap': snapshot_name,
|
||||
'vol': volume_name})
|
||||
LOG.debug("rollback volume %(vol)s to snapshot %(snap)s",
|
||||
{'vol': volume_name,
|
||||
'snap': snapshot_name})
|
||||
|
||||
resp = self.rproxy.pool_request('GET', req)
|
||||
resp = self.rproxy.pool_request('POST', req)
|
||||
|
||||
if not resp["error"] and resp["code"] == 200:
|
||||
return True
|
||||
return
|
||||
|
||||
return False
|
||||
if resp["code"] == 500:
|
||||
if resp["error"]:
|
||||
if resp["error"]["errno"] == 1:
|
||||
raise jexc.JDSSResourceNotFoundException(
|
||||
res="{vol}@{snap}".format(vol=volume_name,
|
||||
snap=snapshot_name))
|
||||
|
||||
self._general_error(req, resp)
|
||||
|
||||
def delete_snapshot(self,
|
||||
volume_name,
|
||||
@ -733,8 +738,6 @@ class JovianRESTAPI(object):
|
||||
umount (defualt false).
|
||||
:return:
|
||||
"""
|
||||
if not self.is_snapshot(volume_name, snapshot_name):
|
||||
return
|
||||
|
||||
req = '/volumes/' + volume_name + '/snapshots/' + snapshot_name
|
||||
|
||||
@ -767,11 +770,7 @@ class JovianRESTAPI(object):
|
||||
if resp["error"]["errno"] == 1000:
|
||||
raise jexc.JDSSSnapshotIsBusyException(
|
||||
snapshot=snapshot_name)
|
||||
msg = 'Failed to delete snapshot {}, err: {}'.format(
|
||||
snapshot_name, resp['error']['message'])
|
||||
raise jexc.JDSSRESTException(msg)
|
||||
msg = 'Failed to delete snapshot {}'.format(snapshot_name)
|
||||
raise jexc.JDSSRESTException(msg)
|
||||
self._general_error(req, resp)
|
||||
|
||||
def get_snapshots(self, volume_name):
|
||||
"""get_snapshots.
|
||||
@ -818,7 +817,8 @@ class JovianRESTAPI(object):
|
||||
if 'message' in resp['error']:
|
||||
if self.resource_dne_msg.match(resp['error']['message']):
|
||||
raise jexc.JDSSResourceNotFoundException(volume_name)
|
||||
raise jexc.JDSSRESTException('unable to get snapshots')
|
||||
|
||||
self._general_error(req, resp)
|
||||
|
||||
def get_pool_stats(self):
|
||||
"""get_pool_stats.
|
||||
@ -890,4 +890,4 @@ class JovianRESTAPI(object):
|
||||
if not resp["error"] and resp["code"] == 200:
|
||||
return resp["data"]
|
||||
|
||||
raise jexc.JDSSRESTException('Unable to get pool info')
|
||||
self._general_error(req, resp)
|
||||
|
@ -16,7 +16,6 @@
|
||||
"""Network connection handling class for JovianDSS driver."""
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import netutils as o_netutils
|
||||
@ -25,6 +24,7 @@ import urllib3
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.utils import retry
|
||||
from cinder.volume.drivers.open_e.jovian_common import exception as jexc
|
||||
|
||||
|
||||
@ -35,17 +35,15 @@ class JovianRESTProxy(object):
|
||||
"""Jovian REST API proxy."""
|
||||
|
||||
def __init__(self, config):
|
||||
""":param config: config is like dict."""
|
||||
""":param config: list of config values."""
|
||||
|
||||
self.proto = 'http'
|
||||
if config.get('driver_use_ssl', True):
|
||||
self.proto = 'https'
|
||||
|
||||
self.hosts = config.safe_get('san_hosts')
|
||||
self.hosts = config.get('san_hosts', [])
|
||||
self.port = str(config.get('san_api_port', 82))
|
||||
|
||||
self.active_host = 0
|
||||
|
||||
for host in self.hosts:
|
||||
if o_netutils.is_valid_ip(host) is False:
|
||||
err_msg = ('Invalid value of jovian_host property: '
|
||||
@ -55,36 +53,50 @@ class JovianRESTProxy(object):
|
||||
LOG.debug(err_msg)
|
||||
raise exception.InvalidConfigurationValue(err_msg)
|
||||
|
||||
self.api_path = "/api/v3"
|
||||
self.active_host = 0
|
||||
|
||||
self.delay = config.get('jovian_recovery_delay', 40)
|
||||
|
||||
self.pool = config.safe_get('jovian_pool')
|
||||
self.pool = config.get('jovian_pool', 'Pool-0')
|
||||
|
||||
self.user = config.get('san_login', 'admin')
|
||||
self.password = config.get('san_password', 'admin')
|
||||
self.auth = requests.auth.HTTPBasicAuth(self.user, self.password)
|
||||
self.verify = False
|
||||
self.retry_n = config.get('jovian_rest_send_repeats', 3)
|
||||
self.header = {'connection': 'keep-alive',
|
||||
'Content-Type': 'application/json',
|
||||
'authorization': 'Basic '}
|
||||
self.verify = config.get('driver_ssl_cert_verify', True)
|
||||
self.cert = config.get('driver_ssl_cert_path')
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
def _get_pool_url(self, host):
|
||||
url = ('%(proto)s://%(host)s:%(port)s/api/v3/pools/%(pool)s' % {
|
||||
'proto': self.proto,
|
||||
'host': host,
|
||||
'port': self.port,
|
||||
'pool': self.pool})
|
||||
return url
|
||||
self.session = self._get_session()
|
||||
|
||||
def _get_session(self):
|
||||
"""Create and init new session object"""
|
||||
|
||||
session = requests.Session()
|
||||
session.auth = (self.user, self.password)
|
||||
session.headers.update({'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Basic'})
|
||||
session.hooks['response'] = [JovianRESTProxy._handle_500]
|
||||
session.verify = self.verify
|
||||
if self.verify and self.cert:
|
||||
session.verify = self.cert
|
||||
return session
|
||||
|
||||
def _get_base_url(self):
|
||||
"""Get url prefix with active host"""
|
||||
|
||||
def _get_url(self, host):
|
||||
url = ('%(proto)s://%(host)s:%(port)s/api/v3' % {
|
||||
'proto': self.proto,
|
||||
'host': host,
|
||||
'host': self.hosts[self.active_host],
|
||||
'port': self.port})
|
||||
|
||||
return url
|
||||
|
||||
def _next_host(self):
|
||||
"""Set next host as active"""
|
||||
|
||||
self.active_host = (self.active_host + 1) % len(self.hosts)
|
||||
|
||||
def request(self, request_method, req, json_data=None):
|
||||
"""Send request to the specific url.
|
||||
|
||||
@ -92,39 +104,31 @@ class JovianRESTProxy(object):
|
||||
:param url: where to send
|
||||
:param json_data: data
|
||||
"""
|
||||
for j in range(self.retry_n):
|
||||
out = None
|
||||
for i in range(len(self.hosts)):
|
||||
host = self.hosts[self.active_host]
|
||||
url = self._get_url(host) + req
|
||||
|
||||
LOG.debug(
|
||||
"sending request of type %(type)s to %(url)s "
|
||||
"attempt: %(num)s.",
|
||||
{'type': request_method,
|
||||
'url': url,
|
||||
'num': j})
|
||||
|
||||
if json_data is not None:
|
||||
LOG.debug(
|
||||
"sending data: %s.", json_data)
|
||||
try:
|
||||
addr = "{base}{req}".format(base=self._get_base_url(),
|
||||
req=req)
|
||||
LOG.debug("Sending %(t)s to %(addr)s",
|
||||
{'t': request_method, 'addr': addr})
|
||||
r = None
|
||||
if json_data:
|
||||
r = requests.Request(request_method,
|
||||
addr,
|
||||
data=json.dumps(json_data))
|
||||
else:
|
||||
r = requests.Request(request_method, addr)
|
||||
|
||||
ret = self._request_routine(url, request_method, json_data)
|
||||
if len(ret) == 0:
|
||||
self.active_host = ((self.active_host + 1)
|
||||
% len(self.hosts))
|
||||
pr = self.session.prepare_request(r)
|
||||
out = self._send(pr)
|
||||
except requests.exceptions.ConnectionError:
|
||||
self._next_host()
|
||||
continue
|
||||
return ret
|
||||
break
|
||||
|
||||
except requests.ConnectionError as err:
|
||||
LOG.debug("Connection error %s", err)
|
||||
self.active_host = (self.active_host + 1) % len(self.hosts)
|
||||
continue
|
||||
time.sleep(self.delay)
|
||||
|
||||
msg = (_('%(times)s faild in a row') % {'times': j})
|
||||
|
||||
raise jexc.JDSSRESTProxyException(host=url, reason=msg)
|
||||
LOG.debug("Geting %(data)s from %(t)s to %(addr)s",
|
||||
{'data': out, 't': request_method, 'addr': addr})
|
||||
return out
|
||||
|
||||
def pool_request(self, request_method, req, json_data=None):
|
||||
"""Send request to the specific url.
|
||||
@ -133,94 +137,65 @@ class JovianRESTProxy(object):
|
||||
:param url: where to send
|
||||
:param json_data: data
|
||||
"""
|
||||
url = ""
|
||||
for j in range(self.retry_n):
|
||||
for i in range(len(self.hosts)):
|
||||
host = self.hosts[self.active_host]
|
||||
url = self._get_pool_url(host) + req
|
||||
req = "/pools/{pool}{req}".format(pool=self.pool, req=req)
|
||||
addr = "{base}{req}".format(base=self._get_base_url(), req=req)
|
||||
LOG.debug("Sending pool request %(t)s to %(addr)s",
|
||||
{'t': request_method, 'addr': addr})
|
||||
return self.request(request_method, req, json_data=json_data)
|
||||
|
||||
LOG.debug(
|
||||
"sending pool request of type %(type)s to %(url)s "
|
||||
"attempt: %(num)s.",
|
||||
{'type': request_method,
|
||||
'url': url,
|
||||
'num': j})
|
||||
@retry((requests.exceptions.ConnectionError,
|
||||
jexc.JDSSOSException),
|
||||
interval=2,
|
||||
backoff_rate=2,
|
||||
retries=7)
|
||||
def _send(self, pr):
|
||||
"""Send prepared request
|
||||
|
||||
if json_data is not None:
|
||||
LOG.debug(
|
||||
"JovianDSS: Sending data: %s.", str(json_data))
|
||||
try:
|
||||
|
||||
ret = self._request_routine(url, request_method, json_data)
|
||||
if len(ret) == 0:
|
||||
self.active_host = ((self.active_host + 1)
|
||||
% len(self.hosts))
|
||||
continue
|
||||
return ret
|
||||
|
||||
except requests.ConnectionError as err:
|
||||
LOG.debug("Connection error %s", err)
|
||||
self.active_host = (self.active_host + 1) % len(self.hosts)
|
||||
continue
|
||||
time.sleep(int(self.delay))
|
||||
|
||||
msg = (_('%(times)s faild in a row') % {'times': j})
|
||||
|
||||
raise jexc.JDSSRESTProxyException(host=url, reason=msg)
|
||||
|
||||
def _request_routine(self, url, request_method, json_data=None):
|
||||
"""Make an HTTPS request and return the results."""
|
||||
|
||||
ret = None
|
||||
for i in range(3):
|
||||
:param pr: prepared request
|
||||
"""
|
||||
ret = dict()
|
||||
try:
|
||||
response_obj = requests.request(request_method,
|
||||
auth=self.auth,
|
||||
url=url,
|
||||
headers=self.header,
|
||||
data=json.dumps(json_data),
|
||||
verify=self.verify)
|
||||
|
||||
LOG.debug('response code: %s', response_obj.status_code)
|
||||
LOG.debug('response data: %s', response_obj.text)
|
||||
response_obj = self.session.send(pr)
|
||||
|
||||
ret['code'] = response_obj.status_code
|
||||
|
||||
if '{' in response_obj.text and '}' in response_obj.text:
|
||||
if "error" in response_obj.text:
|
||||
ret["error"] = json.loads(response_obj.text)["error"]
|
||||
else:
|
||||
ret["error"] = None
|
||||
if "data" in response_obj.text:
|
||||
ret["data"] = json.loads(response_obj.text)["data"]
|
||||
else:
|
||||
ret["data"] = None
|
||||
|
||||
if ret["code"] == 500:
|
||||
if ret["error"] is not None:
|
||||
if (("errno" in ret["error"]) and
|
||||
("class" in ret["error"])):
|
||||
if (ret["error"]["class"] ==
|
||||
"opene.tools.scstadmin.ScstAdminError"):
|
||||
LOG.debug("ScstAdminError %(code)d %(msg)s", {
|
||||
"code": ret["error"]["errno"],
|
||||
"msg": ret["error"]["message"]})
|
||||
continue
|
||||
if (ret["error"]["class"] ==
|
||||
"exceptions.OSError"):
|
||||
LOG.debug("OSError %(code)d %(msg)s", {
|
||||
"code": ret["error"]["errno"],
|
||||
"msg": ret["error"]["message"]})
|
||||
continue
|
||||
break
|
||||
|
||||
except requests.HTTPError as err:
|
||||
LOG.debug("HTTP parsing error %s", err)
|
||||
self.active_host = (self.active_host + 1) % len(self.hosts)
|
||||
try:
|
||||
data = json.loads(response_obj.text)
|
||||
ret["error"] = data.get("error")
|
||||
ret["data"] = data.get("data")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def _handle_500(resp, *args, **kwargs):
|
||||
"""Handle OS error on a storage side"""
|
||||
|
||||
error = None
|
||||
if resp.status_code == 500:
|
||||
try:
|
||||
data = json.loads(resp.text)
|
||||
error = data.get("error")
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
if error:
|
||||
if "class" in error:
|
||||
if error["class"] == "opene.tools.scstadmin.ScstAdminError":
|
||||
LOG.debug("ScstAdminError %(code)d %(msg)s",
|
||||
{'code': error["errno"],
|
||||
'msg': error["message"]})
|
||||
raise jexc.JDSSOSException(_(error["message"]))
|
||||
|
||||
if error["class"] == "exceptions.OSError":
|
||||
LOG.debug("OSError %(code)d %(msg)s",
|
||||
{'code': error["errno"],
|
||||
'msg': error["message"]})
|
||||
raise jexc.JDSSOSException(_(error["message"]))
|
||||
|
||||
def get_active_host(self):
|
||||
"""Return address of currently used host."""
|
||||
return self.hosts[self.active_host]
|
||||
|
@ -19,9 +19,6 @@ jdss_connection_opts = [
|
||||
cfg.ListOpt('san_hosts',
|
||||
default='',
|
||||
help='IP address of Open-E JovianDSS SA'),
|
||||
cfg.IntOpt('jovian_rest_send_repeats',
|
||||
default=3,
|
||||
help='Number of retries to send REST request.'),
|
||||
cfg.IntOpt('jovian_recovery_delay',
|
||||
default=60,
|
||||
help='Time before HA cluster failure.'),
|
||||
@ -41,8 +38,8 @@ jdss_iscsi_opts = [
|
||||
|
||||
jdss_volume_opts = [
|
||||
cfg.StrOpt('jovian_block_size',
|
||||
default='128K',
|
||||
help='Block size for volumes (512 - 128K)'),
|
||||
default='64K',
|
||||
help='Block size can be: 32K, 64K, 128K, 256K, 512K, 1M'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
@ -38,10 +38,11 @@ Provide settings to JovianDSS driver by adding 'jdss-0' description:
|
||||
backend_name = jdss-0
|
||||
chap_password_len = 14
|
||||
driver_use_ssl = True
|
||||
driver_ssl_cert_verify = True
|
||||
driver_ssl_cert_path = /etc/cinder/jdss.crt
|
||||
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
||||
jovian_pool = Pool-0
|
||||
jovian_block_size = 128K
|
||||
jovian_rest_send_repeats = 4
|
||||
san_api_port = 82
|
||||
target_port = 3260
|
||||
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
||||
@ -65,6 +66,12 @@ Provide settings to JovianDSS driver by adding 'jdss-0' description:
|
||||
* - ``driver_use_ssl``
|
||||
- True
|
||||
- Use SSL to send requests to JovianDSS[1]
|
||||
* - ``driver_ssl_cert_verify``
|
||||
- True
|
||||
- Verify authenticity of JovianDSS[1] certificate
|
||||
* - ``driver_ssl_cert_path``
|
||||
- None
|
||||
- Path to the JovianDSS[1] certificate for verification
|
||||
* - ``iscsi_target_prefix``
|
||||
- iqn.2016-04.com.open-e:01:cinder-
|
||||
- Prefix that will be used to form target name for volume
|
||||
@ -74,9 +81,6 @@ Provide settings to JovianDSS driver by adding 'jdss-0' description:
|
||||
* - ``jovian_block_size``
|
||||
- 128K
|
||||
- Block size for newly created volumes
|
||||
* - ``jovian_rest_send_repeats``
|
||||
- 3
|
||||
- Number of times that driver will try to send REST request
|
||||
* - ``san_api_port``
|
||||
- 82
|
||||
- Rest port according to the settings in [1]
|
||||
@ -94,7 +98,7 @@ Provide settings to JovianDSS driver by adding 'jdss-0' description:
|
||||
- Must be set according to the settings in [1]
|
||||
* - ``san_password``
|
||||
- admin
|
||||
- Jovian password [1], **should be changed** for security purpouses
|
||||
- Jovian password [1], **should be changed** for security purposes
|
||||
* - ``san_thin_provision``
|
||||
- False
|
||||
- Using thin provisioning for new volumes
|
||||
@ -126,10 +130,10 @@ For instance if you want to add ``Pool-1`` located on the same host as
|
||||
backend_name = jdss-0
|
||||
chap_password_len = 14
|
||||
driver_use_ssl = True
|
||||
driver_ssl_cert_verify = False
|
||||
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
||||
jovian_pool = Pool-0
|
||||
jovian_block_size = 128K
|
||||
jovian_rest_send_repeats = 4
|
||||
san_api_port = 82
|
||||
target_port = 3260
|
||||
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
||||
@ -142,10 +146,10 @@ For instance if you want to add ``Pool-1`` located on the same host as
|
||||
backend_name = jdss-1
|
||||
chap_password_len = 14
|
||||
driver_use_ssl = True
|
||||
driver_ssl_cert_verify = False
|
||||
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
||||
jovian_pool = Pool-1
|
||||
jovian_block_size = 128K
|
||||
jovian_rest_send_repeats = 4
|
||||
san_api_port = 82
|
||||
target_port = 3260
|
||||
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
||||
@ -175,10 +179,10 @@ and 192.168.31.100 the configuration file will look like:
|
||||
backend_name = jdss-2
|
||||
chap_password_len = 14
|
||||
driver_use_ssl = True
|
||||
driver_ssl_cert_verify = False
|
||||
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
||||
jovian_pool = Pool-0
|
||||
jovian_block_size = 128K
|
||||
jovian_rest_send_repeats = 4
|
||||
san_api_port = 82
|
||||
target_port = 3260
|
||||
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
||||
|
@ -150,6 +150,9 @@ title=Generic NFS Reference Driver (NFS)
|
||||
[driver.nimble]
|
||||
title=Nimble Storage Driver (iSCSI, FC)
|
||||
|
||||
[driver.opene_joviandss]
|
||||
title=Open-E JovianDSS Storage Driver (iSCSI)
|
||||
|
||||
[driver.prophetstor]
|
||||
title=ProphetStor Flexvisor Driver (iSCSI, NFS)
|
||||
|
||||
@ -259,6 +262,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=complete
|
||||
driver.nfs=complete
|
||||
driver.nimble=complete
|
||||
driver.opene_joviandss=complete
|
||||
driver.prophetstor=missing
|
||||
driver.pure=complete
|
||||
driver.qnap=complete
|
||||
@ -328,6 +332,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=complete
|
||||
driver.nfs=missing
|
||||
driver.nimble=complete
|
||||
driver.opene_joviandss=missing
|
||||
driver.prophetstor=complete
|
||||
driver.pure=complete
|
||||
driver.qnap=complete
|
||||
@ -397,6 +402,7 @@ driver.netapp_solidfire=missing
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
driver.nimble=missing
|
||||
driver.opene_joviandss=missing
|
||||
driver.prophetstor=missing
|
||||
driver.pure=missing
|
||||
driver.qnap=missing
|
||||
@ -469,6 +475,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
driver.nimble=missing
|
||||
driver.opene_joviandss=missing
|
||||
driver.prophetstor=missing
|
||||
driver.pure=complete
|
||||
driver.qnap=missing
|
||||
@ -540,6 +547,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
driver.nimble=missing
|
||||
driver.opene_joviandss=missing
|
||||
driver.prophetstor=missing
|
||||
driver.pure=complete
|
||||
driver.qnap=missing
|
||||
@ -612,6 +620,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
driver.nimble=complete
|
||||
driver.opene_joviandss=missing
|
||||
driver.prophetstor=complete
|
||||
driver.pure=complete
|
||||
driver.qnap=missing
|
||||
@ -683,6 +692,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=complete
|
||||
driver.nimble=complete
|
||||
driver.opene_joviandss=complete
|
||||
driver.prophetstor=missing
|
||||
driver.pure=complete
|
||||
driver.qnap=missing
|
||||
@ -755,6 +765,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
driver.nimble=missing
|
||||
driver.opene_joviandss=missing
|
||||
driver.prophetstor=missing
|
||||
driver.pure=missing
|
||||
driver.qnap=missing
|
||||
@ -827,6 +838,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
driver.nimble=complete
|
||||
driver.opene_joviandss=missing
|
||||
driver.prophetstor=missing
|
||||
driver.pure=complete
|
||||
driver.qnap=missing
|
||||
@ -896,6 +908,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
driver.nimble=complete
|
||||
driver.opene_joviandss=complete
|
||||
driver.prophetstor=missing
|
||||
driver.pure=complete
|
||||
driver.qnap=missing
|
||||
@ -969,6 +982,7 @@ driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
driver.nimble=missing
|
||||
driver.opene_joviandss=missing
|
||||
driver.prophetstor=missing
|
||||
driver.pure=complete
|
||||
driver.qnap=missing
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added support of authenticity verification through self-signed certificates
|
||||
for JovianDSS data storage.
|
||||
Added support of revert to snapshot functionality.
|
||||
Expands unit-test coverage for JovianDSS driver.
|
Loading…
Reference in New Issue
Block a user