JovianDSS: Rework Open-E JovianDSS driver

Provide major rework of Open-E JovianDSS driver that involves:
 - improve performance of volume and snapshot creation and deletion
 - remove revert to snapshot support
 - improve storage net interface picking algorithm

Change-Id: Ifc2aa5d3622315ae5c70b2d8e809e1b1553684ea
Implements: blueprint open-e-joviandss-iscsi-rework
This commit is contained in:
Andrei 2023-07-23 13:17:08 -04:00 committed by andrei.perepiolkin
parent 712032959c
commit 40a178e3d3
12 changed files with 3693 additions and 1999 deletions

View File

@ -0,0 +1,305 @@
# Copyright (c) 2023 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 copy
from cinder import exception
from cinder.tests.unit import test
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_S1 = '12345678-1234-1234-1234-100000000001'
UUID_S2 = '12345678-1234-1234-1234-100000000002'
V_UUID_1 = 'v_12345678-1234-1234-1234-000000000001'
V_UUID_2 = 'v_12345678-1234-1234-1234-000000000002'
V_UUID_3 = 'v_12345678-1234-1234-1234-000000000003'
S_UUID_1 = f's_{UUID_S1}_{UUID_1}'
S_UUID_2 = f's_{UUID_S2}_{UUID_1}'
T_UUID_1 = 't_12345678-1234-1234-1234-000000000001'
VOLUME_GET_THAT_IS_CLONE = {
"origin": f"Pool-0/{V_UUID_1}@{S_UUID_1}",
"relatime": None,
"acltype": None,
"vscan": None,
"full_name": f"Pool-0/{jcom.vname(UUID_2)}",
"userrefs": None,
"primarycache": "all",
"logbias": "latency",
"creation": "1695078560",
"sync": "always",
"is_clone": True,
"dedup": "off",
"sharenfs": None,
"receive_resume_token": None,
"volsize": "1073741824",
"referenced": "57344",
"sharesmb": None,
"createtxg": "19812058",
"reservation": "0",
"scontext": None,
"mountpoint": None,
"casesensitivity": None,
"guid": "4947994863040470005",
"usedbyrefreservation": "0",
"dnodesize": None,
"written": "0",
"logicalused": "0",
"compressratio": "1.00",
"rootcontext": "none",
"default_scsi_id": "5c02d042ed8dbce2",
"type": "volume",
"compression": "lz4",
"snapdir": None,
"overlay": None,
"encryption": "off",
"xattr": None,
"volmode": "default",
"copies": "1",
"snapshot_limit": "18446744073709551615",
"aclinherit": None,
"defcontext": "none",
"readonly": "off",
"version": None,
"recordsize": None,
"filesystem_limit": None,
"mounted": None,
"mlslabel": "none",
"secondarycache": "all",
"refreservation": "0",
"available": "954751713280",
"san:volume_id": "5c02d042ed8dbce2570c8d5dc276dd6a2431e138",
"encryptionroot": None,
"exec": None,
"refquota": None,
"refcompressratio": "1.00",
"quota": None,
"utf8only": None,
"keylocation": "none",
"snapdev": "hidden",
"snapshot_count": "18446744073709551615",
"fscontext": "none",
"clones": None,
"canmount": None,
"keystatus": None,
"atime": None,
"usedbysnapshots": "0",
"normalization": None,
"usedbychildren": "0",
"volblocksize": "65536",
"usedbydataset": "0",
"objsetid": "19228",
"name": "a2",
"defer_destroy": None,
"pbkdf2iters": "0",
"checksum": "on",
"redundant_metadata": "all",
"filesystem_count": None,
"devices": None,
"keyformat": "none",
"setuid": None,
"used": "0",
"logicalreferenced": "28672",
"context": "none",
"zoned": None,
"nbmand": None,
}
SNAPSHOT_GET = {
'referenced': '57344',
'userrefs': '0',
'primarycache': 'all',
'creation': '2023-06-28 16:49:33',
'volsize': '1073741824',
'createtxg': '18402390',
'guid': '15554334551928551694',
'compressratio': '1.00',
'rootcontext': 'none',
'encryption': 'off',
'defcontext': 'none',
'written': '0',
'type': 'snapshot',
'secondarycache': 'all',
'used': '0',
'refcompressratio': '1.00',
'fscontext': 'none',
'objsetid': '106843',
'name': S_UUID_1,
'defer_destroy': 'off',
'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca',
'mlslabel': 'none',
'logicalreferenced': '28672',
'context': 'none'}
SNAPSHOT_MULTIPLE_CLONES = {
'referenced': '57344',
'userrefs': '0',
'primarycache': 'all',
'creation': '2023-06-28 18:44:49',
'volsize': '1073741824',
'createtxg': '18403768',
'guid': '18319280142829358721',
'compressratio': '1.00',
'rootcontext': 'none',
'encryption': 'off',
'defcontext': 'none',
'written': '0',
'type': 'snapshot',
'secondarycache': 'all',
'used': '0',
'refcompressratio': '1.00',
'fscontext': 'none',
'objsetid': '107416',
'clones': f'Pool-0/{V_UUID_2},Pool-0/{V_UUID_3}',
'name': S_UUID_1,
'defer_destroy': 'off',
'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca',
'mlslabel': 'none',
'logicalreferenced': '28672',
'context': 'none'}
SNAPSHOTS_GET_NO_CLONES = [
{'referenced': '57344',
'userrefs': '0',
'primarycache': 'all',
'creation': '2023-06-28 16:49:33',
'volsize': '1073741824',
'createtxg': '18402390',
'guid': '15554334551928551694',
'compressratio': '1.00',
'rootcontext': 'none',
'encryption': 'off',
'defcontext': 'none',
'written': '0',
'type': 'snapshot',
'secondarycache': 'all',
'used': '0',
'refcompressratio': '1.00',
'fscontext': 'none',
'objsetid': '106843',
'name': S_UUID_1,
'defer_destroy': 'off',
'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca',
'mlslabel': 'none',
'logicalreferenced': '28672',
'context': 'none'},
{'referenced': '57344',
'userrefs': '0',
'primarycache': 'all',
'creation': '2023-06-28 18:44:49',
'volsize': '1073741824',
'createtxg': '18403768',
'guid': '18319280142829358721',
'compressratio': '1.00',
'rootcontext': 'none',
'encryption': 'off',
'defcontext': 'none',
'written': '0',
'type': 'snapshot',
'secondarycache': 'all',
'used': '0',
'refcompressratio': '1.00',
'fscontext': 'none',
'objsetid': '107416',
'name': S_UUID_2,
'defer_destroy': 'off',
'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca',
'mlslabel': 'none',
'logicalreferenced': '28672',
'context': 'none'}]
class TestOpenEJovianDSSCommon(test.TestCase):
def test_is_volume(self):
self.assertFalse(jcom.is_volume("asdasd"))
self.assertFalse(jcom.is_volume(UUID_1))
self.assertTrue(jcom.is_volume(V_UUID_1))
def test_is_snapshot(self):
self.assertFalse(jcom.is_snapshot("asdasd"))
self.assertFalse(jcom.is_snapshot(UUID_S1))
self.assertTrue(jcom.is_snapshot(S_UUID_1))
def test_idname(self):
self.assertEqual(UUID_1, jcom.idname(V_UUID_1))
self.assertEqual(UUID_S1, jcom.idname(S_UUID_1))
self.assertEqual(UUID_1, jcom.idname(T_UUID_1))
self.assertRaises(exception.VolumeDriverException, jcom.idname, 'asd')
def test_vname(self):
self.assertEqual(V_UUID_1, jcom.vname(UUID_1))
self.assertEqual(V_UUID_1, jcom.vname(V_UUID_1))
self.assertRaises(exception.VolumeDriverException,
jcom.vname, S_UUID_1)
def test_sname_to_id(self):
self.assertEqual((UUID_S1, UUID_1), jcom.sname_to_id(S_UUID_1))
def test_sid_from_sname(self):
self.assertEqual(UUID_S1, jcom.sid_from_sname(S_UUID_1))
def test_vid_from_sname(self):
self.assertEqual(UUID_1, jcom.vid_from_sname(S_UUID_1))
def test_sname(self):
self.assertEqual(S_UUID_1, jcom.sname(UUID_S1, UUID_1))
def test_sname_from_snap(self):
snap = copy.deepcopy(SNAPSHOT_GET)
self.assertEqual(S_UUID_1, jcom.sname_from_snap(snap))
def test_is_hidden(self):
self.assertTrue(jcom.is_hidden(T_UUID_1))
self.assertFalse(jcom.is_hidden(S_UUID_1))
def test_origin_snapshot(self):
vol = copy.deepcopy(VOLUME_GET_THAT_IS_CLONE)
self.assertEqual(S_UUID_1, jcom.origin_snapshot(vol))
def test_origin_volume(self):
vol = copy.deepcopy(VOLUME_GET_THAT_IS_CLONE)
self.assertEqual(V_UUID_1, jcom.origin_volume(vol))
def test_snapshot_clones(self):
clones = [V_UUID_2, V_UUID_3]
snap = copy.deepcopy(SNAPSHOT_MULTIPLE_CLONES)
self.assertEqual(clones, jcom.snapshot_clones(snap))
def test_hidden(self):
self.assertEqual(T_UUID_1, jcom.hidden(V_UUID_1))
def test_get_newest_snapshot_name(self):
snaps = copy.deepcopy(SNAPSHOTS_GET_NO_CLONES)
self.assertEqual(S_UUID_2, jcom.get_newest_snapshot_name(snaps))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,6 @@ from unittest import mock
from oslo_utils import units as o_units
from cinder import context
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 jdss_common as jcom
@ -28,6 +27,10 @@ UUID_1 = '12345678-1234-1234-1234-000000000001'
UUID_2 = '12345678-1234-1234-1234-000000000002'
UUID_3 = '12345678-1234-1234-1234-000000000003'
UUID_S1 = '12345678-1234-1234-1234-100000000001'
UUID_S2 = '12345678-1234-1234-1234-100000000002'
UUID_S3 = '12345678-1234-1234-1234-100000000003'
CONFIG_OK = {
'san_hosts': ['192.168.0.2'],
'san_api_port': 82,
@ -39,7 +42,7 @@ CONFIG_OK = {
'jovian_ignore_tpath': [],
'target_port': 3260,
'jovian_pool': 'Pool-0',
'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:',
'target_prefix': 'iqn.2020-04.com.open-e.cinder:',
'chap_password_len': 12,
'san_thin_provision': False,
'jovian_block_size': '128K'
@ -408,7 +411,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
delete_lun_expected += [mock.call('DELETE', addr)]
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(
exception.VolumeIsBusy,
jexc.JDSSResourceIsBusyException,
jrest.delete_lun,
'v_' + UUID_1)
@ -424,7 +427,6 @@ class TestOpenEJovianRESTAPI(test.TestCase):
'error': None,
'code': 204}
req = {'recursively_children': True,
'recursively_dependents': True,
'force_umount': True}
delete_lun_expected = [mock.call('DELETE', addr, json_data=req)]
@ -432,7 +434,6 @@ class TestOpenEJovianRESTAPI(test.TestCase):
self.assertIsNone(
jrest.delete_lun('v_' + UUID_1,
recursively_children=True,
recursively_dependents=True,
force_umount=True))
jrest.rproxy.pool_request.assert_has_calls(delete_lun_expected)
@ -441,7 +442,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
addr = '/san/iscsi/targets/{}'.format(tname)
data = {'incoming_users_active': True,
'name': tname,
@ -483,7 +484,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
# Create OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
addr = '/san/iscsi/targets'
data = {'incoming_users_active': True,
'name': tname,
@ -506,7 +507,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
self.assertIsNone(jrest.create_target(tname))
# Target exists
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
addr = '/san/iscsi/targets'
data = {'incoming_users_active': True,
'name': tname,
@ -541,7 +542,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest.create_target, tname)
# Unknown error
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
addr = "/san/iscsi/targets"
resp = {'data': data,
@ -576,7 +577,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
# Delete OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
addr = '/san/iscsi/targets/{}'.format(tname)
resp = {'data': None,
@ -628,7 +629,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
# Modify OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
addr = '/san/iscsi/targets/{}/incoming-users'.format(tname)
chap_cred = {"name": "chapuser",
@ -682,7 +683,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
# Get OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
addr = '/san/iscsi/targets/{}/incoming-users'.format(tname)
chap_users = {"name": "chapuser"}
@ -736,7 +737,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
# Delete OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
user = "chapuser"
addr = '/san/iscsi/targets/{}/incoming-users/chapuser'.format(tname)
@ -791,7 +792,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
# lun present
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
vname = jcom.vname(UUID_1)
addr = '/san/iscsi/targets/{target}/luns/{lun}'.format(
target=tname, lun=vname)
@ -852,7 +853,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
# attach ok
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
vname = jcom.vname(UUID_1)
addr = '/san/iscsi/targets/{}/luns'.format(tname)
@ -875,7 +876,38 @@ class TestOpenEJovianRESTAPI(test.TestCase):
mock.call('POST', addr, json_data=jbody)]
self.assertIsNone(jrest.attach_target_vol(tname, vname))
# attach with mode and lun
jrest, ctx = self.get_rest(CONFIG_OK)
tname = CONFIG_OK['target_prefix'] + UUID_1
vname = jcom.vname(UUID_1)
addr = '/san/iscsi/targets/{}/luns'.format(tname)
jbody = {"name": vname, "lun": 1, "mode": 'ro'}
data = {"block_size": 512,
"device_handler": "vdisk_fileio",
"lun": 0,
"mode": "ro",
"name": vname,
"prod_id": "Storage",
"scsi_id": "99e2c883331edf87"}
resp = {'data': data,
'error': None,
'code': 201}
jrest.rproxy.pool_request.return_value = resp
attach_target_vol_expected = [
mock.call('POST', addr, json_data=jbody)]
self.assertIsNone(jrest.attach_target_vol(tname, vname,
lun_id=1, mode='ro'))
jrest.rproxy.pool_request.assert_has_calls(attach_target_vol_expected)
# lun attached already
jrest, ctx = self.get_rest(CONFIG_OK)
jbody = {"name": vname, "lun": 0}
url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr)
msg = 'Volume /dev/Pool-0/{} is already used.'.format(vname)
err = {"class": "opene.exceptions.ItemConflictError",
@ -893,6 +925,9 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest.attach_target_vol, tname, vname)
# no such target
jrest, ctx = self.get_rest(CONFIG_OK)
jbody = {"name": vname, "lun": 0}
url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr)
msg = 'Target {} not exists.'.format(vname)
err = {"class": "opene.exceptions.ItemNotFoundError",
@ -904,12 +939,14 @@ class TestOpenEJovianRESTAPI(test.TestCase):
'code': 404}
jrest.rproxy.pool_request.return_value = resp
attach_target_vol_expected += [
attach_target_vol_expected = [
mock.call('POST', addr, json_data=jbody)]
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.attach_target_vol, tname, vname)
# error unknown
jrest, ctx = self.get_rest(CONFIG_OK)
jbody = {"name": vname, "lun": 0}
url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr)
msg = 'Target {} not exists.'.format(vname)
@ -923,17 +960,28 @@ class TestOpenEJovianRESTAPI(test.TestCase):
'code': 500}
jrest.rproxy.pool_request.return_value = resp
attach_target_vol_expected += [
attach_target_vol_expected = [
mock.call('POST', addr, json_data=jbody)]
self.assertRaises(jexc.JDSSException,
jrest.attach_target_vol, tname, vname)
jrest.rproxy.pool_request.assert_has_calls(attach_target_vol_expected)
# error incorrect mode
jrest, ctx = self.get_rest(CONFIG_OK)
jbody = {"name": vname, "lun": 0}
url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr)
attach_target_vol_expected = [
mock.call('POST', addr, json_data=jbody)]
self.assertRaises(jexc.JDSSException,
jrest.attach_target_vol, tname, vname, mode='bad')
jrest.rproxy.pool_request.assert_not_called()
def test_detach_target_vol(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# detach target vol ok
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
tname = CONFIG_OK['target_prefix'] + UUID_1
vname = jcom.vname(UUID_1)
addr = '/san/iscsi/targets/{tar}/luns/{vol}'.format(
@ -989,9 +1037,9 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
vname = jcom.vname(UUID_1)
sname = jcom.sname(UUID_2)
sname = jcom.sname(UUID_S1, UUID_1)
data = {'name': jcom.sname(UUID_2)}
data = {'name': jcom.sname(UUID_S2, UUID_1)}
resp = {'data': data,
'error': None,
'code': 201}
@ -1003,7 +1051,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
vname = jcom.vname(UUID_1)
sname = jcom.sname(UUID_2)
sname = jcom.sname(UUID_S1, UUID_1)
addr = '/volumes/{vol}/snapshots'.format(vol=vname)
req = {'snapshot_name': sname}
@ -1066,7 +1114,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
vname = jcom.vname(UUID_1)
sname = jcom.sname(UUID_2)
sname = jcom.sname(UUID_S1, UUID_1)
cname = jcom.vname(UUID_3)
addr = '/volumes/{vol}/clone'.format(vol=vname)
@ -1101,7 +1149,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
vname = jcom.vname(UUID_1)
sname = jcom.sname(UUID_2)
sname = jcom.sname(UUID_S2, UUID_2)
cname = jcom.vname(UUID_3)
addr = '/volumes/{vol}/clone'.format(vol=vname)
@ -1177,7 +1225,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
vname = jcom.vname(UUID_1)
sname = jcom.sname(UUID_2)
sname = jcom.sname(UUID_S2, UUID_2)
req = ('/volumes/{vol}/snapshots/'
'{snap}/rollback').format(vol=vname, snap=sname)
@ -1198,7 +1246,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
vname = jcom.vname(UUID_1)
sname = jcom.sname(UUID_2)
sname = jcom.sname(UUID_S2, UUID_2)
req = ('/volumes/{vol}/snapshots/'
'{snap}/rollback').format(vol=vname,
@ -1253,13 +1301,12 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
vname = jcom.vname(UUID_1)
sname = jcom.sname(UUID_2)
sname = jcom.sname(UUID_S2, UUID_2)
addr = '/volumes/{vol}/snapshots/{snap}'.format(vol=vname, snap=sname)
jbody = {
'recursively_children': True,
'recursively_dependents': True,
'force_umount': True
}
@ -1268,7 +1315,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
'code': 204}
jrest.rproxy.pool_request.return_value = resp
delete_snapshot_expected = [mock.call('DELETE', addr)]
delete_snapshot_expected = [mock.call('DELETE', addr, json_data={})]
self.assertIsNone(jrest.delete_snapshot(vname, sname))
delete_snapshot_expected += [
@ -1276,7 +1323,6 @@ class TestOpenEJovianRESTAPI(test.TestCase):
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)
@ -1285,8 +1331,8 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest, ctx = self.get_rest(CONFIG_OK)
vname = jcom.vname(UUID_1)
sname = jcom.sname(UUID_2)
cname = jcom.sname(UUID_3)
sname = jcom.sname(UUID_S2, UUID_1)
cname = jcom.sname(UUID_S3, UUID_1)
addr = '/volumes/{vol}/snapshots/{snap}'.format(vol=vname, snap=sname)
@ -1307,7 +1353,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
jrest.rproxy.pool_request.return_value = resp
delete_snapshot_expected = [
mock.call('DELETE', addr)]
mock.call('DELETE', addr, json_data={})]
self.assertRaises(jexc.JDSSSnapshotIsBusyException,
jrest.delete_snapshot,
@ -1325,7 +1371,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
'code': 500}
jrest.rproxy.pool_request.return_value = resp
delete_snapshot_expected += [mock.call('DELETE', addr)]
delete_snapshot_expected += [mock.call('DELETE', addr, json_data={})]
self.assertRaises(jexc.JDSSException,
jrest.delete_snapshot, vname, sname)
@ -1340,7 +1386,7 @@ class TestOpenEJovianRESTAPI(test.TestCase):
data = {"results": 2,
"entries": {"referenced": "65536",
"name": jcom.sname(UUID_2),
"name": jcom.sname(UUID_S1, UUID_1),
"defer_destroy": "off",
"userrefs": "0",
"primarycache": "all",

View File

@ -41,7 +41,7 @@ CONFIG_OK = {
'jovian_ignore_tpath': [],
'target_port': 3260,
'jovian_pool': 'Pool-0',
'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:',
'target_prefix': 'iqn.2020-04.com.open-e.cinder:',
'chap_password_len': 12,
'san_thin_provision': False,
'jovian_block_size': '128K'
@ -61,7 +61,7 @@ CONFIG_BAD_IP = {
'jovian_ignore_tpath': [],
'target_port': 3260,
'jovian_pool': 'Pool-0',
'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:',
'target_prefix': 'iqn.2020-04.com.open-e.cinder:',
'chap_password_len': 12,
'san_thin_provision': False,
'jovian_block_size': '128K'
@ -81,7 +81,7 @@ CONFIG_MULTIHOST = {
'jovian_ignore_tpath': [],
'target_port': 3260,
'jovian_pool': 'Pool-0',
'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:',
'target_prefix': 'iqn.2020-04.com.open-e.cinder:',
'chap_password_len': 12,
'san_thin_provision': False,
'jovian_block_size': '128K'
@ -102,12 +102,12 @@ class TestOpenEJovianRESTProxy(test.TestCase):
def test_init(self):
self.assertRaises(exception.InvalidConfigurationValue,
rest_proxy.JovianRESTProxy,
rest_proxy.JovianDSSRESTProxy,
CONFIG_BAD_IP)
def test_get_base_url(self):
proxy = rest_proxy.JovianRESTProxy(CONFIG_OK)
proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_OK)
url = proxy._get_base_url()
@ -119,7 +119,7 @@ class TestOpenEJovianRESTProxy(test.TestCase):
def test_next_host(self):
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST)
self.assertEqual(0, proxy.active_host)
proxy._next_host()
@ -134,7 +134,7 @@ class TestOpenEJovianRESTProxy(test.TestCase):
def test_request(self):
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST)
patches = [
mock.patch.object(requests, "Request", return_value="request"),
@ -153,7 +153,7 @@ class TestOpenEJovianRESTProxy(test.TestCase):
def test_request_host_failure(self):
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST)
patches = [
mock.patch.object(requests, "Request", return_value="request"),
@ -185,7 +185,7 @@ class TestOpenEJovianRESTProxy(test.TestCase):
def test_pool_request(self):
proxy = rest_proxy.JovianRESTProxy(CONFIG_OK)
proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_OK)
patches = [mock.patch.object(proxy, "request")]
@ -199,7 +199,7 @@ class TestOpenEJovianRESTProxy(test.TestCase):
def test_send(self):
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST)
json_data = {"data": [{"available": "949998694400",
"status": 26,
@ -236,70 +236,52 @@ class TestOpenEJovianRESTProxy(test.TestCase):
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'
def test_request_host_change(self):
proxy = rest_proxy.JovianDSSRESTProxy(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'),
mock.call('GET',
'https://192.168.0.2:82/api/v3/pools/Pool-0')]
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)
proxy._send.side_effect = [
requests.exceptions.ConnectionError(),
requests.exceptions.ConnectionError(),
requests.exceptions.ConnectionError(),
"out_data"]
proxy.request('GET', '/pools/Pool-0')
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'])
requests.Request.assert_has_calls(request_expected)
self.stop_patches(patches)
def test_send_mixed_error(self):
proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST)
json_data = {"data": None,
"error": None}
def test_send_jsondecode_error(self):
proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST)
session_ret = mock.Mock()
session_ret.text = json.dumps(json_data)
session_ret.text = "{ some-bad-json"
session_ret.status_code = 200
patches = [mock.patch.object(proxy.session, "send")]
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]
side_effect = [session_ret] * 3
proxy.session.send.side_effect = side_effect
send_expected = [mock.call(pr)] * 3
send_expected = [mock.call(pr)] * 7
self.assertRaises(jexc.JDSSOSException, proxy._send, pr)
self.assertRaises(json.JSONDecodeError, 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",
@ -314,7 +296,7 @@ class TestOpenEJovianRESTProxy(test.TestCase):
session_ret.status_code = 500
self.assertRaises(jexc.JDSSOSException,
rest_proxy.JovianRESTProxy._handle_500,
rest_proxy.JovianDSSRESTProxy._handle_500,
session_ret)
session_ret.status_code = 200
@ -322,4 +304,5 @@ class TestOpenEJovianRESTProxy(test.TestCase):
"error": None}
session_ret.text = json.dumps(json_data)
self.assertIsNone(rest_proxy.JovianRESTProxy._handle_500(session_ret))
self.assertIsNone(
rest_proxy.JovianDSSRESTProxy._handle_500(session_ret))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,814 @@
# Copyright (c) 2023 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.
from oslo_log import log as logging
from oslo_utils import units as o_units
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 jdss_common as jcom
from cinder.volume.drivers.open_e.jovian_common import rest
LOG = logging.getLogger(__name__)
class JovianDSSDriver(object):
def __init__(self, config):
self.configuration = config
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.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.get('jovian_block_size', '64K'))
self.jovian_sparse = (
self.configuration.get('san_thin_provision', True))
self.jovian_ignore_tpath = self.configuration.get(
'jovian_ignore_tpath', None)
self.jovian_hosts = self.configuration.get(
'san_hosts', [])
self.ra = rest.JovianRESTAPI(config)
def rest_config_is_ok(self):
"""Check config correctness by checking pool availability"""
return self.ra.is_pool_exists()
def get_active_ifaces(self):
"""Return list of ip addresses for iSCSI connection"""
return self.jovian_hosts
def get_provider_location(self, volume_name):
"""Return volume iscsiadm-formatted provider location string."""
return '%(host)s:%(port)s,1 %(name)s 0' % {
'host': self.ra.get_active_host(),
'port': self.jovian_iscsi_target_portal_port,
'name': self._get_target_name(volume_name)}
def create_volume(self, volume_id, volume_size, sparse=False,
block_size=None):
"""Create a volume.
:param str volume_id: volume id
:param int volume_size: size in Gi
:param bool sparse: thin or thick volume flag (default thin)
:param int block_size: size of block (default None)
:return: None
"""
vname = jcom.vname(volume_id)
LOG.debug("Create volume:%(name)s with size:%(size)s",
{'name': volume_id, 'size': volume_size})
self.ra.create_lun(vname,
volume_size * o_units.Gi,
sparse=sparse,
block_size=block_size)
return
def _promote_newest_delete(self, vname, snapshots=None):
'''Promotes and delete volume
This function deletes volume.
It will promote volume if needed before deletion.
:param str vname: physical volume id
:param list snapshots: snapshot data list (default None)
:return: None
'''
if snapshots is None:
try:
snapshots = self.ra.get_snapshots(vname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s do not exists, it was already '
'deleted', vname)
return
bsnaps = self._list_busy_snapshots(vname, snapshots)
if len(bsnaps) != 0:
promote_target = None
sname = jcom.get_newest_snapshot_name(bsnaps)
for snap in bsnaps:
if snap['name'] == sname:
cvnames = jcom.snapshot_clones(snap)
for cvname in cvnames:
if jcom.is_volume(cvname):
promote_target = cvname
if jcom.is_snapshot(cvname):
self._promote_newest_delete(cvname)
if jcom.is_hidden(cvname):
self._promote_newest_delete(cvname)
break
if promote_target is None:
self._promote_newest_delete(vname)
return
self.ra.promote(vname, sname, promote_target)
self._delete_vol_with_source_snap(vname, recursive=True)
def _delete_vol_with_source_snap(self, vname, recursive=False):
'''Delete volume and its source snapshot if required
This function deletes volume.
If volume is a clone it will check its source snapshot if
one is originates from volume to delete.
:param str vname: physical volume id
:param bool recursive: recursive flag (default False)
:return: None
'''
vol = None
try:
vol = self.ra.get_lun(vname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('unable to get volume %s info, '
'assume it was already deleted', vname)
return
try:
self.ra.delete_lun(vname,
force_umount=True,
recursively_children=recursive)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s do not exists, it was already '
'deleted', vname)
return
if vol is not None and \
'origin' in vol and \
vol['origin'] is not None:
if jcom.is_volume(jcom.origin_snapshot(vol)) or \
jcom.is_hidden(jcom.origin_snapshot(vol)) or \
(jcom.vid_from_sname(jcom.origin_snapshot(vol)) ==
jcom.idname(vname)):
self.ra.delete_snapshot(jcom.origin_volume(vol),
jcom.origin_snapshot(vol),
recursively_children=True,
force_umount=True)
def _clean_garbage_resources(self, vname, snapshots=None):
'''Removes resources that is not related to volume
Goes through volume snapshots and it clones to identify one
that is clearly not related to vname volume and therefore
have to be deleted.
:param str vname: physical volume id
:param list snapshots: list of snapshot info dictionaries
:return: updated list of snapshots
'''
if snapshots is None:
try:
snapshots = self.ra.get_snapshots(vname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s do not exists, it was already '
'deleted', vname)
return
update = False
for snap in snapshots:
if jcom.is_volume(jcom.sname_from_snap(snap)):
cvnames = jcom.snapshot_clones(snap)
if len(cvnames) == 0:
self._delete_snapshot(vname, jcom.sname_from_snap(snap))
update = True
if jcom.is_snapshot(jcom.sname_from_snap(snap)):
cvnames = jcom.snapshot_clones(snap)
for cvname in cvnames:
if jcom.is_hidden(cvname):
self._promote_newest_delete(cvname)
update = True
if jcom.is_snapshot(cvname):
if jcom.idname(vname) != jcom.vid_from_sname(cvname):
self._promote_newest_delete(cvname)
update = True
if update:
snapshots = self.ra.get_snapshots(vname)
return snapshots
def _list_busy_snapshots(self, vname, snapshots,
exclude_dedicated_volumes=False) -> list:
"""List all volume snapshots with clones
Goes through provided list of snapshots.
If additional parameters are given, will filter list of snapshots
accordingly.
Keyword arguments:
:param str vname: zvol id
:param list snapshots: list of snapshots data dicts
:param bool exclude_dedicated_volumes: list snapshots that has clones
(default False)
:return: filtered list of snapshot data dicts
:rtype: list
"""
out = []
for snap in snapshots:
clones = jcom.snapshot_clones(snap)
add = False
for cvname in clones:
if exclude_dedicated_volumes and jcom.is_volume(cvname):
continue
add = True
if add:
out.append(snap)
return out
def _clean_volume_snapshots_mount_points(self, vname, snapshots):
update = False
for snap in snapshots:
clones = jcom.snapshot_clones(snap)
for cname in [c for c in clones if jcom.is_snapshot(c)]:
update = True
self._delete_volume(cname, cascade=True)
if update:
snapshots = self.ra.get_snapshots(vname)
return snapshots
def _delete_volume(self, vname, cascade=False):
"""_delete_volume delete routine containing delete logic
:param str vname: physical volume id
:param bool cascade: flag for cascade volume deletion
with its snapshots
:return: None
"""
try:
self.ra.delete_lun(vname,
force_umount=True,
recursively_children=cascade)
except jexc.JDSSResourceIsBusyException:
LOG.debug('unable to conduct direct volume %s deletion', vname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s do not exists, it was already '
'deleted', vname)
return
except jexc.JDSSRESTException as jerr:
LOG.debug(
"Unable to delete physical volume %(volume)s "
"with error %(err)s.", {
"volume": vname,
"err": jerr})
else:
LOG.debug('in place deletion suceeded')
return
snapshots = None
try:
snapshots = self.ra.get_snapshots(vname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s do not exists, it was already '
'deleted', vname)
return
if cascade is False:
bsnaps = self._list_busy_snapshots(vname,
snapshots,
exclude_dedicated_volumes=True)
if len(bsnaps) > 0:
raise exception.VolumeIsBusy('Volume has snapshots')
snaps = self._clean_garbage_resources(vname, snapshots)
snaps = self._clean_volume_snapshots_mount_points(vname, snapshots)
self._promote_newest_delete(vname, snapshots=snaps)
def delete_volume(self, volume_name, cascade=False):
"""Delete volume
:param volume: volume reference
:param cascade: remove snapshots of a volume as well
"""
vname = jcom.vname(volume_name)
LOG.debug('deleting volume %s', vname)
self._delete_volume(vname, cascade=cascade)
def _clone_object(self, cvname, sname, ovname,
sparse=None,
create_snapshot=False):
"""Creates a clone of specified object
Will create snapshot if it is not provided
:param str cvname: clone volume name
:param str sname: snapshot name
:param str ovname: original volume name
:param bool sparse: sparse property of new volume
:param bool create_snapshot:
"""
LOG.debug('cloning %(ovname)s to %(coname)s', {
"ovname": ovname,
"coname": cvname})
if create_snapshot:
self.ra.create_snapshot(ovname, sname)
try:
self.ra.create_volume_from_snapshot(
cvname,
sname,
ovname,
sparse=sparse)
except jexc.JDSSException as jerr:
# This is a garbage collecting section responsible for cleaning
# all the mess of request failed
if create_snapshot:
try:
self.ra.delete_snapshot(ovname,
cvname,
recursively_children=True,
force_umount=True)
except jexc.JDSSException as jerrd:
LOG.warning("Because of %s physical snapshot %s of volume"
" %s have to be removed manually",
jerrd,
sname,
ovname)
raise jerr
def resize_volume(self, volume_name, new_size):
"""Extend an existing volume.
:param str volume_name: volume id
:param int new_size: volume new size in Gi
"""
LOG.debug("Extend volume:%(name)s to size:%(size)s",
{'name': volume_name, 'size': new_size})
self.ra.extend_lun(jcom.vname(volume_name),
int(new_size) * o_units.Gi)
def create_cloned_volume(self,
clone_name,
volume_name,
size,
snapshot_name=None,
sparse=False):
"""Create a clone of the specified volume.
:param str clone_name: new volume id
:param volume_name: original volume id
:param int size: size in Gi
:param str snapshot_name: openstack snapshot id to use for cloning
:param bool sparse: sparse flag
"""
cvname = jcom.vname(clone_name)
ovname = jcom.vname(volume_name)
LOG.debug('clone volume %(id)s to %(id_clone)s', {
"id": volume_name,
"id_clone": clone_name})
if snapshot_name:
sname = jcom.sname(snapshot_name, volume_name)
self._clone_object(cvname, sname, ovname,
create_snapshot=False,
sparse=sparse)
else:
sname = jcom.vname(clone_name)
self._clone_object(cvname, sname, ovname,
create_snapshot=True,
sparse=sparse)
clone_size = 0
try:
clone_size = int(self.ra.get_lun(cvname)['volsize'])
except jexc.JDSSException as jerr:
self.delete_volume(clone_name, cascade=False)
raise exception.VolumeBackendAPIException(
_("Fail in cloning volume %(vol)s to %(clone)s.") % {
'vol': volume_name, 'clone': clone_name}) from jerr
try:
if int(clone_size) < o_units.Gi * int(size):
self.resize_volume(clone_name, int(size))
except jexc.JDSSException as jerr:
# If volume can't be set to a proper size make sure to clean it
# before failing
try:
self.delete_volume(clone_name, cascade=False)
except jexc.JDSSException as jerrex:
LOG.warning("Error %s during cleaning failed volume %s",
jerrex, volume_name)
raise jerr from jerrex
def create_snapshot(self, snapshot_name, volume_name):
"""Create snapshot of existing volume.
:param str snapshot_name: new snapshot id
:param str volume_name: original volume id
"""
LOG.debug('create snapshot %(snap)s for volume %(vol)s', {
'snap': snapshot_name,
'vol': volume_name})
vname = jcom.vname(volume_name)
sname = jcom.sname(snapshot_name, volume_name)
self.ra.create_snapshot(vname, sname)
def create_export_snapshot(self, snapshot_name, volume_name,
provider_auth):
"""Creates iscsi resources needed to start using snapshot
:param str snapshot_name: openstack snapshot id
:param str volume_name: openstack volume id
:param str provider_auth: space-separated triple
'<auth method> <auth username> <auth password>'
"""
sname = jcom.sname(snapshot_name, volume_name)
ovname = jcom.vname(volume_name)
self._clone_object(sname, sname, ovname,
sparse=True,
create_snapshot=False)
try:
self._ensure_target_volume(snapshot_name, sname, provider_auth,
ro=True)
except jexc.JDSSException as jerr:
self._delete_volume(sname, cascade=True)
raise jerr
def remove_export(self, volume_name):
"""Remove iscsi target created to make volume attachable
:param str volume_name: openstack volume id
"""
vname = jcom.vname(volume_name)
try:
self._remove_target_volume(volume_name, vname)
except jexc.JDSSException as jerr:
LOG.warning(jerr)
def remove_export_snapshot(self, snapshot_name, volume_name):
"""Remove tmp vol and iscsi target created to make snap attachable
:param str snapshot_name: openstack snapshot id
:param str volume_name: openstack volume id
"""
sname = jcom.sname(snapshot_name, volume_name)
try:
self._remove_target_volume(snapshot_name, sname)
except jexc.JDSSException as jerr:
self._delete_volume(sname, cascade=True)
raise jerr
self._delete_volume(sname, cascade=True)
def _delete_snapshot(self, vname, sname):
"""Delete snapshot
This method will delete snapshot mount point and snapshot if possible
:param str vname: zvol name
:param dict snap: snapshot info dictionary
:return: None
"""
try:
self.ra.delete_snapshot(vname, sname, force_umount=True)
except jexc.JDSSResourceIsBusyException:
LOG.debug('Direct deletion of snapshot %s failed', vname)
else:
return
snap = self.ra.get_snapshot(vname, sname)
clones = jcom.snapshot_clones(snap)
busy = False
for cvname in clones:
if jcom.is_snapshot(cvname):
self._promote_newest_delete(cvname)
if jcom.is_volume(cvname):
LOG.debug('Will not delete snap %(snap)s,'
'becasue it is used by %(vol)s',
{'snap': sname,
'vol': cvname})
busy = True
if busy:
return
try:
self.ra.delete_snapshot(vname, sname, force_umount=True)
except jexc.JDSSResourceIsBusyException:
LOG.debug('Unable to delete snap %(snap)s because it is busy',
{'snap': jcom.sname_from_snap(snap)})
def delete_snapshot(self, volume_name, snapshot_name):
"""Delete snapshot of existing volume.
:param str volume_name: volume id
:param str snapshot_name: snapshot id
"""
vname = jcom.vname(volume_name)
sname = jcom.sname(snapshot_name, volume_name)
self._delete_snapshot(vname, sname)
def _ensure_target_volume(self, id, vid, provider_auth, ro=False):
"""Checks if target configured properly and volume is attached to it
:param str id: id that would be used for target naming
:param str vname: physical volume id
:param str provider_auth: space-separated triple
'<auth method> <auth username> <auth password>'
"""
LOG.debug("ensure volume %s assigned to a proper target", id)
target_name = self._get_target_name(id)
if not provider_auth:
msg = _("volume %s is missing provider_auth") % jcom.idname(id)
raise jexc.JDSSException(msg)
if not self.ra.is_target(target_name):
return self._create_target_volume(id, vid, provider_auth)
if not self.ra.is_target_lun(target_name, vid):
self._attach_target_volume(target_name, vid)
(__, auth_username, auth_secret) = provider_auth.split()
chap_cred = {"name": auth_username,
"password": auth_secret}
try:
users = self.ra.get_target_user(target_name)
if len(users) == 1:
if users[0]['name'] == chap_cred['name']:
return
self.ra.delete_target_user(
target_name,
users[0]['name'])
for user in users:
self.ra.delete_target_user(
target_name,
user['name'])
self._set_target_credentials(target_name, chap_cred)
except jexc.JDSSException as jerr:
self.ra.delete_target(target_name)
raise exception.VolumeBackendAPIException(jerr)
def _get_target_name(self, volume_id):
"""Return iSCSI target name to access volume."""
return f'{self.jovian_target_prefix}{volume_id}'
def _get_iscsi_properties(self, volume_id, provider_auth, multipath=False):
"""Return dict according to cinder/driver.py implementation.
:param volume_id: UUID of volume, might take snapshot UUID
:param str provider_auth: space-separated triple
'<auth method> <auth username> <auth password>'
:return:
"""
tname = self._get_target_name(volume_id)
iface_info = []
if multipath:
iface_info = self.get_active_ifaces()
if not iface_info:
raise exception.InvalidConfigurationValue(
_('No available interfaces '
'or config excludes them'))
iscsi_properties = {}
if multipath:
iscsi_properties['target_iqns'] = []
iscsi_properties['target_portals'] = []
iscsi_properties['target_luns'] = []
LOG.debug('tpaths %s.', iface_info)
for iface in iface_info:
iscsi_properties['target_iqns'].append(
self._get_target_name(volume_id))
iscsi_properties['target_portals'].append(
iface +
":" +
str(self.jovian_iscsi_target_portal_port))
iscsi_properties['target_luns'].append(0)
else:
iscsi_properties['target_iqn'] = tname
iscsi_properties['target_portal'] = (
self.ra.get_active_host() +
":" +
str(self.jovian_iscsi_target_portal_port))
iscsi_properties['target_discovered'] = False
if provider_auth:
(auth_method, auth_username, auth_secret) = provider_auth.split()
iscsi_properties['auth_method'] = auth_method
iscsi_properties['auth_username'] = auth_username
iscsi_properties['auth_password'] = auth_secret
iscsi_properties['target_lun'] = 0
return iscsi_properties
def _remove_target_volume(self, id, vid):
"""_remove_target_volume
Ensure that volume is not attached to target and target do not exists.
"""
target_name = self._get_target_name(id)
LOG.debug("remove export")
LOG.debug("detach volume:%(vol)s from target:%(targ)s.", {
'vol': id,
'targ': target_name})
try:
self.ra.detach_target_vol(target_name, vid)
except jexc.JDSSResourceNotFoundException as jerrrnf:
LOG.debug('failed to remove resource %(t)s because of %(err)s', {
't': target_name,
'err': jerrrnf.args[0]})
except jexc.JDSSException as jerr:
LOG.warning('failed to Terminate_connection for target %(targ)s '
'because of: %(err)s', {'targ': target_name,
'err': jerr.args[0]})
raise jerr
LOG.debug("delete target: %s", target_name)
try:
self.ra.delete_target(target_name)
except jexc.JDSSResourceNotFoundException as jerrrnf:
LOG.debug('failed to remove resource %(target)s because '
'of %(err)s',
{'target': target_name, 'err': jerrrnf.args[0]})
except jexc.JDSSException as jerr:
LOG.warning('Failed to Terminate_connection for target %(targ)s '
'because of: %(err)s ',
{'targ': target_name, 'err': jerr.args[0]})
raise jerr
def ensure_export(self, volume_id, provider_auth):
vname = jcom.vname(volume_id)
self._ensure_target_volume(volume_id, vname, provider_auth)
def initialize_connection(self, volume_id, provider_auth,
snapshot_id=None,
multipath=False):
"""Ensures volume is ready for connection and return connection data
Ensures that particular volume is ready to be used over iscsi
with credentials provided in provider_auth
If snapshot name is provided method will ensure that connection
leads to read only volume object associated with particular snapshot
:param str volume_id: Volume id string
:param str provider_auth: space-separated triple
'<auth method> <auth username> <auth password>'
:param str snapshot_id: id of snapshot that should be connected
:param bool multipath: specifies if multipath should be used
"""
id_of_disk_to_attach = volume_id
vid = jcom.vname(volume_id)
if provider_auth is None:
raise jexc.JDSSException(_("CHAP credentials missing"))
if snapshot_id:
id_of_disk_to_attach = snapshot_id
vid = jcom.sname(snapshot_id, volume_id)
iscsi_properties = self._get_iscsi_properties(id_of_disk_to_attach,
provider_auth,
multipath=multipath)
if snapshot_id:
self._ensure_target_volume(id_of_disk_to_attach,
vid,
provider_auth,
mode='ro')
else:
self._ensure_target_volume(id_of_disk_to_attach,
vid,
provider_auth)
LOG.debug(
"initialize_connection for physical disk %(vid)s with %(id)s",
{'vid': vid, 'id': id_of_disk_to_attach})
return {
'driver_volume_type': 'iscsi',
'data': iscsi_properties,
}
def _create_target_volume(self, id, vid, provider_auth):
"""Creates target and attach volume to it
:param id: uuid of particular resource
:param vid: physical volume id, might identify snapshot mount
:param str provider_auth: space-separated triple
'<auth method> <auth username> <auth password>'
:return:
"""
LOG.debug("create target and attach volume %s to it", vid)
target_name = self._get_target_name(id)
(__, auth_username, auth_secret) = provider_auth.split()
chap_cred = {"name": auth_username,
"password": auth_secret}
# Create target
self.ra.create_target(target_name, use_chap=True)
# Attach volume
self._attach_target_volume(target_name, vid)
# Set credentials
self._set_target_credentials(target_name, chap_cred)
def _attach_target_volume(self, target_name, vname):
"""Attach target to volume and handles exceptions
Attempts to set attach volume to specific target.
In case of failure will remove target.
:param target_name: name of target
:param use_chap: flag for using chap
"""
try:
self.ra.attach_target_vol(target_name, vname)
except jexc.JDSSException as jerr:
msg = ('Unable to attach volume {volume} to target {target} '
'because of {error}.')
LOG.warning(msg, {"volume": vname,
"target": target_name,
"error": jerr})
self.ra.delete_target(target_name)
raise jerr
def _set_target_credentials(self, target_name, cred):
"""Set CHAP configuration for target and handle exceptions
Attempts to set CHAP credentials for specific target.
In case of failure will remove target.
:param target_name: name of target
:param cred: CHAP user name and password
"""
try:
self.ra.create_target_user(target_name, cred)
except jexc.JDSSException as jerr:
try:
self.ra.delete_target(target_name)
except jexc.JDSSException:
pass
err_msg = (('Unable to create user %(user)s '
'for target %(target)s '
'because of %(error)s.') % {
'target': target_name,
'user': cred['name'],
'error': jerr})
LOG.error(err_msg)
raise jexc.JDSSException(_(err_msg))

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from datetime import datetime
from cinder import exception
from cinder.i18n import _
@ -30,13 +32,16 @@ def is_snapshot(name):
def idname(name):
"""Convert id into name"""
"""Extract UUID from physical volume name"""
if name.startswith(('s_', 'v_', 't_')):
if name.startswith(('v_', 't_')):
return name[2:]
if name.startswith(('s_')):
return sid_from_sname(name)
msg = _('Object name %s is incorrect') % name
raise exception.VolumeBackendAPIException(message=msg)
raise exception.VolumeDriverException(message=msg)
def vname(name):
@ -47,30 +52,46 @@ def vname(name):
if name.startswith('s_'):
msg = _('Attempt to use snapshot %s as a volume') % name
raise exception.VolumeBackendAPIException(message=msg)
raise exception.VolumeDriverException(message=msg)
if name.startswith('t_'):
msg = _('Attempt to use deleted object %s as a volume') % name
raise exception.VolumeBackendAPIException(message=msg)
raise exception.VolumeDriverException(message=msg)
return 'v_' + name
return f'v_{name}'
def sname(name):
"""Convert id into snapshot name"""
def sname_to_id(sname):
if name.startswith('s_'):
return name
spl = sname.split('_')
if name.startswith('v_'):
msg = _('Attempt to use volume %s as a snapshot') % name
raise exception.VolumeBackendAPIException(message=msg)
if len(spl) == 2:
return (spl[1], None)
if name.startswith('t_'):
msg = _('Attempt to use deleted object %s as a snapshot') % name
raise exception.VolumeBackendAPIException(message=msg)
return (spl[1], spl[2])
return 's_' + name
def sid_from_sname(name):
return sname_to_id(name)[0]
def vid_from_sname(name):
return sname_to_id(name)[1]
def sname(sid, vid):
"""Convert id into snapshot name
:param: vid: volume id
:param: sid: snapshot id
"""
if vid is None:
return 's_%(sid)s' % {'sid': sid}
return 's_%(sid)s_%(vid)s' % {'sid': sid, 'vid': vid}
def sname_from_snap(snapshot_struct):
return snapshot_struct['name']
def is_hidden(name):
@ -83,22 +104,33 @@ def is_hidden(name):
return False
def origin_snapshot(origin_str):
"""Extracts original physical snapshot name from origin record"""
return origin_str.split("@")[1]
def origin_snapshot(vol):
"""Extracts original physical snapshot name from volume dict"""
if 'origin' in vol and vol['origin'] is not None:
return vol['origin'].split("@")[1]
return None
def origin_volume(origin_str):
"""Extracts original physical volume name from origin record"""
def origin_volume(vol):
"""Extracts original physical volume name from volume dict"""
return origin_str.split("@")[0].split("/")[1]
if 'origin' in vol and vol['origin'] is not None:
return vol['origin'].split("@")[0].split("/")[1]
return None
def full_name_volume(name):
"""Get volume id from full_name"""
def snapshot_clones(snap):
"""Return list of clones associated with snapshot or return empty list"""
out = []
clones = []
if 'clones' not in snap:
return out
else:
clones = snap['clones'].split(',')
return name.split('/')[1]
for clone in clones:
out.append(clone.split('/')[1])
return out
def hidden(name):
@ -110,3 +142,14 @@ def hidden(name):
if name[:2] == 'v_' or name[:2] == 's_':
return 't_' + name[2:]
return 't_' + name
def get_newest_snapshot_name(snapshots):
newest_date = None
sname = None
for snap in snapshots:
current_date = datetime.strptime(snap['creation'], "%Y-%m-%d %H:%M:%S")
if newest_date is None or current_date > newest_date:
newest_date = current_date
sname = snap['name']
return sname

View File

@ -19,7 +19,6 @@ import re
from oslo_log import log as logging
from cinder import exception
from cinder.i18n import _
from cinder.volume.drivers.open_e.jovian_common import exception as jexc
from cinder.volume.drivers.open_e.jovian_common import rest_proxy
@ -28,23 +27,36 @@ LOG = logging.getLogger(__name__)
class JovianRESTAPI(object):
"""Jovian REST API proxy."""
"""Jovian REST API"""
def __init__(self, config):
self.pool = config.get('jovian_pool', 'Pool-0')
self.rproxy = rest_proxy.JovianRESTProxy(config)
self.rproxy = rest_proxy.JovianDSSRESTProxy(config)
self.resource_dne_msg = (
re.compile(r'^Zfs resource: .* not found in this collection\.$'))
self.resource_has_clones_msg = (
re.compile(r'^In order to delete a zvol, you must delete all of '
'its clones first.$'))
self.resource_has_clones_class = (
re.compile(r'^opene.storage.zfs.ZfsOeError$'))
self.resource_has_snapshots_msg = (
re.compile(r"^cannot destroy '.*/.*': volume has children\nuse "
"'-r' to destroy the following datasets:\n.*"))
self.resource_has_snapshots_class = (
re.compile(r'^zfslib.wrap.zfs.ZfsCmdError$'))
def _general_error(self, url, resp):
reason = "Request %s failure" % url
LOG.debug("error resp %s", resp)
if 'error' in resp:
eclass = resp.get('class', 'Unknown')
code = resp.get('code', 'Unknown')
msg = resp.get('message', 'Unknown')
eclass = resp['error'].get('class', 'Unknown')
code = resp['error'].get('code', 'Unknown')
msg = resp['error'].get('message', 'Unknown')
reason = _("Request to %(url)s failed with code: %(code)s "
"of type:%(eclass)s reason:%(message)s")
@ -119,8 +131,11 @@ class JovianRESTAPI(object):
:param volume_name:
:param volume_size:
:param sparse: thin or thick volume flag
:param block_size: size of block
:return:
"""
LOG.debug("create volume start")
volume_size_str = str(volume_size)
jbody = {
'name': volume_name,
@ -190,13 +205,11 @@ class JovianRESTAPI(object):
return False
def get_lun(self, volume_name):
"""get_lun.
"""get_lun
GET /volumes/<volume_name>
:param volume_name:
:return:
{
"data":
:param volume_name: zvol id
:return: volume dict
{
"origin": null,
"referenced": "65536",
@ -230,9 +243,7 @@ class JovianRESTAPI(object):
"name": "v1",
"checksum": "on",
"refreservation": "1076101120"
},
"error": null
}
}
"""
req = '/volumes/' + volume_name
@ -252,8 +263,8 @@ class JovianRESTAPI(object):
def modify_lun(self, volume_name, prop=None):
"""Update volume properties
:prop volume_name: volume name
:prop prop: dictionary
:param volume_name: volume name
:param prop: dictionary
{
<property>: <value>
}
@ -287,8 +298,8 @@ class JovianRESTAPI(object):
def modify_property_lun(self, volume_name, prop=None):
"""Change volume properties
:prop: volume_name: volume name
:prop: prop: dictionary of volume properties in format
:param volume_name: volume name
:param prop: dictionary of volume properties in format
{ "property_name": "<name of property>",
"property_value":"<value of a property>"}
"""
@ -311,9 +322,33 @@ class JovianRESTAPI(object):
reason=resp['error']['message'])
raise jexc.JDSSRESTException(request=req, reason="unknown")
def promote(self, volume_name, snapshot_name, clone_name):
"""promote volume
POST /volumes/<volumename>/snapshots/<snapshotname>/clones/
<clonename>/promoteclone_promote
:param volume_name: parent volume for the one that should be promoted
:param snapshot_name: snapshot that is linking parent and clone
:param clone_name: volume name that is going to be promoted
:return:
"""
jbody = {}
req = '/volumes/' + volume_name + \
'/snapshots/' + snapshot_name + \
'/clones/' + clone_name + '/promote'
LOG.debug("promote clone %s", clone_name)
resp = self.rproxy.pool_request('POST', req, json_data=jbody)
if resp["code"] == 200:
LOG.debug("clone %s promoted", clone_name)
return
self._general_error(req, resp)
def delete_lun(self, volume_name,
recursively_children=False,
recursively_dependents=False,
force_umount=False):
"""delete_volume.
@ -325,15 +360,12 @@ class JovianRESTAPI(object):
if recursively_children:
jbody['recursively_children'] = True
if recursively_dependents:
jbody['recursively_dependents'] = True
if force_umount:
jbody['force_umount'] = True
req = '/volumes/' + volume_name
LOG.debug(("delete volume:%(vol)s "
"recursively children:%(args)s"),
"args:%(args)s"),
{'vol': volume_name,
'args': jbody})
@ -357,10 +389,20 @@ class JovianRESTAPI(object):
# Handle volume busy
if resp["code"] == 500 and resp["error"]:
if resp["error"]["errno"] == 1000:
LOG.warning(
"volume %s is busy", volume_name)
raise exception.VolumeIsBusy(volume_name=volume_name)
if 'message' in resp['error'] and \
'class' in resp['error']:
if self.resource_has_clones_msg.match(
resp['error']['message']) and \
self.resource_has_clones_class.match(
resp['error']['class']):
LOG.warning("volume %s is busy", volume_name)
raise jexc.JDSSResourceIsBusyException(res=volume_name)
if self.resource_has_snapshots_msg.match(
resp['error']['message']) and \
self.resource_has_snapshots_class.match(
resp['error']['class']):
LOG.warning("volume %s is busy", volume_name)
raise jexc.JDSSResourceIsBusyException(res=volume_name)
raise jexc.JDSSRESTException('Failed to delete volume.')
@ -562,17 +604,27 @@ class JovianRESTAPI(object):
self._general_error(req, resp)
def attach_target_vol(self, target_name, lun_name, lun_id=0):
def attach_target_vol(self, target_name, lun_name,
lun_id=0,
mode=None):
"""attach_target_vol.
POST /san/iscsi/targets/<target_name>/luns
:param target_name:
:param lun_name:
:param target_name: name of the target
:param lun_name: phisical volume name to be attached
:param lun_id: id that would be assigned to volume
:param mode: one of "wt", "wb" or "ro"
:return:
"""
req = '/san/iscsi/targets/%s/luns' % target_name
jbody = {"name": lun_name, "lun": lun_id}
if mode is not None:
if mode in ['wt', 'wb', 'ro']:
jbody['mode'] = mode
else:
raise jexc.JDSSException(
_("Incoret mode for target %s" % mode))
LOG.debug("atach volume %(vol)s to target %(tar)s",
{'vol': lun_name,
'tar': target_name})
@ -671,6 +723,9 @@ class JovianRESTAPI(object):
if 'sparse' in options:
jbody['sparse'] = options['sparse']
if 'ro' in options:
jbody['ro'] = options['sparse']
LOG.debug("create volume %(vol)s from snapshot %(snap)s",
{'vol': volume_name,
'snap': snapshot_name})
@ -722,11 +777,42 @@ class JovianRESTAPI(object):
self._general_error(req, resp)
def count_rollback_dependents(self, volume_name, snapshot_name):
"""Count volumes and snapshots affected by rollback
GET /volumes/<volume_name>/snapshots/<snapshot_name>/rollback
:param str volume_name: volume that is going to be reverted
:param str snapshot_name: snapshot of a volume above
:return: None
"""
req = ('/volumes/%(vol)s/snapshots/'
'%(snap)s/rollback') % {'vol': volume_name,
'snap': snapshot_name}
LOG.debug("get rollback count for volume %(vol)s to snapshot %(snap)s",
{'vol': volume_name,
'snap': snapshot_name})
resp = self.rproxy.pool_request('GET', req)
if not resp["error"] and resp["code"] == 200:
return resp['data']
if resp["code"] == 500:
if resp["error"]:
if resp["error"]["errno"] == 1:
raise jexc.JDSSResourceNotFoundException(
res="%(vol)s@%(snap)s" % {'vol': volume_name,
'snap': snapshot_name})
self._general_error(req, resp)
def delete_snapshot(self,
volume_name,
snapshot_name,
recursively_children=False,
recursively_dependents=False,
force_umount=False):
"""delete_snapshot.
@ -756,17 +842,10 @@ class JovianRESTAPI(object):
if recursively_children:
jbody['recursively_children'] = True
if recursively_dependents:
jbody['recursively_dependents'] = True
if force_umount:
jbody['force_umount'] = True
resp = dict()
if len(jbody) > 0:
resp = self.rproxy.pool_request('DELETE', req, json_data=jbody)
else:
resp = self.rproxy.pool_request('DELETE', req)
resp = self.rproxy.pool_request('DELETE', req, json_data=jbody)
if resp["code"] in (200, 201, 204):
LOG.debug("snapshot %s deleted", snapshot_name)
@ -779,6 +858,25 @@ class JovianRESTAPI(object):
snapshot=snapshot_name)
self._general_error(req, resp)
def get_snapshot(self, volume_name, snapshot_name):
req = (('/volumes/%(vol)s/snapshots/%(snap)s') %
{'vol': volume_name, 'snap': snapshot_name})
LOG.debug("get snapshots for volume %s ", volume_name)
resp = self.rproxy.pool_request('GET', req)
if not resp["error"] and resp["code"] == 200:
return resp["data"]
if resp['code'] == 500:
if 'message' in resp['error']:
if self.resource_dne_msg.match(resp['error']['message']):
raise jexc.JDSSResourceNotFoundException(volume_name)
self._general_error(req, resp)
def get_snapshots(self, volume_name):
"""get_snapshots.

