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 exception as jexc
|
||||||
from cinder.volume.drivers.open_e.jovian_common import jdss_common as jcom
|
from cinder.volume.drivers.open_e.jovian_common import jdss_common as jcom
|
||||||
|
|
||||||
|
|
||||||
UUID_1 = '12345678-1234-1234-1234-000000000001'
|
UUID_1 = '12345678-1234-1234-1234-000000000001'
|
||||||
UUID_2 = '12345678-1234-1234-1234-000000000002'
|
UUID_2 = '12345678-1234-1234-1234-000000000002'
|
||||||
UUID_3 = '12345678-1234-1234-1234-000000000003'
|
UUID_3 = '12345678-1234-1234-1234-000000000003'
|
||||||
@ -36,7 +35,7 @@ UUID_4 = '12345678-1234-1234-1234-000000000004'
|
|||||||
CONFIG_OK = {
|
CONFIG_OK = {
|
||||||
'san_hosts': ['192.168.0.2'],
|
'san_hosts': ['192.168.0.2'],
|
||||||
'san_api_port': 82,
|
'san_api_port': 82,
|
||||||
'driver_use_ssl': 'https',
|
'driver_use_ssl': 'true',
|
||||||
'jovian_rest_send_repeats': 3,
|
'jovian_rest_send_repeats': 3,
|
||||||
'jovian_recovery_delay': 60,
|
'jovian_recovery_delay': 60,
|
||||||
'jovian_user': 'admin',
|
'jovian_user': 'admin',
|
||||||
@ -53,7 +52,7 @@ CONFIG_OK = {
|
|||||||
CONFIG_BLOCK_SIZE = {
|
CONFIG_BLOCK_SIZE = {
|
||||||
'san_hosts': ['192.168.0.2'],
|
'san_hosts': ['192.168.0.2'],
|
||||||
'san_api_port': 82,
|
'san_api_port': 82,
|
||||||
'driver_use_ssl': 'https',
|
'driver_use_ssl': 'true',
|
||||||
'jovian_rest_send_repeats': 3,
|
'jovian_rest_send_repeats': 3,
|
||||||
'jovian_recovery_delay': 60,
|
'jovian_recovery_delay': 60,
|
||||||
'jovian_user': 'admin',
|
'jovian_user': 'admin',
|
||||||
@ -67,10 +66,27 @@ CONFIG_BLOCK_SIZE = {
|
|||||||
'jovian_block_size': '64K'
|
'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 = {
|
CONFIG_BACKEND_NAME = {
|
||||||
'san_hosts': ['192.168.0.2'],
|
'san_hosts': ['192.168.0.2'],
|
||||||
'san_api_port': 82,
|
'san_api_port': 82,
|
||||||
'driver_use_ssl': 'https',
|
'driver_use_ssl': 'true',
|
||||||
'jovian_rest_send_repeats': 3,
|
'jovian_rest_send_repeats': 3,
|
||||||
'jovian_recovery_delay': 60,
|
'jovian_recovery_delay': 60,
|
||||||
'jovian_user': 'admin',
|
'jovian_user': 'admin',
|
||||||
@ -89,7 +105,7 @@ CONFIG_BACKEND_NAME = {
|
|||||||
CONFIG_MULTI_HOST = {
|
CONFIG_MULTI_HOST = {
|
||||||
'san_hosts': ['192.168.0.2', '192.168.0.3'],
|
'san_hosts': ['192.168.0.2', '192.168.0.3'],
|
||||||
'san_api_port': 82,
|
'san_api_port': 82,
|
||||||
'driver_use_ssl': 'https',
|
'driver_use_ssl': 'true',
|
||||||
'jovian_rest_send_repeats': 3,
|
'jovian_rest_send_repeats': 3,
|
||||||
'jovian_recovery_delay': 60,
|
'jovian_recovery_delay': 60,
|
||||||
'jovian_user': 'admin',
|
'jovian_user': 'admin',
|
||||||
@ -168,10 +184,6 @@ def get_jdss_exceptions():
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def fake_safe_get(value):
|
|
||||||
return CONFIG_OK[value]
|
|
||||||
|
|
||||||
|
|
||||||
class TestOpenEJovianDSSDriver(test.TestCase):
|
class TestOpenEJovianDSSDriver(test.TestCase):
|
||||||
|
|
||||||
def get_driver(self, config):
|
def get_driver(self, config):
|
||||||
@ -179,11 +191,16 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
|
|
||||||
cfg = mock.Mock()
|
cfg = mock.Mock()
|
||||||
cfg.append_config_values.return_value = None
|
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 = iscsi.JovianISCSIDriver()
|
||||||
|
|
||||||
jdssd.configuration = cfg
|
jdssd.configuration = cfg
|
||||||
jdssd.do_setup(ctx)
|
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()
|
jdssd.ra = mock.Mock()
|
||||||
return jdssd, ctx
|
return jdssd, ctx
|
||||||
|
|
||||||
@ -195,6 +212,39 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
for p in patches:
|
for p in patches:
|
||||||
p.stop()
|
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):
|
def test_get_provider_location(self):
|
||||||
jdssd, ctx = self.get_driver(CONFIG_OK)
|
jdssd, ctx = self.get_driver(CONFIG_OK)
|
||||||
host = CONFIG_OK["san_hosts"][0]
|
host = CONFIG_OK["san_hosts"][0]
|
||||||
@ -351,17 +401,18 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
SNAPSHOTS_EMPTY,
|
SNAPSHOTS_EMPTY,
|
||||||
SNAPSHOTS_EMPTY]
|
SNAPSHOTS_EMPTY]
|
||||||
|
|
||||||
fake_gc = mock.Mock()
|
patches = [mock.patch.object(jdssd, "_gc_delete"),
|
||||||
fake_hide_object = mock.Mock()
|
mock.patch.object(jdssd, "_hide_object")]
|
||||||
gc = mock.patch.object(jdssd, "_gc_delete", new=fake_gc)
|
|
||||||
gc.start()
|
self.start_patches(patches)
|
||||||
hide = mock.patch.object(jdssd, "_hide_object", new=fake_hide_object)
|
|
||||||
hide.start()
|
|
||||||
jdssd._cascade_volume_delete(o_vname, o_snaps)
|
jdssd._cascade_volume_delete(o_vname, o_snaps)
|
||||||
|
|
||||||
jdssd._hide_object.assert_called_once_with(o_vname)
|
jdssd._hide_object.assert_called_once_with(o_vname)
|
||||||
hide.stop()
|
|
||||||
jdssd._gc_delete.assert_not_called()
|
jdssd._gc_delete.assert_not_called()
|
||||||
gc.stop()
|
|
||||||
|
self.stop_patches(patches)
|
||||||
|
|
||||||
delete_snapshot_expected = [
|
delete_snapshot_expected = [
|
||||||
mock.call(o_vname,
|
mock.call(o_vname,
|
||||||
SNAPSHOTS_CASCADE_2[0]["name"],
|
SNAPSHOTS_CASCADE_2[0]["name"],
|
||||||
@ -399,17 +450,17 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
mock.call(SNAPSHOTS_CASCADE_1[1]["name"]),
|
mock.call(SNAPSHOTS_CASCADE_1[1]["name"]),
|
||||||
mock.call(o_vname)]
|
mock.call(o_vname)]
|
||||||
|
|
||||||
fake_gc = mock.Mock()
|
patches = [mock.patch.object(jdssd, "_gc_delete"),
|
||||||
fake_hide_object = mock.Mock()
|
mock.patch.object(jdssd, "_hide_object")]
|
||||||
gc = mock.patch.object(jdssd, "_gc_delete", new=fake_gc)
|
|
||||||
gc.start()
|
self.start_patches(patches)
|
||||||
hide = mock.patch.object(jdssd, "_hide_object", new=fake_hide_object)
|
|
||||||
hide.start()
|
|
||||||
jdssd._cascade_volume_delete(o_vname, o_snaps)
|
jdssd._cascade_volume_delete(o_vname, o_snaps)
|
||||||
jdssd._hide_object.assert_has_calls(hide_object_expected)
|
jdssd._hide_object.assert_has_calls(hide_object_expected)
|
||||||
hide.stop()
|
|
||||||
jdssd._gc_delete.assert_not_called()
|
jdssd._gc_delete.assert_not_called()
|
||||||
gc.stop()
|
|
||||||
|
self.stop_patches(patches)
|
||||||
|
|
||||||
jdssd.ra.get_snapshots.assert_has_calls(get_snapshots)
|
jdssd.ra.get_snapshots.assert_has_calls(get_snapshots)
|
||||||
|
|
||||||
delete_snapshot_expected = [
|
delete_snapshot_expected = [
|
||||||
@ -522,7 +573,8 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
jdssd._gc_delete(jcom.vname(UUID_1))
|
jdssd._gc_delete(jcom.vname(UUID_1))
|
||||||
|
|
||||||
jdssd._delete_back_recursively.assert_not_called()
|
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)
|
self.stop_patches(patches)
|
||||||
|
|
||||||
@ -655,6 +707,133 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
self.assertIsInstance(err, exception.VolumeBackendAPIException)
|
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):
|
def test_clone_object(self):
|
||||||
jdssd, ctx = self.get_driver(CONFIG_OK)
|
jdssd, ctx = self.get_driver(CONFIG_OK)
|
||||||
origin = jcom.vname(UUID_1)
|
origin = jcom.vname(UUID_1)
|
||||||
@ -1103,7 +1282,7 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
location_info = 'JovianISCSIDriver:192.168.0.2:Pool-0'
|
location_info = 'JovianISCSIDriver:192.168.0.2:Pool-0'
|
||||||
correct_out = {
|
correct_out = {
|
||||||
'vendor_name': 'Open-E',
|
'vendor_name': 'Open-E',
|
||||||
'driver_version': "1.0.0",
|
'driver_version': "1.0.1",
|
||||||
'storage_protocol': 'iSCSI',
|
'storage_protocol': 'iSCSI',
|
||||||
'total_capacity_gb': 100,
|
'total_capacity_gb': 100,
|
||||||
'free_capacity_gb': 50,
|
'free_capacity_gb': 50,
|
||||||
@ -1311,6 +1490,7 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
jdssd._create_target_volume.assert_called_once_with(vol)
|
jdssd._create_target_volume.assert_called_once_with(vol)
|
||||||
|
|
||||||
jdssd.ra.is_target_lun.assert_not_called()
|
jdssd.ra.is_target_lun.assert_not_called()
|
||||||
|
self.stop_patches(patches)
|
||||||
|
|
||||||
def test_remove_target_volume(self):
|
def test_remove_target_volume(self):
|
||||||
|
|
||||||
@ -1454,7 +1634,6 @@ class TestOpenEJovianDSSDriver(test.TestCase):
|
|||||||
'driver_volume_type': 'iscsi',
|
'driver_volume_type': 'iscsi',
|
||||||
'data': properties,
|
'data': properties,
|
||||||
}
|
}
|
||||||
jdssd.ra.activate_target.return_value = None
|
|
||||||
|
|
||||||
ret = jdssd.initialize_connection(vol, connector)
|
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_1 = '12345678-1234-1234-1234-000000000001'
|
||||||
UUID_2 = '12345678-1234-1234-1234-000000000002'
|
UUID_2 = '12345678-1234-1234-1234-000000000002'
|
||||||
|
UUID_3 = '12345678-1234-1234-1234-000000000003'
|
||||||
|
|
||||||
CONFIG_OK = {
|
CONFIG_OK = {
|
||||||
'san_hosts': ['192.168.0.2'],
|
'san_hosts': ['192.168.0.2'],
|
||||||
'san_api_port': 82,
|
'san_api_port': 82,
|
||||||
'driver_use_ssl': 'https',
|
'driver_use_ssl': 'true',
|
||||||
'jovian_rest_send_repeats': 3,
|
'jovian_rest_send_repeats': 3,
|
||||||
'jovian_recovery_delay': 60,
|
'jovian_recovery_delay': 60,
|
||||||
'san_login': 'admin',
|
'san_login': 'admin',
|
||||||
@ -45,10 +46,6 @@ CONFIG_OK = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def fake_safe_get(value):
|
|
||||||
return CONFIG_OK[value]
|
|
||||||
|
|
||||||
|
|
||||||
class TestOpenEJovianRESTAPI(test.TestCase):
|
class TestOpenEJovianRESTAPI(test.TestCase):
|
||||||
|
|
||||||
def get_rest(self, config):
|
def get_rest(self, config):
|
||||||
@ -57,19 +54,11 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
|||||||
cfg = mock.Mock()
|
cfg = mock.Mock()
|
||||||
cfg.append_config_values.return_value = None
|
cfg.append_config_values.return_value = None
|
||||||
cfg.safe_get = lambda val: config[val]
|
cfg.safe_get = lambda val: config[val]
|
||||||
cfg.get = lambda val, default: config[val]
|
cfg.get = lambda val, default: config.get(val, default)
|
||||||
jdssr = rest.JovianRESTAPI(cfg)
|
jdssr = rest.JovianRESTAPI(config)
|
||||||
jdssr.rproxy = mock.Mock()
|
jdssr.rproxy = mock.Mock()
|
||||||
return jdssr, ctx
|
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):
|
def test_get_active_host(self):
|
||||||
|
|
||||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||||
@ -114,7 +103,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
|||||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||||
resp = {'data': [{
|
resp = {'data': [{
|
||||||
'vscan': None,
|
'vscan': None,
|
||||||
'full_name': 'pool-0/' + UUID_1,
|
'full_name': 'Pool-0/' + UUID_1,
|
||||||
'userrefs': None,
|
'userrefs': None,
|
||||||
'primarycache': 'all',
|
'primarycache': 'all',
|
||||||
'logbias': 'latency',
|
'logbias': 'latency',
|
||||||
@ -148,7 +137,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
|||||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||||
resp = {'data': {
|
resp = {'data': {
|
||||||
'vscan': None,
|
'vscan': None,
|
||||||
'full_name': 'pool-0/' + jcom.vname(UUID_1),
|
'full_name': 'Pool-0/' + jcom.vname(UUID_1),
|
||||||
'userrefs': None,
|
'userrefs': None,
|
||||||
'primarycache': 'all',
|
'primarycache': 'all',
|
||||||
'logbias': 'latency',
|
'logbias': 'latency',
|
||||||
@ -231,7 +220,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
|||||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||||
resp = {'data': {
|
resp = {'data': {
|
||||||
"vscan": None,
|
"vscan": None,
|
||||||
"full_name": "pool-0/" + jcom.vname(UUID_1),
|
"full_name": "Pool-0/" + jcom.vname(UUID_1),
|
||||||
"userrefs": None,
|
"userrefs": None,
|
||||||
"primarycache": "all",
|
"primarycache": "all",
|
||||||
"logbias": "latency",
|
"logbias": "latency",
|
||||||
@ -268,7 +257,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
|||||||
def test_get_lun(self):
|
def test_get_lun(self):
|
||||||
jrest, ctx = self.get_rest(CONFIG_OK)
|
jrest, ctx = self.get_rest(CONFIG_OK)
|
||||||
resp = {'data': {"vscan": None,
|
resp = {'data': {"vscan": None,
|
||||||
"full_name": "pool-0/v_" + UUID_1,
|
"full_name": "Pool-0/v_" + UUID_1,
|
||||||
"userrefs": None,
|
"userrefs": None,
|
||||||
"primarycache": "all",
|
"primarycache": "all",
|
||||||
"logbias": "latency",
|
"logbias": "latency",
|
||||||
@ -995,3 +984,498 @@ class TestOpenEJovianRESTAPI(test.TestCase):
|
|||||||
self.assertRaises(jexc.JDSSException,
|
self.assertRaises(jexc.JDSSException,
|
||||||
jrest.detach_target_vol, tname, vname)
|
jrest.detach_target_vol, tname, vname)
|
||||||
jrest.rproxy.pool_request.assert_has_calls(detach_target_vol_expected)
|
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
|
.. code-block:: none
|
||||||
|
|
||||||
1.0.0 - Open-E JovianDSS driver with basic functionality
|
1.0.0 - Open-E JovianDSS driver with basic functionality
|
||||||
|
1.0.1 - Added certificate support
|
||||||
|
Added revert to snapshot support
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
# ThirdPartySystems wiki page
|
||||||
CI_WIKI_NAME = "Open-E_JovianDSS_CI"
|
CI_WIKI_NAME = "Open-E_JovianDSS_CI"
|
||||||
VERSION = "1.0.0"
|
VERSION = "1.0.1"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(JovianISCSIDriver, self).__init__(*args, **kwargs)
|
super(JovianISCSIDriver, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self._stats = None
|
self._stats = None
|
||||||
self._pool = 'Pool-0'
|
|
||||||
self.jovian_iscsi_target_portal_port = "3260"
|
self.jovian_iscsi_target_portal_port = "3260"
|
||||||
self.jovian_target_prefix = 'iqn.2020-04.com.open-e.cinder:'
|
self.jovian_target_prefix = 'iqn.2020-04.com.open-e.cinder:'
|
||||||
self.jovian_chap_pass_len = 12
|
self.jovian_chap_pass_len = 12
|
||||||
self.jovian_sparse = False
|
self.jovian_sparse = False
|
||||||
self.jovian_ignore_tpath = None
|
self.jovian_ignore_tpath = None
|
||||||
self.jovian_hosts = None
|
self.jovian_hosts = None
|
||||||
|
self._pool = 'Pool-0'
|
||||||
self.ra = None
|
self.ra = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -67,7 +69,8 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
|||||||
"""Return backend name."""
|
"""Return backend name."""
|
||||||
backend_name = None
|
backend_name = None
|
||||||
if self.configuration:
|
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:
|
if not backend_name:
|
||||||
backend_name = self.__class__.__name__
|
backend_name = self.__class__.__name__
|
||||||
return backend_name
|
return backend_name
|
||||||
@ -82,26 +85,30 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
|||||||
options.jdss_volume_opts)
|
options.jdss_volume_opts)
|
||||||
self.configuration.append_config_values(san.san_opts)
|
self.configuration.append_config_values(san.san_opts)
|
||||||
|
|
||||||
self._pool = self.configuration.safe_get('jovian_pool')
|
self._pool = self.configuration.get('jovian_pool', 'Pool-0')
|
||||||
self.jovian_iscsi_target_portal_port = self.configuration.safe_get(
|
self.jovian_iscsi_target_portal_port = self.configuration.get(
|
||||||
'target_port')
|
'target_port', 3260)
|
||||||
|
|
||||||
self.jovian_target_prefix = self.configuration.safe_get(
|
self.jovian_target_prefix = self.configuration.get(
|
||||||
'target_prefix')
|
'target_prefix',
|
||||||
self.jovian_chap_pass_len = self.configuration.safe_get(
|
'iqn.2020-04.com.open-e.cinder:')
|
||||||
'chap_password_len')
|
self.jovian_chap_pass_len = self.configuration.get(
|
||||||
|
'chap_password_len', 12)
|
||||||
self.block_size = (
|
self.block_size = (
|
||||||
self.configuration.safe_get('jovian_block_size'))
|
self.configuration.get('jovian_block_size', '64K'))
|
||||||
self.jovian_sparse = (
|
self.jovian_sparse = (
|
||||||
self.configuration.safe_get('san_thin_provision'))
|
self.configuration.get('san_thin_provision', True))
|
||||||
self.jovian_ignore_tpath = self.configuration.get(
|
self.jovian_ignore_tpath = self.configuration.get(
|
||||||
'jovian_ignore_tpath', None)
|
'jovian_ignore_tpath', None)
|
||||||
self.jovian_hosts = self.configuration.safe_get(
|
self.jovian_hosts = self.configuration.get(
|
||||||
'san_hosts')
|
'san_hosts', [])
|
||||||
|
|
||||||
self.ra = rest.JovianRESTAPI(self.configuration)
|
self.ra = rest.JovianRESTAPI(self.configuration)
|
||||||
|
|
||||||
|
self.check_for_setup_error()
|
||||||
|
|
||||||
def check_for_setup_error(self):
|
def check_for_setup_error(self):
|
||||||
"""Verify that the pool exists."""
|
"""Check for setup error."""
|
||||||
if len(self.jovian_hosts) == 0:
|
if len(self.jovian_hosts) == 0:
|
||||||
msg = _("No hosts provided in configuration")
|
msg = _("No hosts provided in configuration")
|
||||||
raise exception.VolumeDriverException(msg)
|
raise exception.VolumeDriverException(msg)
|
||||||
@ -110,6 +117,12 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
|||||||
msg = (_("Unable to identify pool %s") % self._pool)
|
msg = (_("Unable to identify pool %s") % self._pool)
|
||||||
raise exception.VolumeDriverException(msg)
|
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):
|
def _get_target_name(self, volume_name):
|
||||||
"""Return iSCSI target name to access volume."""
|
"""Return iSCSI target name to access volume."""
|
||||||
return '%s%s' % (self.jovian_target_prefix, volume_name)
|
return '%s%s' % (self.jovian_target_prefix, volume_name)
|
||||||
@ -290,14 +303,14 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
|||||||
jcom.origin_snapshot(vol['origin']))
|
jcom.origin_snapshot(vol['origin']))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.ra.delete_lun(vname)
|
self.ra.delete_lun(vname, force_umount=True)
|
||||||
except jexc.JDSSRESTException as err:
|
except jexc.JDSSRESTException as err:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Unable to delete physical volume %(volume)s "
|
"Unable to delete physical volume %(volume)s "
|
||||||
"with error %(err)s.", {
|
"with error %(err)s.", {
|
||||||
"volume": vname,
|
"volume": vname,
|
||||||
"err": err})
|
"err": err})
|
||||||
raise exception.SnapshotIsBusy(err)
|
raise exception.VolumeIsBusy(err)
|
||||||
|
|
||||||
def _delete_back_recursively(self, opvname, opsname):
|
def _delete_back_recursively(self, opvname, opsname):
|
||||||
"""Deletes snapshot by removing its oldest removable parent
|
"""Deletes snapshot by removing its oldest removable parent
|
||||||
@ -391,6 +404,46 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
|||||||
raise exception.VolumeBackendAPIException(
|
raise exception.VolumeBackendAPIException(
|
||||||
(_('Failed to extend volume %s.'), volume.id))
|
(_('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):
|
def _clone_object(self, oname, coname):
|
||||||
"""Creates a clone of specified object
|
"""Creates a clone of specified object
|
||||||
|
|
||||||
@ -430,7 +483,7 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
|||||||
coname,
|
coname,
|
||||||
oname,
|
oname,
|
||||||
sparse=self.jovian_sparse)
|
sparse=self.jovian_sparse)
|
||||||
except jexc.JDSSVolumeExistsException:
|
except jexc.JDSSResourceExistsException:
|
||||||
raise exception.Duplicate()
|
raise exception.Duplicate()
|
||||||
except jexc.JDSSException as err:
|
except jexc.JDSSException as err:
|
||||||
try:
|
try:
|
||||||
@ -671,7 +724,7 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
|||||||
free_capacity = math.floor(int(pool_stats["available"]) / o_units.Gi)
|
free_capacity = math.floor(int(pool_stats["available"]) / o_units.Gi)
|
||||||
|
|
||||||
reserved_percentage = (
|
reserved_percentage = (
|
||||||
self.configuration.safe_get('reserved_percentage'))
|
self.configuration.get('reserved_percentage', 0))
|
||||||
|
|
||||||
if total_capacity is None:
|
if total_capacity is None:
|
||||||
total_capacity = 'unknown'
|
total_capacity = 'unknown'
|
||||||
@ -784,7 +837,7 @@ class JovianISCSIDriver(driver.ISCSIDriver):
|
|||||||
auth = volume.provider_auth
|
auth = volume.provider_auth
|
||||||
|
|
||||||
if not 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)
|
raise exception.VolumeDriverException(msg)
|
||||||
|
|
||||||
(__, auth_username, auth_secret) = auth.split()
|
(__, auth_username, auth_secret) = auth.split()
|
||||||
|
@ -80,3 +80,9 @@ class JDSSSnapshotIsBusyException(JDSSResourceIsBusyException):
|
|||||||
"""Snapshot have dependent clones"""
|
"""Snapshot have dependent clones"""
|
||||||
|
|
||||||
message = _("JDSS snapshot %(snapshot)s is busy.")
|
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 oslo_log import log as logging
|
||||||
|
|
||||||
from cinder import exception
|
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 exception as jexc
|
||||||
from cinder.volume.drivers.open_e.jovian_common import rest_proxy
|
from cinder.volume.drivers.open_e.jovian_common import rest_proxy
|
||||||
|
|
||||||
@ -32,9 +31,7 @@ class JovianRESTAPI(object):
|
|||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
|
|
||||||
self.target_p = config.get('iscsi_target_prefix',
|
self.pool = config.get('jovian_pool', 'Pool-0')
|
||||||
'iqn.2020-04.com.open-e.cinder:')
|
|
||||||
self.pool = config.safe_get('jovian_pool')
|
|
||||||
self.rproxy = rest_proxy.JovianRESTProxy(config)
|
self.rproxy = rest_proxy.JovianRESTProxy(config)
|
||||||
|
|
||||||
self.resource_dne_msg = (
|
self.resource_dne_msg = (
|
||||||
@ -48,7 +45,7 @@ class JovianRESTAPI(object):
|
|||||||
code = resp.get('code', 'Unknown')
|
code = resp.get('code', 'Unknown')
|
||||||
msg = resp.get('message', '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}")
|
"of type:{eclass} reason:{message}")
|
||||||
reason = reason.format(eclass=eclass,
|
reason = reason.format(eclass=eclass,
|
||||||
code=code,
|
code=code,
|
||||||
@ -638,12 +635,12 @@ class JovianRESTAPI(object):
|
|||||||
|
|
||||||
if resp["code"] == 500:
|
if resp["code"] == 500:
|
||||||
if resp["error"]:
|
if resp["error"]:
|
||||||
if resp["error"]["errno"] == 1:
|
|
||||||
raise jexc.JDSSVolumeNotFoundException(
|
|
||||||
volume=volume_name)
|
|
||||||
if resp["error"]["errno"] == 5:
|
if resp["error"]["errno"] == 5:
|
||||||
raise jexc.JDSSSnapshotExistsException(
|
raise jexc.JDSSSnapshotExistsException(
|
||||||
snapshot=snapshot_name)
|
snapshot=snapshot_name)
|
||||||
|
if resp["error"]["errno"] == 1:
|
||||||
|
raise jexc.JDSSVolumeNotFoundException(
|
||||||
|
volume=volume_name)
|
||||||
|
|
||||||
self._general_error(req, resp)
|
self._general_error(req, resp)
|
||||||
|
|
||||||
@ -682,34 +679,42 @@ class JovianRESTAPI(object):
|
|||||||
if resp["error"]["errno"] == 100:
|
if resp["error"]["errno"] == 100:
|
||||||
raise jexc.JDSSVolumeExistsException(
|
raise jexc.JDSSVolumeExistsException(
|
||||||
volume=volume_name)
|
volume=volume_name)
|
||||||
args = {"vol": volume_name, "e": resp['error']['message']}
|
if resp["error"]["errno"] == 1:
|
||||||
msg = _('Failed to create volume %(vol)s, err: %(e)s') % args
|
raise jexc.JDSSResourceNotFoundException(
|
||||||
raise jexc.JDSSRESTException(msg)
|
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):
|
def rollback_volume_to_snapshot(self, volume_name, snapshot_name):
|
||||||
"""is_snapshots.
|
"""Rollback volume to its snapshot
|
||||||
|
|
||||||
GET
|
POST /volumes/<volume_name>/snapshots/<snapshot_name>/rollback
|
||||||
/volumes/<string:volumename>/snapshots/<string:snapshotname>/clones
|
:param volume_name: volume that is going to be restored
|
||||||
|
:param snapshot_name: snapshot of a volume above
|
||||||
:param volume_name: that snapshot belongs to
|
:return:
|
||||||
:return: bool
|
|
||||||
"""
|
"""
|
||||||
req = '/volumes/' + volume_name + '/snapshots/' + snapshot_name + \
|
req = ('/volumes/{vol}/snapshots/'
|
||||||
'/clones'
|
'{snap}/rollback').format(vol=volume_name,
|
||||||
|
snap=snapshot_name)
|
||||||
|
|
||||||
LOG.debug("check if snapshot %(snap)s of volume %(vol)s exists",
|
LOG.debug("rollback volume %(vol)s to snapshot %(snap)s",
|
||||||
{'snap': snapshot_name,
|
{'vol': volume_name,
|
||||||
'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:
|
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,
|
def delete_snapshot(self,
|
||||||
volume_name,
|
volume_name,
|
||||||
@ -733,8 +738,6 @@ class JovianRESTAPI(object):
|
|||||||
umount (defualt false).
|
umount (defualt false).
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if not self.is_snapshot(volume_name, snapshot_name):
|
|
||||||
return
|
|
||||||
|
|
||||||
req = '/volumes/' + volume_name + '/snapshots/' + snapshot_name
|
req = '/volumes/' + volume_name + '/snapshots/' + snapshot_name
|
||||||
|
|
||||||
@ -767,11 +770,7 @@ class JovianRESTAPI(object):
|
|||||||
if resp["error"]["errno"] == 1000:
|
if resp["error"]["errno"] == 1000:
|
||||||
raise jexc.JDSSSnapshotIsBusyException(
|
raise jexc.JDSSSnapshotIsBusyException(
|
||||||
snapshot=snapshot_name)
|
snapshot=snapshot_name)
|
||||||
msg = 'Failed to delete snapshot {}, err: {}'.format(
|
self._general_error(req, resp)
|
||||||
snapshot_name, resp['error']['message'])
|
|
||||||
raise jexc.JDSSRESTException(msg)
|
|
||||||
msg = 'Failed to delete snapshot {}'.format(snapshot_name)
|
|
||||||
raise jexc.JDSSRESTException(msg)
|
|
||||||
|
|
||||||
def get_snapshots(self, volume_name):
|
def get_snapshots(self, volume_name):
|
||||||
"""get_snapshots.
|
"""get_snapshots.
|
||||||
@ -818,7 +817,8 @@ class JovianRESTAPI(object):
|
|||||||
if 'message' in resp['error']:
|
if 'message' in resp['error']:
|
||||||
if self.resource_dne_msg.match(resp['error']['message']):
|
if self.resource_dne_msg.match(resp['error']['message']):
|
||||||
raise jexc.JDSSResourceNotFoundException(volume_name)
|
raise jexc.JDSSResourceNotFoundException(volume_name)
|
||||||
raise jexc.JDSSRESTException('unable to get snapshots')
|
|
||||||
|
self._general_error(req, resp)
|
||||||
|
|
||||||
def get_pool_stats(self):
|
def get_pool_stats(self):
|
||||||
"""get_pool_stats.
|
"""get_pool_stats.
|
||||||
@ -890,4 +890,4 @@ class JovianRESTAPI(object):
|
|||||||
if not resp["error"] and resp["code"] == 200:
|
if not resp["error"] and resp["code"] == 200:
|
||||||
return resp["data"]
|
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."""
|
"""Network connection handling class for JovianDSS driver."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import netutils as o_netutils
|
from oslo_utils import netutils as o_netutils
|
||||||
@ -25,6 +24,7 @@ import urllib3
|
|||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
|
from cinder.utils import retry
|
||||||
from cinder.volume.drivers.open_e.jovian_common import exception as jexc
|
from cinder.volume.drivers.open_e.jovian_common import exception as jexc
|
||||||
|
|
||||||
|
|
||||||
@ -35,17 +35,15 @@ class JovianRESTProxy(object):
|
|||||||
"""Jovian REST API proxy."""
|
"""Jovian REST API proxy."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
""":param config: config is like dict."""
|
""":param config: list of config values."""
|
||||||
|
|
||||||
self.proto = 'http'
|
self.proto = 'http'
|
||||||
if config.get('driver_use_ssl', True):
|
if config.get('driver_use_ssl', True):
|
||||||
self.proto = 'https'
|
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.port = str(config.get('san_api_port', 82))
|
||||||
|
|
||||||
self.active_host = 0
|
|
||||||
|
|
||||||
for host in self.hosts:
|
for host in self.hosts:
|
||||||
if o_netutils.is_valid_ip(host) is False:
|
if o_netutils.is_valid_ip(host) is False:
|
||||||
err_msg = ('Invalid value of jovian_host property: '
|
err_msg = ('Invalid value of jovian_host property: '
|
||||||
@ -55,36 +53,50 @@ class JovianRESTProxy(object):
|
|||||||
LOG.debug(err_msg)
|
LOG.debug(err_msg)
|
||||||
raise exception.InvalidConfigurationValue(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.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.user = config.get('san_login', 'admin')
|
||||||
self.password = config.get('san_password', 'admin')
|
self.password = config.get('san_password', 'admin')
|
||||||
self.auth = requests.auth.HTTPBasicAuth(self.user, self.password)
|
self.verify = config.get('driver_ssl_cert_verify', True)
|
||||||
self.verify = False
|
self.cert = config.get('driver_ssl_cert_path')
|
||||||
self.retry_n = config.get('jovian_rest_send_repeats', 3)
|
|
||||||
self.header = {'connection': 'keep-alive',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'authorization': 'Basic '}
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
def _get_pool_url(self, host):
|
self.session = self._get_session()
|
||||||
url = ('%(proto)s://%(host)s:%(port)s/api/v3/pools/%(pool)s' % {
|
|
||||||
'proto': self.proto,
|
def _get_session(self):
|
||||||
'host': host,
|
"""Create and init new session object"""
|
||||||
'port': self.port,
|
|
||||||
'pool': self.pool})
|
session = requests.Session()
|
||||||
return url
|
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' % {
|
url = ('%(proto)s://%(host)s:%(port)s/api/v3' % {
|
||||||
'proto': self.proto,
|
'proto': self.proto,
|
||||||
'host': host,
|
'host': self.hosts[self.active_host],
|
||||||
'port': self.port})
|
'port': self.port})
|
||||||
|
|
||||||
return url
|
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):
|
def request(self, request_method, req, json_data=None):
|
||||||
"""Send request to the specific url.
|
"""Send request to the specific url.
|
||||||
|
|
||||||
@ -92,39 +104,31 @@ class JovianRESTProxy(object):
|
|||||||
:param url: where to send
|
:param url: where to send
|
||||||
:param json_data: data
|
:param json_data: data
|
||||||
"""
|
"""
|
||||||
for j in range(self.retry_n):
|
out = None
|
||||||
for i in range(len(self.hosts)):
|
for i in range(len(self.hosts)):
|
||||||
host = self.hosts[self.active_host]
|
try:
|
||||||
url = self._get_url(host) + req
|
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)
|
||||||
|
|
||||||
LOG.debug(
|
pr = self.session.prepare_request(r)
|
||||||
"sending request of type %(type)s to %(url)s "
|
out = self._send(pr)
|
||||||
"attempt: %(num)s.",
|
except requests.exceptions.ConnectionError:
|
||||||
{'type': request_method,
|
self._next_host()
|
||||||
'url': url,
|
continue
|
||||||
'num': j})
|
break
|
||||||
|
|
||||||
if json_data is not None:
|
LOG.debug("Geting %(data)s from %(t)s to %(addr)s",
|
||||||
LOG.debug(
|
{'data': out, 't': request_method, 'addr': addr})
|
||||||
"sending data: %s.", json_data)
|
return out
|
||||||
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(self.delay)
|
|
||||||
|
|
||||||
msg = (_('%(times)s faild in a row') % {'times': j})
|
|
||||||
|
|
||||||
raise jexc.JDSSRESTProxyException(host=url, reason=msg)
|
|
||||||
|
|
||||||
def pool_request(self, request_method, req, json_data=None):
|
def pool_request(self, request_method, req, json_data=None):
|
||||||
"""Send request to the specific url.
|
"""Send request to the specific url.
|
||||||
@ -133,94 +137,65 @@ class JovianRESTProxy(object):
|
|||||||
:param url: where to send
|
:param url: where to send
|
||||||
:param json_data: data
|
:param json_data: data
|
||||||
"""
|
"""
|
||||||
url = ""
|
req = "/pools/{pool}{req}".format(pool=self.pool, req=req)
|
||||||
for j in range(self.retry_n):
|
addr = "{base}{req}".format(base=self._get_base_url(), req=req)
|
||||||
for i in range(len(self.hosts)):
|
LOG.debug("Sending pool request %(t)s to %(addr)s",
|
||||||
host = self.hosts[self.active_host]
|
{'t': request_method, 'addr': addr})
|
||||||
url = self._get_pool_url(host) + req
|
return self.request(request_method, req, json_data=json_data)
|
||||||
|
|
||||||
LOG.debug(
|
@retry((requests.exceptions.ConnectionError,
|
||||||
"sending pool request of type %(type)s to %(url)s "
|
jexc.JDSSOSException),
|
||||||
"attempt: %(num)s.",
|
interval=2,
|
||||||
{'type': request_method,
|
backoff_rate=2,
|
||||||
'url': url,
|
retries=7)
|
||||||
'num': j})
|
def _send(self, pr):
|
||||||
|
"""Send prepared request
|
||||||
|
|
||||||
if json_data is not None:
|
:param pr: prepared request
|
||||||
LOG.debug(
|
"""
|
||||||
"JovianDSS: Sending data: %s.", str(json_data))
|
ret = dict()
|
||||||
try:
|
|
||||||
|
|
||||||
ret = self._request_routine(url, request_method, json_data)
|
response_obj = self.session.send(pr)
|
||||||
if len(ret) == 0:
|
|
||||||
self.active_host = ((self.active_host + 1)
|
|
||||||
% len(self.hosts))
|
|
||||||
continue
|
|
||||||
return ret
|
|
||||||
|
|
||||||
except requests.ConnectionError as err:
|
ret['code'] = response_obj.status_code
|
||||||
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})
|
try:
|
||||||
|
data = json.loads(response_obj.text)
|
||||||
raise jexc.JDSSRESTProxyException(host=url, reason=msg)
|
ret["error"] = data.get("error")
|
||||||
|
ret["data"] = data.get("data")
|
||||||
def _request_routine(self, url, request_method, json_data=None):
|
except json.JSONDecodeError:
|
||||||
"""Make an HTTPS request and return the results."""
|
pass
|
||||||
|
|
||||||
ret = None
|
|
||||||
for i in range(3):
|
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return ret
|
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):
|
def get_active_host(self):
|
||||||
"""Return address of currently used host."""
|
"""Return address of currently used host."""
|
||||||
return self.hosts[self.active_host]
|
return self.hosts[self.active_host]
|
||||||
|
@ -19,9 +19,6 @@ jdss_connection_opts = [
|
|||||||
cfg.ListOpt('san_hosts',
|
cfg.ListOpt('san_hosts',
|
||||||
default='',
|
default='',
|
||||||
help='IP address of Open-E JovianDSS SA'),
|
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',
|
cfg.IntOpt('jovian_recovery_delay',
|
||||||
default=60,
|
default=60,
|
||||||
help='Time before HA cluster failure.'),
|
help='Time before HA cluster failure.'),
|
||||||
@ -41,8 +38,8 @@ jdss_iscsi_opts = [
|
|||||||
|
|
||||||
jdss_volume_opts = [
|
jdss_volume_opts = [
|
||||||
cfg.StrOpt('jovian_block_size',
|
cfg.StrOpt('jovian_block_size',
|
||||||
default='128K',
|
default='64K',
|
||||||
help='Block size for volumes (512 - 128K)'),
|
help='Block size can be: 32K, 64K, 128K, 256K, 512K, 1M'),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
@ -38,10 +38,11 @@ Provide settings to JovianDSS driver by adding 'jdss-0' description:
|
|||||||
backend_name = jdss-0
|
backend_name = jdss-0
|
||||||
chap_password_len = 14
|
chap_password_len = 14
|
||||||
driver_use_ssl = True
|
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:
|
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
||||||
jovian_pool = Pool-0
|
jovian_pool = Pool-0
|
||||||
jovian_block_size = 128K
|
jovian_block_size = 128K
|
||||||
jovian_rest_send_repeats = 4
|
|
||||||
san_api_port = 82
|
san_api_port = 82
|
||||||
target_port = 3260
|
target_port = 3260
|
||||||
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
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``
|
* - ``driver_use_ssl``
|
||||||
- True
|
- True
|
||||||
- Use SSL to send requests to JovianDSS[1]
|
- 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``
|
* - ``iscsi_target_prefix``
|
||||||
- iqn.2016-04.com.open-e:01:cinder-
|
- iqn.2016-04.com.open-e:01:cinder-
|
||||||
- Prefix that will be used to form target name for volume
|
- 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``
|
* - ``jovian_block_size``
|
||||||
- 128K
|
- 128K
|
||||||
- Block size for newly created volumes
|
- 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``
|
* - ``san_api_port``
|
||||||
- 82
|
- 82
|
||||||
- Rest port according to the settings in [1]
|
- 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]
|
- Must be set according to the settings in [1]
|
||||||
* - ``san_password``
|
* - ``san_password``
|
||||||
- admin
|
- admin
|
||||||
- Jovian password [1], **should be changed** for security purpouses
|
- Jovian password [1], **should be changed** for security purposes
|
||||||
* - ``san_thin_provision``
|
* - ``san_thin_provision``
|
||||||
- False
|
- False
|
||||||
- Using thin provisioning for new volumes
|
- 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
|
backend_name = jdss-0
|
||||||
chap_password_len = 14
|
chap_password_len = 14
|
||||||
driver_use_ssl = True
|
driver_use_ssl = True
|
||||||
|
driver_ssl_cert_verify = False
|
||||||
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
||||||
jovian_pool = Pool-0
|
jovian_pool = Pool-0
|
||||||
jovian_block_size = 128K
|
jovian_block_size = 128K
|
||||||
jovian_rest_send_repeats = 4
|
|
||||||
san_api_port = 82
|
san_api_port = 82
|
||||||
target_port = 3260
|
target_port = 3260
|
||||||
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
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
|
backend_name = jdss-1
|
||||||
chap_password_len = 14
|
chap_password_len = 14
|
||||||
driver_use_ssl = True
|
driver_use_ssl = True
|
||||||
|
driver_ssl_cert_verify = False
|
||||||
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
||||||
jovian_pool = Pool-1
|
jovian_pool = Pool-1
|
||||||
jovian_block_size = 128K
|
jovian_block_size = 128K
|
||||||
jovian_rest_send_repeats = 4
|
|
||||||
san_api_port = 82
|
san_api_port = 82
|
||||||
target_port = 3260
|
target_port = 3260
|
||||||
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
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
|
backend_name = jdss-2
|
||||||
chap_password_len = 14
|
chap_password_len = 14
|
||||||
driver_use_ssl = True
|
driver_use_ssl = True
|
||||||
|
driver_ssl_cert_verify = False
|
||||||
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
|
||||||
jovian_pool = Pool-0
|
jovian_pool = Pool-0
|
||||||
jovian_block_size = 128K
|
jovian_block_size = 128K
|
||||||
jovian_rest_send_repeats = 4
|
|
||||||
san_api_port = 82
|
san_api_port = 82
|
||||||
target_port = 3260
|
target_port = 3260
|
||||||
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
|
||||||
|
@ -150,6 +150,9 @@ title=Generic NFS Reference Driver (NFS)
|
|||||||
[driver.nimble]
|
[driver.nimble]
|
||||||
title=Nimble Storage Driver (iSCSI, FC)
|
title=Nimble Storage Driver (iSCSI, FC)
|
||||||
|
|
||||||
|
[driver.opene_joviandss]
|
||||||
|
title=Open-E JovianDSS Storage Driver (iSCSI)
|
||||||
|
|
||||||
[driver.prophetstor]
|
[driver.prophetstor]
|
||||||
title=ProphetStor Flexvisor Driver (iSCSI, NFS)
|
title=ProphetStor Flexvisor Driver (iSCSI, NFS)
|
||||||
|
|
||||||
@ -259,6 +262,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=complete
|
driver.nexenta=complete
|
||||||
driver.nfs=complete
|
driver.nfs=complete
|
||||||
driver.nimble=complete
|
driver.nimble=complete
|
||||||
|
driver.opene_joviandss=complete
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=complete
|
driver.qnap=complete
|
||||||
@ -328,6 +332,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=complete
|
driver.nexenta=complete
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=complete
|
driver.nimble=complete
|
||||||
|
driver.opene_joviandss=missing
|
||||||
driver.prophetstor=complete
|
driver.prophetstor=complete
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=complete
|
driver.qnap=complete
|
||||||
@ -397,6 +402,7 @@ driver.netapp_solidfire=missing
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=missing
|
driver.nimble=missing
|
||||||
|
driver.opene_joviandss=missing
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=missing
|
driver.pure=missing
|
||||||
driver.qnap=missing
|
driver.qnap=missing
|
||||||
@ -469,6 +475,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=missing
|
driver.nimble=missing
|
||||||
|
driver.opene_joviandss=missing
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=missing
|
driver.qnap=missing
|
||||||
@ -540,6 +547,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=missing
|
driver.nimble=missing
|
||||||
|
driver.opene_joviandss=missing
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=missing
|
driver.qnap=missing
|
||||||
@ -612,6 +620,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=complete
|
driver.nimble=complete
|
||||||
|
driver.opene_joviandss=missing
|
||||||
driver.prophetstor=complete
|
driver.prophetstor=complete
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=missing
|
driver.qnap=missing
|
||||||
@ -683,6 +692,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=complete
|
driver.nfs=complete
|
||||||
driver.nimble=complete
|
driver.nimble=complete
|
||||||
|
driver.opene_joviandss=complete
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=missing
|
driver.qnap=missing
|
||||||
@ -755,6 +765,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=missing
|
driver.nimble=missing
|
||||||
|
driver.opene_joviandss=missing
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=missing
|
driver.pure=missing
|
||||||
driver.qnap=missing
|
driver.qnap=missing
|
||||||
@ -827,6 +838,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=complete
|
driver.nimble=complete
|
||||||
|
driver.opene_joviandss=missing
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=missing
|
driver.qnap=missing
|
||||||
@ -896,6 +908,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=complete
|
driver.nimble=complete
|
||||||
|
driver.opene_joviandss=complete
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=missing
|
driver.qnap=missing
|
||||||
@ -969,6 +982,7 @@ driver.netapp_solidfire=complete
|
|||||||
driver.nexenta=missing
|
driver.nexenta=missing
|
||||||
driver.nfs=missing
|
driver.nfs=missing
|
||||||
driver.nimble=missing
|
driver.nimble=missing
|
||||||
|
driver.opene_joviandss=missing
|
||||||
driver.prophetstor=missing
|
driver.prophetstor=missing
|
||||||
driver.pure=complete
|
driver.pure=complete
|
||||||
driver.qnap=missing
|
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