Browse Source

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
changes/60/763760/10
zenkuro 6 months ago
parent
commit
d501d1a880
  1. 239
      cinder/tests/unit/volume/drivers/open_e/test_iscsi.py
  2. 522
      cinder/tests/unit/volume/drivers/open_e/test_rest.py
  3. 325
      cinder/tests/unit/volume/drivers/open_e/test_rest_proxy.py
  4. 93
      cinder/volume/drivers/open_e/iscsi.py
  5. 6
      cinder/volume/drivers/open_e/jovian_common/exception.py
  6. 72
      cinder/volume/drivers/open_e/jovian_common/rest.py
  7. 255
      cinder/volume/drivers/open_e/jovian_common/rest_proxy.py
  8. 7
      cinder/volume/drivers/open_e/options.py
  9. 20
      doc/source/configuration/block-storage/drivers/open-e-joviandss-driver.rst
  10. 14
      doc/source/reference/support-matrix.ini
  11. 7
      releasenotes/notes/bp-jdss-add-cert-and-snapshot-revert-b34f352754ad07de.yaml

239
cinder/tests/unit/volume/drivers/open_e/test_iscsi.py

@ -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,11 +191,16 @@ 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
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()
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)

522
cinder/tests/unit/volume/drivers/open_e/test_rest.py

@ -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

@ -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))

93
cinder/volume/drivers/open_e/iscsi.py

@ -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'