View File

@ -31,8 +31,8 @@ from cinder.volume.drivers.open_e.jovian_common import exception as jexc
LOG = logging.getLogger(__name__)
class JovianRESTProxy(object):
"""Jovian REST API proxy."""
class JovianDSSRESTProxy(object):
"""Jovian REST API proxy"""
def __init__(self, config):
""":param config: list of config values."""
@ -62,7 +62,7 @@ class JovianRESTProxy(object):
self.user = config.get('san_login', 'admin')
self.password = config.get('san_password', 'admin')
self.verify = config.get('driver_ssl_cert_verify', True)
self.cert = config.get('driver_ssl_cert_path')
self.cert = config.get('driver_ssl_cert_path', None)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@ -76,7 +76,7 @@ class JovianRESTProxy(object):
session.headers.update({'Connection': 'keep-alive',
'Content-Type': 'application/json',
'Authorization': 'Basic'})
session.hooks['response'] = [JovianRESTProxy._handle_500]
session.hooks['response'] = [JovianDSSRESTProxy._handle_500]
session.verify = self.verify
if self.verify and self.cert:
session.verify = self.cert
@ -101,34 +101,36 @@ class JovianRESTProxy(object):
"""Send request to the specific url.
:param request_method: GET, POST, DELETE
:param url: where to send
:param req: where to send
:param json_data: data
"""
out = None
for i in range(len(self.hosts)):
try:
addr = "%(base)s%(req)s" % {'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)
for i in range(3):
for i in range(len(self.hosts)):
try:
addr = "%(base)s%(req)s" % {'base': self._get_base_url(),
'req': req}
LOG.debug("Sending %(t)s to %(addr)s data %(data)s",
{'t': request_method,
'addr': addr,
'data': json_data})
r = None
if json_data:
r = requests.Request(request_method,
addr,
data=json.dumps(json_data))
else:
r = requests.Request(request_method, addr)
pr = self.session.prepare_request(r)
out = self._send(pr)
except requests.exceptions.ConnectionError:
self._next_host()
continue
break
pr = self.session.prepare_request(r)
out = self._send(pr)
except requests.exceptions.ConnectionError:
self._next_host()
continue
LOG.debug("Geting %(data)s from %(t)s to %(addr)s",
{'data': out, 't': request_method, 'addr': addr})
return out
LOG.debug("Geting %(data)s from %(t)s to %(addr)s",
{'data': out, 't': request_method, 'addr': addr})
return out
def pool_request(self, request_method, req, json_data=None):
"""Send request to the specific url.
@ -143,28 +145,25 @@ class JovianRESTProxy(object):
{'t': request_method, 'addr': addr})
return self.request(request_method, req, json_data=json_data)
@retry((requests.exceptions.ConnectionError,
jexc.JDSSOSException),
interval=2,
backoff_rate=2,
retries=7)
@retry(json.JSONDecodeError,
retries=3)
def _send(self, pr):
"""Send prepared request
:param pr: prepared request
"""
ret = dict()
ret = {}
response_obj = self.session.send(pr)
ret['code'] = response_obj.status_code
if ret['code'] == 204:
ret["data"] = None
return ret
try:
data = json.loads(response_obj.text)
ret["error"] = data.get("error")
ret["data"] = data.get("data")
except json.JSONDecodeError:
pass
data = json.loads(response_obj.text)
ret["error"] = data.get("error")
ret["data"] = data.get("data")
return ret

View File

@ -932,7 +932,7 @@ driver.netapp_ontap=complete
driver.netapp_solidfire=complete
driver.nexenta=missing
driver.nfs=missing
driver.opene_joviandss=complete
driver.opene_joviandss=missing
driver.prophetstor=missing
driver.pure=complete
driver.qnap=missing

View File

@ -0,0 +1,9 @@
---
features:
- |
Open-E JovianDSS driver: revert-to-snapshot has been removed.
other:
- |
Open-E JovianDSS driver: general rework of volume and snapshot creation and deletion.
- |
Open-E JovianDSS driver: network interfaces selection on JovianDSS storage has been reworked.