Merge "Add Cinder driver for Open-E JovianDSS data storage"

This commit is contained in:
Zuul 2020-10-15 18:55:35 +00:00 committed by Gerrit Code Review
commit 27959daf2b
14 changed files with 5002 additions and 0 deletions

View File

@ -139,6 +139,8 @@ from cinder.volume.drivers.nexenta import options as \
cinder_volume_drivers_nexenta_options
from cinder.volume.drivers import nfs as cinder_volume_drivers_nfs
from cinder.volume.drivers import nimble as cinder_volume_drivers_nimble
from cinder.volume.drivers.open_e import options as \
cinder_volume_drivers_open_e_options
from cinder.volume.drivers.prophetstor import options as \
cinder_volume_drivers_prophetstor_options
from cinder.volume.drivers import pure as cinder_volume_drivers_pure
@ -268,6 +270,9 @@ def list_opts():
instorage_mcs_opts,
cinder_volume_drivers_inspur_instorage_instorageiscsi.
instorage_mcs_iscsi_opts,
cinder_volume_drivers_open_e_options.jdss_connection_opts,
cinder_volume_drivers_open_e_options.jdss_iscsi_opts,
cinder_volume_drivers_open_e_options.jdss_volume_opts,
cinder_volume_drivers_sandstone_sdsdriver.sds_opts,
cinder_volume_drivers_veritas_access_veritasiscsi.VA_VOL_OPTS,
cinder_volume_manager.volume_manager_opts,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,997 @@
# 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.
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
from cinder.volume.drivers.open_e.jovian_common import rest
UUID_1 = '12345678-1234-1234-1234-000000000001'
UUID_2 = '12345678-1234-1234-1234-000000000002'
CONFIG_OK = {
'san_hosts': ['192.168.0.2'],
'san_api_port': 82,
'driver_use_ssl': 'https',
'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'
}
def fake_safe_get(value):
return CONFIG_OK[value]
class TestOpenEJovianRESTAPI(test.TestCase):
def get_rest(self, config):
ctx = context.get_admin_context()
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)
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)
jrest.rproxy.get_active_host.return_value = "test_data"
ret = jrest.get_active_host()
self.assertEqual("test_data", ret)
def test_is_pool_exists(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {'code': 200,
'error': None}
jrest.rproxy.pool_request.return_value = resp
self.assertTrue(jrest.is_pool_exists())
err = {'errorid': 12}
resp = {'code': 404,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertFalse(jrest.is_pool_exists())
pool_request_expected = [
mock.call('GET', ''),
mock.call('GET', '')]
jrest.rproxy.pool_request.assert_has_calls(pool_request_expected)
def get_iface_info(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {
'code': 200,
'error': None}
jrest.rproxy.pool_request.return_value = resp
self.assertTrue(jrest.is_pool_exists())
def test_get_luns(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {'data': [{
'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'}],
'error': None,
'code': 200}
jrest.rproxy.pool_request.return_value = resp
self.assertEqual(resp['data'], jrest.get_luns())
err = {'errorid': 12, 'message': 'test failure'}
resp = {'code': 404,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(jexc.JDSSRESTException, jrest.get_luns)
get_luns_expected = [
mock.call('GET', "/volumes"),
mock.call('GET', "/volumes")]
jrest.rproxy.pool_request.assert_has_calls(get_luns_expected)
def test_create_lun(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {'data': {
'vscan': None,
'full_name': 'pool-0/' + jcom.vname(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'},
'error': None,
'code': 200}
jbody = {
'name': jcom.vname(UUID_1),
'size': "1073741824",
'sparse': False
}
jbody_sparse = {
'name': jcom.vname(UUID_1),
'size': "1073741824",
'sparse': True
}
jrest.rproxy.pool_request.return_value = resp
self.assertIsNone(jrest.create_lun(jcom.vname(UUID_1), o_units.Gi))
err = {'errno': '5', 'message': 'test failure'}
resp = {'code': 404,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(jexc.JDSSRESTException,
jrest.create_lun,
jcom.vname(UUID_1),
o_units.Gi,
sparse=True)
addr = "/volumes"
create_lun_expected = [
mock.call('POST', addr, json_data=jbody),
mock.call('POST', addr, json_data=jbody_sparse)]
jrest.rproxy.pool_request.assert_has_calls(create_lun_expected)
def test_extend_lun(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {'data': None,
'error': None,
'code': 201}
jbody = {
'size': "2147483648",
}
jrest.rproxy.pool_request.return_value = resp
self.assertIsNone(jrest.extend_lun(jcom.vname(UUID_1), 2 * o_units.Gi))
err = {'message': 'test failure'}
resp = {'code': 500,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(jexc.JDSSRESTException,
jrest.extend_lun,
jcom.vname(UUID_1),
2 * o_units.Gi)
addr = "/volumes/" + jcom.vname(UUID_1)
create_lun_expected = [
mock.call('PUT', addr, json_data=jbody),
mock.call('PUT', addr, json_data=jbody)]
jrest.rproxy.pool_request.assert_has_calls(create_lun_expected)
def test_is_lun(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {'data': {
"vscan": None,
"full_name": "pool-0/" + jcom.vname(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"},
'error': None,
'code': 200}
jrest.rproxy.pool_request.return_value = resp
self.assertTrue(jrest.is_lun(jcom.vname(UUID_1)))
err = {'errno': 1,
'message': ('Zfs resource: Pool-0/' + jcom.vname(UUID_1) +
' not found in this collection.')}
resp = {'code': 500,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertEqual(False, jrest.is_lun(jcom.vname(UUID_1)))
jrest.rproxy.pool_request.side_effect = (
jexc.JDSSRESTProxyException(host='test_host', reason='test'))
self.assertRaises(jexc.JDSSRESTProxyException,
jrest.is_lun,
'v_' + UUID_1)
def test_get_lun(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {'data': {"vscan": None,
"full_name": "pool-0/v_" + 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"},
'error': None,
'code': 200}
jrest.rproxy.pool_request.return_value = resp
self.assertEqual(resp['data'], jrest.get_lun('v_' + UUID_1))
err = {'errno': 1,
'message': ('Zfs resource: Pool-0/v_' + UUID_1 +
' not found in this collection.')}
resp = {'code': 500,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.get_lun,
'v_' + UUID_1)
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.get_lun,
'v_' + UUID_1)
err = {'errno': 10,
'message': ('Test error')}
resp = {'code': 500,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(jexc.JDSSException, jrest.get_lun, 'v_' + UUID_1)
def test_modify_lun(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {'data': None,
'error': None,
'code': 201}
req = {'name': 'v_' + UUID_2}
jrest.rproxy.pool_request.return_value = resp
self.assertIsNone(jrest.modify_lun('v_' + UUID_1, prop=req))
err = {'errno': 1,
'message': ('Zfs resource: Pool-0/v_' + UUID_1 +
' not found in this collection.')}
resp = {'code': 500,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.modify_lun,
'v_' + UUID_1,
prop=req)
err = {'errno': 10,
'message': ('Test error')}
resp = {'code': 500,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(jexc.JDSSException,
jrest.modify_lun,
'v_' + UUID_1,
prop=req)
addr = "/volumes/v_" + UUID_1
modify_lun_expected = [
mock.call('PUT', addr, json_data=req),
mock.call('PUT', addr, json_data=req),
mock.call('PUT', addr, json_data=req)]
jrest.rproxy.pool_request.assert_has_calls(modify_lun_expected)
def test_make_readonly_lun(self):
jrest, ctx = self.get_rest(CONFIG_OK)
resp = {'data': None,
'error': None,
'code': 201}
req = {'property_name': 'readonly', 'property_value': 'on'}
jrest.rproxy.pool_request.return_value = resp
self.assertIsNone(jrest.modify_lun('v_' + UUID_1, prop=req))
addr = "/volumes/v_" + UUID_1
modify_lun_expected = [mock.call('PUT', addr, json_data=req)]
jrest.rproxy.pool_request.assert_has_calls(modify_lun_expected)
def test_delete_lun(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# Delete OK
resp = {'data': None,
'error': None,
'code': 204}
jrest.rproxy.pool_request.return_value = resp
self.assertIsNone(jrest.delete_lun('v_' + UUID_1))
addr = "/volumes/v_" + UUID_1
delete_lun_expected = [mock.call('DELETE', addr)]
jrest.rproxy.pool_request.assert_has_calls(delete_lun_expected)
# No volume to delete
err = {'errno': 1,
'message': ('Zfs resource: Pool-0/v_' + UUID_1 +
' not found in this collection.')}
resp = {'code': 500,
'data': None,
'error': err}
jrest.rproxy.pool_request.return_value = resp
self.assertIsNone(jrest.delete_lun('v_' + UUID_1))
delete_lun_expected += [mock.call('DELETE', addr)]
jrest.rproxy.pool_request.assert_has_calls(delete_lun_expected)
# Volume has snapshots
msg = ("cannot destroy 'Pool-0/{vol}': volume has children\nuse '-r'"
" to destroy the following datasets:\nPool-0/{vol}@s1")
msg = msg.format(vol='v_' + UUID_1)
url = "http://192.168.0.2:82/api/v3/pools/Pool-0/volumes/" + UUID_1
err = {"class": "zfslib.wrap.zfs.ZfsCmdError",
"errno": 1000,
"message": msg,
"url": url}
resp = {
'code': 500,
'data': None,
'error': err}
delete_lun_expected += [mock.call('DELETE', addr)]
jrest.rproxy.pool_request.return_value = resp
self.assertRaises(
exception.VolumeIsBusy,
jrest.delete_lun,
'v_' + UUID_1)
jrest.rproxy.pool_request.assert_has_calls(delete_lun_expected)
def test_delete_lun_args(self):
jrest, ctx = self.get_rest(CONFIG_OK)
addr = "/volumes/v_" + UUID_1
# Delete OK
resp = {'data': None,
'error': None,
'code': 204}
req = {'recursively_children': True,
'recursively_dependents': True,
'force_umount': True}
delete_lun_expected = [mock.call('DELETE', addr, json_data=req)]
jrest.rproxy.pool_request.return_value = resp
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)
def test_is_target(self):
jrest, ctx = self.get_rest(CONFIG_OK)
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
addr = '/san/iscsi/targets/{}'.format(tname)
data = {'incoming_users_active': True,
'name': tname,
'allow_ip': [],
'outgoing_user': None,
'active': True,
'conflicted': False,
'deny_ip': []}
resp = {'data': data,
'error': None,
'code': 200}
is_target_expected = [mock.call('GET', addr)]
jrest.rproxy.pool_request.return_value = resp
self.assertTrue(jrest.is_target(tname))
msg = "Target {} not exists.".format(tname)
url = ("http://{addr}:{port}/api/v3/pools/Pool-0/"
"san/iscsi/targets/{target}")
url = url.format(addr=CONFIG_OK['san_hosts'][0],
port=CONFIG_OK['san_api_port'],
target=tname)
err = {"class": "opene.exceptions.ItemNotFoundError",
"message": msg,
"url": url}
resp = {'data': None,
'error': err,
'code': 404}
is_target_expected += [mock.call('GET', addr)]
jrest.rproxy.pool_request.return_value = resp
self.assertEqual(False, jrest.is_target(tname))
jrest.rproxy.pool_request.assert_has_calls(is_target_expected)
def test_create_target(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# Create OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
addr = '/san/iscsi/targets'
data = {'incoming_users_active': True,
'name': tname,
'allow_ip': [],
'outgoing_user': None,
'active': True,
'conflicted': False,
'deny_ip': []}
resp = {'data': data,
'error': None,
'code': 201}
req = {'name': tname,
'active': True,
'incoming_users_active': True}
jrest.rproxy.pool_request.return_value = resp
create_target_expected = [mock.call('POST', addr, json_data=req)]
self.assertIsNone(jrest.create_target(tname))
# Target exists
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
addr = '/san/iscsi/targets'
data = {'incoming_users_active': True,
'name': tname,
'allow_ip': [],
'outgoing_user': None,
'active': True,
'conflicted': False,
'deny_ip': []}
resp = {'data': data,
'error': None,
'code': 201}
url = ("http://{addr}:{port}/api/v3/pools/Pool-0/"
"san/iscsi/targets")
url = url.format(addr=CONFIG_OK['san_hosts'][0],
port=CONFIG_OK['san_api_port'])
msg = "Target with name {} is already present on Pool-0.".format(tname)
err = {"class": "opene.san.target.base.iscsi.TargetNameConflictError",
"message": msg,
"url": url}
resp = {'data': None,
'error': err,
'code': 409}
jrest.rproxy.pool_request.return_value = resp
create_target_expected += [mock.call('POST', addr, json_data=req)]
self.assertRaises(jexc.JDSSResourceExistsException,
jrest.create_target, tname)
# Unknown error
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
addr = "/san/iscsi/targets"
resp = {'data': data,
'error': None,
'code': 500}
url = ("http://{addr}:{port}/api/v3/pools/Pool-0/"
"san/iscsi/targets")
url = url.format(addr=CONFIG_OK['san_hosts'][0],
port=CONFIG_OK['san_api_port'])
msg = "Target with name {} faced some fatal failure.".format(tname)
err = {"class": "some test error",
"message": msg,
"url": url,
"errno": 123}
resp = {'data': None,
'error': err,
'code': 500}
jrest.rproxy.pool_request.return_value = resp
create_target_expected += [mock.call('POST', addr, json_data=req)]
self.assertRaises(jexc.JDSSException,
jrest.create_target, tname)
jrest.rproxy.pool_request.assert_has_calls(create_target_expected)
def test_delete_target(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# Delete OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
addr = '/san/iscsi/targets/{}'.format(tname)
resp = {'data': None,
'error': None,
'code': 204}
jrest.rproxy.pool_request.return_value = resp
delete_target_expected = [mock.call('DELETE', addr)]
self.assertIsNone(jrest.delete_target(tname))
# Delete no such target
url = ("http://{addr}:{port}/api/v3/pools/Pool-0/"
"san/iscsi/targets")
url = url.format(addr=CONFIG_OK['san_hosts'][0],
port=CONFIG_OK['san_api_port'])
err = {"class": "opene.exceptions.ItemNotFoundError",
"message": "Target {} not exists.".format(tname),
"url": url}
resp = {'data': None,
'error': err,
'code': 404}
jrest.rproxy.pool_request.return_value = resp
delete_target_expected += [mock.call('DELETE', addr)]
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.delete_target, tname)
# Delete unknown error
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_target_expected += [mock.call('DELETE', addr)]
self.assertRaises(jexc.JDSSException,
jrest.delete_target, tname)
jrest.rproxy.pool_request.assert_has_calls(delete_target_expected)
def test_create_target_user(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# Modify OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
addr = '/san/iscsi/targets/{}/incoming-users'.format(tname)
chap_cred = {"name": "chapuser",
"password": "123456789012"}
resp = {'data': None,
'error': None,
'code': 201}
jrest.rproxy.pool_request.return_value = resp
expected = [mock.call('POST', addr, json_data=chap_cred)]
self.assertIsNone(jrest.create_target_user(tname, chap_cred))
# No such target
url = ("http://{addr}:{port}/api/v3/pools/Pool-0/"
"san/iscsi/targets")
url = url.format(addr=CONFIG_OK['san_hosts'][0],
port=CONFIG_OK['san_api_port'])
err = {"class": "opene.exceptions.ItemNotFoundError",
"message": "Target {} not exists.".format(tname),
"url": url}
resp = {'data': None,
'error': err,
'code': 404}
jrest.rproxy.pool_request.return_value = resp
expected += [mock.call('POST', addr, json_data=chap_cred)]
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.create_target_user, tname, chap_cred)
# Unknown error
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
expected += [mock.call('POST', addr, json_data=chap_cred)]
self.assertRaises(jexc.JDSSException,
jrest.create_target_user, tname, chap_cred)
jrest.rproxy.pool_request.assert_has_calls(expected)
def test_get_target_user(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# Get OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
addr = '/san/iscsi/targets/{}/incoming-users'.format(tname)
chap_users = {"name": "chapuser"}
resp = {'data': chap_users,
'error': None,
'code': 200}
jrest.rproxy.pool_request.return_value = resp
get_target_user_expected = [mock.call('GET', addr)]
self.assertEqual(chap_users, jrest.get_target_user(tname))
# No such target
url = ("http://{addr}:{port}/api/v3/pools/Pool-0/"
"san/iscsi/targets")
url = url.format(addr=CONFIG_OK['san_hosts'][0],
port=CONFIG_OK['san_api_port'])
err = {"class": "opene.exceptions.ItemNotFoundError",
"message": "Target {} not exists.".format(tname),
"url": url}
resp = {'data': None,
'error': err,
'code': 404}
jrest.rproxy.pool_request.return_value = resp
get_target_user_expected += [mock.call('GET', addr)]
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.get_target_user, tname)
# Unknown error
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_target_user_expected += [mock.call('GET', addr)]
self.assertRaises(jexc.JDSSException,
jrest.get_target_user, tname)
jrest.rproxy.pool_request.assert_has_calls(get_target_user_expected)
def test_delete_target_user(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# Delete OK
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
user = "chapuser"
addr = '/san/iscsi/targets/{}/incoming-users/chapuser'.format(tname)
resp = {'data': None,
'error': None,
'code': 204}
jrest.rproxy.pool_request.return_value = resp
delete_target_user_expected = [mock.call('DELETE', addr)]
self.assertIsNone(jrest.delete_target_user(tname, user))
# No such user
url = ("http://{addr}:{port}/api/v3/pools/Pool-0/"
"san/iscsi/targets/{tname}/incoming-user/{chapuser}")
url = url.format(addr=CONFIG_OK['san_hosts'][0],
port=CONFIG_OK['san_api_port'],
tname=tname,
chapuser=user)
err = {"class": "opene.exceptions.ItemNotFoundError",
"message": "User {} not exists.".format(user),
"url": url}
resp = {'data': None,
'error': err,
'code': 404}
jrest.rproxy.pool_request.return_value = resp
delete_target_user_expected += [mock.call('DELETE', addr)]
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.delete_target_user, tname, user)
# Unknown error
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_target_user_expected += [mock.call('DELETE', addr)]
self.assertRaises(jexc.JDSSException,
jrest.delete_target_user, tname, user)
jrest.rproxy.pool_request.assert_has_calls(delete_target_user_expected)
def test_is_target_lun(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# lun present
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
vname = jcom.vname(UUID_1)
addr = '/san/iscsi/targets/{target}/luns/{lun}'.format(
target=tname, lun=vname)
data = {
"block_size": 512,
"device_handler": "vdisk_fileio",
"lun": 0,
"mode": "wt",
"name": vname,
"prod_id": "Storage",
"scsi_id": "99e2c883331edf87"}
resp = {'data': data,
'error': None,
'code': 200}
jrest.rproxy.pool_request.return_value = resp
is_target_lun_expected = [mock.call('GET', addr)]
self.assertTrue(jrest.is_target_lun(tname, vname))
url = "http://{ip}:{port}/api/v3/pools/Pool-0{addr}"
url = url.format(ip=CONFIG_OK['san_hosts'][0],
port=CONFIG_OK['san_api_port'],
tname=tname,
addr=addr)
msg = "volume name {lun} is not attached to target {target}"
msg = msg.format(lun=vname, target=tname)
err = {"class": "opene.exceptions.ItemNotFoundError",
"message": msg,
"url": url}
resp = {'data': None,
'error': err,
'code': 404}
jrest.rproxy.pool_request.return_value = resp
is_target_lun_expected += [mock.call('GET', addr)]
self.assertEqual(False, jrest.is_target_lun(tname, vname))
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
is_target_lun_expected += [mock.call('GET', addr)]
self.assertRaises(jexc.JDSSException,
jrest.is_target_lun, tname, vname)
jrest.rproxy.pool_request.assert_has_calls(is_target_lun_expected)
def test_attach_target_vol(self):
jrest, ctx = self.get_rest(CONFIG_OK)
# attach ok
tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1
vname = jcom.vname(UUID_1)
addr = '/san/iscsi/targets/{}/luns'.format(tname)
jbody = {"name": vname, "lun": 0}
data = {"block_size": 512,
"device_handler": "vdisk_fileio",
"lun": 0,
"mode": "wt",
"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 attached already
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",
"message": msg,
"url": url}
resp = {'data': None,
'error': err,
'code': 409}
jrest.rproxy.pool_request.return_value = resp
attach_target_vol_expected += [
mock.call('POST', addr, json_data=jbody)]
self.assertRaises(jexc.JDSSResourceExistsException,
jrest.attach_target_vol, tname, vname)
# no such target
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",
"message": msg,
"url": url}
resp = {'data': None,
'error': err,
'code': 404}
jrest.rproxy.pool_request.return_value = resp
attach_target_vol_expected += [
mock.call('POST', addr, json_data=jbody)]
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.attach_target_vol, tname, vname)
# error unknown
url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr)
msg = 'Target {} not exists.'.format(vname)
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
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)
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
vname = jcom.vname(UUID_1)
addr = '/san/iscsi/targets/{tar}/luns/{vol}'.format(
tar=tname, vol=vname)
resp = {'data': None,
'error': None,
'code': 204}
jrest.rproxy.pool_request.return_value = resp
detach_target_vol_expected = [
mock.call('DELETE', addr)]
self.assertIsNone(jrest.detach_target_vol(tname, vname))
# no such target
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",
"message": msg,
"url": url}
resp = {'data': None,
'error': err,
'code': 404}
jrest.rproxy.pool_request.return_value = resp
detach_target_vol_expected += [
mock.call('DELETE', addr)]
self.assertRaises(jexc.JDSSResourceNotFoundException,
jrest.detach_target_vol, tname, vname)
# error unknown
url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr)
msg = 'Target {} not exists.'.format(vname)
err = {"class": "some test error",
"message": "test error message",
"url": url,
"errno": 125}
resp = {'data': None,
'error': err,
'code': 500}
jrest.rproxy.pool_request.return_value = resp
detach_target_vol_expected += [
mock.call('DELETE', addr)]
self.assertRaises(jexc.JDSSException,
jrest.detach_target_vol, tname, vname)
jrest.rproxy.pool_request.assert_has_calls(detach_target_vol_expected)

View File

View File

@ -0,0 +1,975 @@
# 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.
"""iSCSI volume driver for JovianDSS driver."""
import math
import string
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 import interface
from cinder.volume import driver
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
from cinder.volume.drivers.open_e import options
from cinder.volume.drivers.san import san
from cinder.volume import volume_utils
LOG = logging.getLogger(__name__)
@interface.volumedriver
class JovianISCSIDriver(driver.ISCSIDriver):
"""Executes volume driver commands on Open-E JovianDSS V7.
Version history:
.. code-block:: none
1.0.0 - Open-E JovianDSS driver with basic functionality
"""
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Open-E_JovianDSS_CI"
VERSION = "1.0.0"
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.ra = None
@property
def backend_name(self):
"""Return backend name."""
backend_name = None
if self.configuration:
backend_name = self.configuration.safe_get('volume_backend_name')
if not backend_name:
backend_name = self.__class__.__name__
return backend_name
def do_setup(self, context):
"""Any initialization the volume driver does while starting."""
self.configuration.append_config_values(
options.jdss_connection_opts)
self.configuration.append_config_values(
options.jdss_iscsi_opts)
self.configuration.append_config_values(
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.jovian_target_prefix = self.configuration.safe_get(
'target_prefix')
self.jovian_chap_pass_len = self.configuration.safe_get(
'chap_password_len')
self.block_size = (
self.configuration.safe_get('jovian_block_size'))
self.jovian_sparse = (
self.configuration.safe_get('san_thin_provision'))
self.jovian_ignore_tpath = self.configuration.get(
'jovian_ignore_tpath', None)
self.jovian_hosts = self.configuration.safe_get(
'san_hosts')
self.ra = rest.JovianRESTAPI(self.configuration)
def check_for_setup_error(self):
"""Verify that the pool exists."""
if len(self.jovian_hosts) == 0:
msg = _("No hosts provided in configuration")
raise exception.VolumeDriverException(msg)
if not self.ra.is_pool_exists():
msg = (_("Unable to identify pool %s") % self._pool)
raise exception.VolumeDriverException(msg)
def _get_target_name(self, volume_name):
"""Return iSCSI target name to access volume."""
return '%s%s' % (self.jovian_target_prefix, volume_name)
def _get_active_ifaces(self):
"""Return list of ip addreses for iSCSI connection"""
return self.jovian_hosts
def create_volume(self, volume):
"""Create a volume.
:param volume: volume reference
:return: model update dict for volume reference
"""
vname = jcom.vname(volume.id)
LOG.debug('creating volume %s.', vname)
provider_location = self._get_provider_location(volume.id)
provider_auth = self._get_provider_auth()
try:
self.ra.create_lun(vname,
volume.size * o_units.Gi,
sparse=self.jovian_sparse,
block_size=self.block_size)
except jexc.JDSSException as ex:
LOG.error("Create volume error. Because %(err)s",
{"err": ex})
raise exception.VolumeBackendAPIException(
_('Failed to create volume %s.') % volume.id)
ret = {}
if provider_auth is not None:
ret['provider_auth'] = provider_auth
ret['provider_location'] = provider_location
return ret
def _hide_object(self, vname):
"""Mark volume/snapshot as hidden
:param vname: physical volume name
"""
rename = {'name': jcom.hidden(vname)}
try:
self.ra.modify_lun(vname, rename)
except jexc.JDSSException as err:
msg = _('Failure in hidding {object}, err: {error},'
' object have to be removed manually')
emsg = msg.format(object=vname, error=err)
LOG.warning(emsg)
raise exception.VolumeBackendAPIException(emsg)
def _clean_garbage_snapshots(self, vname, snapshots):
"""Delete physical snapshots that have no descendents"""
garbage = []
for snap in snapshots:
if snap['clones'] == '':
try:
self.ra.delete_snapshot(vname, snap['name'])
except jexc.JDSSException as err:
args = {'obj': jcom.idname(vname), 'err': err}
msg = (_("Unable to clean garbage for "
"%(obj)s: %(err)s") % args)
raise exception.VolumeBackendAPIException(msg)
garbage.append(snap)
for snap in garbage:
snapshots.remove(snap)
return snapshots
def _cascade_volume_delete(self, o_vname, o_snaps):
"""Delete or hides volume(if it is busy)
Go over snapshots and deletes them if possible
Calls for recursive volume deletion if volume do not have children
"""
vsnaps = []
deletable = True
for snap in o_snaps:
if jcom.is_snapshot(snap['name']):
vsnaps += [(snap['name'],
jcom.full_name_volume(snap['clones']))]
active_vsnaps = [vs for vs in vsnaps if jcom.is_hidden(vs[1]) is False]
# If volume have clones or hidden snapshots it should be hidden
if len(active_vsnaps) < len(o_snaps):
deletable = False
for vsnap in active_vsnaps:
psnap = []
try:
psnap = self.ra.get_snapshots(vsnap[1])
except jexc.JDSSException:
msg = (_('Failure in acquiring snapshot for %s.') % vsnap[1])
raise exception.VolumeBackendAPIException(msg)
try:
psnap = self._clean_garbage_snapshots(vsnap[1], psnap)
except exception.VolumeBackendAPIException as err:
msg = (_('Failure in cleaning garbage snapshots %s'
' for volume %s, %s') % psnap, vsnap[1], err)
raise exception.VolumeBackendAPIException(msg)
if len(psnap) > 0:
deletable = False
self._hide_object(vsnap[1])
else:
try:
self.ra.delete_snapshot(o_vname,
vsnap[0],
recursively_children=True,
recursively_dependents=True,
force_umount=True)
except jexc.JDSSException as err:
LOG.warning('Failure during deletion of physical '
'snapshot %s, err: %s', vsnap[0], err)
msg = (_('Failure during deletion of virtual snapshot '
'%s') % vsnap[1])
raise exception.VolumeBackendAPIException(msg)
if deletable:
self._gc_delete(o_vname)
else:
self._hide_object(o_vname)
def delete_volume(self, volume, cascade=False):
"""Delete volume
:param volume: volume reference
:param cascade: remove snapshots of a volume as well
"""
vname = jcom.vname(volume.id)
LOG.debug('deleating volume %s', vname)
snapshots = None
try:
snapshots = self.ra.get_snapshots(vname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s dne, it was already '
'deleted', vname)
return
except jexc.JDSSException as err:
raise exception.VolumeBackendAPIException(err)
snapshots = self._clean_garbage_snapshots(vname, snapshots)
if cascade:
self._cascade_volume_delete(vname, snapshots)
else:
if len(snapshots) > 0:
self._hide_object(vname)
else:
self._gc_delete(vname)
def _gc_delete(self, vname):
"""Delete volume and its hidden parents
Deletes volume by going recursively to the first active
parent and cals recursive deletion on storage side
"""
vol = None
try:
vol = self.ra.get_lun(vname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s does not exist, it was already '
'deleted.', vname)
return
except jexc.JDSSException as err:
raise exception.VolumeBackendAPIException(err)
if vol['is_clone']:
self._delete_back_recursively(jcom.origin_volume(vol['origin']),
jcom.origin_snapshot(vol['origin']))
else:
try:
self.ra.delete_lun(vname)
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)
def _delete_back_recursively(self, opvname, opsname):
"""Deletes snapshot by removing its oldest removable parent
Checks if source volume for this snapshot is hidden:
If it is hidden and have no other descenents, it calls itself on its
source snapshot if such exists, or deletes it
If it is not hidden, trigers delete for snapshot
:param ovname: origin phisical volume name
:param osname: origin phisical snapshot name
"""
if jcom.is_hidden(opvname):
# Resource is hidden
snaps = []
try:
snaps = self.ra.get_snapshots(opvname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('Unable to get physical snapshots related to'
' physical volume %s, volume do not exist',
opvname)
return
except jexc.JDSSException as err:
raise exception.VolumeBackendAPIException(err)
snaps = self._clean_garbage_snapshots(opvname, snaps)
if len(snaps) > 1:
# opvname has active snapshots and cant be deleted
# that is why we delete branch related to opsname
try:
self.ra.delete_snapshot(opvname,
opsname,
recursively_children=True,
recursively_dependents=True,
force_umount=True)
except jexc.JDSSException as err:
raise exception.VolumeBackendAPIException(err)
else:
vol = None
try:
vol = self.ra.get_lun(opvname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s does not exist, it was already'
'deleted.', opvname)
return
except jexc.JDSSException as err:
raise exception.VolumeBackendAPIException(err)
if vol['is_clone']:
self._delete_back_recursively(
jcom.origin_volume(vol['origin']),
jcom.origin_snapshot(vol['origin']))
else:
try:
self.ra.delete_lun(opvname,
recursively_children=True,
recursively_dependents=True,
force_umount=True)
except jexc.JDSSResourceNotFoundException:
LOG.debug('volume %s does not exist, it was already'
'deleted.', opvname)
return
except jexc.JDSSException as err:
raise exception.VolumeBackendAPIException(err)
else:
# Resource is active
try:
self.ra.delete_snapshot(opvname,
opsname,
recursively_children=True,
recursively_dependents=True,
force_umount=True)
except jexc.JDSSException as err:
raise exception.VolumeBackendAPIException(err)
def extend_volume(self, volume, new_size):
"""Extend an existing volume.
:param volume: volume reference
:param new_size: volume new size in GB
"""
LOG.debug("Extend volume %s", volume.id)
try:
self.ra.extend_lun(jcom.vname(volume.id),
new_size * o_units.Gi)
except jexc.JDSSException:
raise exception.VolumeBackendAPIException(
(_('Failed to extend volume %s.'), volume.id))
def _clone_object(self, oname, coname):
"""Creates a clone of specified object
:param: oname: name of an object to clone
:param: coname: name of a new clone
"""
LOG.debug('cloning %(oname)s to %(coname)s', {
"oname": oname,
"coname": coname})
try:
self.ra.create_snapshot(oname, coname)
except jexc.JDSSSnapshotExistsException:
try:
self.ra.delete_snapshot(oname, coname)
except jexc.JDSSSnapshotIsBusyException:
raise exception.Duplicate()
except jexc.JDSSException:
raise exception.VolumeBackendAPIException(
(_("Unable to create volume %s.") % coname))
except jexc.JDSSResourceNotFoundException:
if jcom.is_volume(oname):
raise exception.VolumeNotFound(volume_id=jcom.idname(oname))
raise exception.SnapshotNotFound(snapshot_id=jcom.idname(oname))
except jexc.JDSSException as err:
args = {'snapshot': coname,
'object': oname,
'err': err}
msg = (_('Failed to create tmp snapshot %(snapshot)s'
'for object %(object)s: %(err)s') % args)
raise exception.VolumeBackendAPIException(msg)
try:
self.ra.create_volume_from_snapshot(
coname,
coname,
oname,
sparse=self.jovian_sparse)
except jexc.JDSSVolumeExistsException:
raise exception.Duplicate()
except jexc.JDSSException as err:
try:
self.ra.delete_snapshot(oname,
coname,
recursively_children=True,
recursively_dependents=True,
force_umount=True)
except jexc.JDSSException as terr:
LOG.warning("Because of %s phisical snapshot %s of volume"
" %s have to be removed manually",
terr,
coname,
oname)
raise exception.VolumeBackendAPIException(
_("Unable to create volume {vol} because of {err}.").format(
vol=coname, err=err))
def create_cloned_volume(self, volume, src_vref):
"""Create a clone of the specified volume.
:param volume: new volume reference
:param src_vref: source volume reference
"""
cvname = jcom.vname(volume.id)
vname = jcom.vname(src_vref.id)
LOG.debug('cloned volume %(id)s to %(id_clone)s', {
"id": src_vref.id,
"id_clone": volume.id})
self._clone_object(vname, cvname)
clone_size = 0
try:
clone_size = int(self.ra.get_lun(cvname)['volsize'])
except jexc.JDSSException:
self._delete_back_recursively(vname, cvname)
raise exception.VolumeBackendAPIException(
_("Fail in cloning volume {vol} to {clone}.").format(
vol=src_vref.id, clone=volume.id))
try:
if int(clone_size) < o_units.Gi * int(volume.size):
self.extend_volume(volume, int(volume.size))
except exception.VolumeBackendAPIException:
# If volume can't be set to a proper size make sure to clean it
# before failing
try:
self._delete_back_recursively(cvname, cvname)
except exception.VolumeBackendAPIException as err:
LOG.warning("Because of %s phisical snapshot %s of volume"
" %s have to be removed manualy",
err,
cvname,
vname)
raise
provider_location = self._get_provider_location(volume.id)
provider_auth = self._get_provider_auth()
ret = {}
if provider_auth:
ret['provider_auth'] = provider_auth
ret['provider_location'] = provider_location
return ret
def create_volume_from_snapshot(self, volume, snapshot):
"""Create a volume from a snapshot.
If volume_type extra specs includes 'replication: <is> True'
the driver needs to create a volume replica (secondary),
and setup replication between the newly created volume and
the secondary volume.
"""
LOG.debug('create volume %(vol)s from snapshot %(snap)s', {
'vol': volume.id,
'snap': snapshot.id})
cvname = jcom.vname(volume.id)
sname = jcom.sname(snapshot.id)
self._clone_object(sname, cvname)
clone_size = 0
try:
clone_size = int(self.ra.get_lun(cvname)['volsize'])
except jexc.JDSSException:
self._delete_back_recursively(sname, cvname)
raise exception.VolumeBackendAPIException(
_("Fail in cloning snapshot {snap} to {clone}.").format(
snap=snapshot.id, clone=volume.id))
try:
if clone_size < o_units.Gi * int(volume.size):
self.extend_volume(volume, int(volume.size))
except exception.VolumeBackendAPIException:
# If volume can't be set to a proper size make sure to clean it
# before failing
try:
self._delete_back_recursively(cvname, cvname)
except exception.VolumeBackendAPIException as ierr:
msg = ("Hidden snapshot %s of volume %s "
"have to be removed manualy, "
"as automatic removal failed: %s")
LOG.warning(msg, cvname, sname, ierr)
raise
provider_location = self._get_provider_location(volume.id)
provider_auth = self._get_provider_auth()
ret = {}
if provider_auth is not None:
ret['provider_auth'] = provider_auth
ret['provider_location'] = provider_location
return ret
def create_snapshot(self, snapshot):
"""Create snapshot of existing volume.
:param snapshot: snapshot reference
"""
LOG.debug('create snapshot %(snap)s for volume %(vol)s', {
'snap': snapshot.id,
'vol': snapshot.volume_id})
vname = jcom.vname(snapshot.volume_id)
sname = jcom.sname(snapshot.id)
self._clone_object(vname, sname)
try:
self.ra.make_readonly_lun(sname)
except jexc.JDSSException as err:
# Name of snapshot should be the same as a name of volume
# that is going to be created from it
self._delete_back_recursively(vname, sname)
raise exception.VolumeBackendAPIException(err)
def delete_snapshot(self, snapshot):
"""Delete snapshot of existing volume.
:param snapshot: snapshot reference
"""
sname = jcom.sname(snapshot.id)
LOG.debug('deleating snapshot %s.', sname)
snapshots = None
try:
snapshots = self.ra.get_snapshots(sname)
except jexc.JDSSResourceNotFoundException:
LOG.debug('physical volume %s dne, it was already'
'deleted.', sname)
return
except jexc.JDSSException as err:
raise exception.VolumeBackendAPIException(err)
snapshots = self._clean_garbage_snapshots(sname, snapshots)
if len(snapshots) > 0:
self._hide_object(sname)
else:
self._gc_delete(sname)
def _get_provider_auth(self):
"""Get provider authentication for the volume.
:return: string of auth method and credentials
"""
chap_user = volume_utils.generate_password(
length=8,
symbolgroups=(string.ascii_lowercase +
string.ascii_uppercase))
chap_password = volume_utils.generate_password(
length=self.jovian_chap_pass_len,
symbolgroups=(string.ascii_lowercase +
string.ascii_uppercase + string.digits))
return 'CHAP {user} {passwd}'.format(
user=chap_user, passwd=chap_password)
def _get_provider_location(self, volume_name):
"""Return volume iscsiadm-formatted provider location string."""
return '{host}:{port},1 {name} 0'.format(
host=self.ra.get_active_host(),
port=self.jovian_iscsi_target_portal_port,
name=self._get_target_name(volume_name)
)
def create_export(self, _ctx, volume, connector):
"""Create new export for zvol.
:param volume: reference of volume to be exported
:return: iscsiadm-formatted provider location string
"""
LOG.debug("create export for volume: %s.", volume.id)
self._create_target_volume(volume)
return {'provider_location': self._get_provider_location(volume.id)}
def ensure_export(self, _ctx, volume):
"""Recreate parts of export if necessary.
:param volume: reference of volume to be exported
"""
LOG.debug("ensure export for volume: %s.", volume.id)
self._ensure_target_volume(volume)
def remove_export(self, _ctx, volume):
"""Destroy all resources created to export zvol.
:param volume: reference of volume to be unexported
"""
LOG.debug("remove_export for volume: %s.", volume.id)
self._remove_target_volume(volume)
def _update_volume_stats(self):
"""Retrieve stats info."""
LOG.debug('Updating volume stats')
pool_stats = self.ra.get_pool_stats()
total_capacity = math.floor(int(pool_stats["size"]) / o_units.Gi)
free_capacity = math.floor(int(pool_stats["available"]) / o_units.Gi)
reserved_percentage = (
self.configuration.safe_get('reserved_percentage'))
if total_capacity is None:
total_capacity = 'unknown'
if free_capacity is None:
free_capacity = 'unknown'
location_info = '%(driver)s:%(host)s:%(volume)s' % {
'driver': self.__class__.__name__,
'host': self.ra.get_active_host()[0],
'volume': self._pool
}
self._stats = {
'vendor_name': 'Open-E',
'driver_version': self.VERSION,
'storage_protocol': 'iSCSI',
'total_capacity_gb': total_capacity,
'free_capacity_gb': free_capacity,
'reserved_percentage': int(reserved_percentage),
'volume_backend_name': self.backend_name,
'QoS_support': False,
'location_info': location_info
}
LOG.debug('Total capacity: %d, '
'Free %d.',
self._stats['total_capacity_gb'],
self._stats['free_capacity_gb'])
def _create_target(self, target_name, use_chap=True):
"""Creates target and handles exceptions
Tryes to create target.
:param target_name: name of target
:param use_chap: flag for using chap
"""
try:
self.ra.create_target(target_name,
use_chap=use_chap)
except jexc.JDSSResourceExistsException:
raise exception.Duplicate()
except jexc.JDSSException as ex:
msg = (_('Unable to create target %(target)s '
'because of %(error)s.') % {'target': target_name,
'error': ex})
raise exception.VolumeBackendAPIException(msg)
def _attach_target_volume(self, target_name, vname):
"""Attach target to volume and handles exceptions
Tryes to set attach volume to specific target.
In case of failure will remve 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 ex:
msg = ('Unable to attach volume to target {target} '
'because of {error}.')
emsg = msg.format(target=target_name, error=ex)
LOG.debug(msg, {"target": target_name, "error": ex})
try:
self.ra.delete_target(target_name)
except jexc.JDSSException:
pass
raise exception.VolumeBackendAPIException(_(emsg))
def _set_target_credentials(self, target_name, cred):
"""Set CHAP configuration for target and handle exceptions
Tryes to set CHAP credentials for specific target.
In case of failure will remve 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 ex:
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': ex})
LOG.debug(err_msg)
raise exception.VolumeBackendAPIException(_(err_msg))
def _create_target_volume(self, volume):
"""Creates target and attach volume to it
:param volume: volume id
:return:
"""
LOG.debug("create target and attach volume %s to it", volume.id)
target_name = self.jovian_target_prefix + volume.id
vname = jcom.vname(volume.id)
auth = volume.provider_auth
if not auth:
msg = _("Volume {} is missing provider_auth") % volume.id
raise exception.VolumeDriverException(msg)
(__, auth_username, auth_secret) = auth.split()
chap_cred = {"name": auth_username,
"password": auth_secret}
# Create target
self._create_target(target_name, True)
# Attach volume
self._attach_target_volume(target_name, vname)
# Set credentials
self._set_target_credentials(target_name, chap_cred)
def _ensure_target_volume(self, volume):
"""Checks if target configured properly and volume is attached to it
param: volume: volume structure
"""
LOG.debug("ensure volume %s assigned to a proper target", volume.id)
target_name = self.jovian_target_prefix + volume.id
auth = volume.provider_auth
if not auth:
msg = _("volume {} is missing provider_auth").format(volume.id)
raise exception.VolumeDriverException(msg)
(__, auth_username, auth_secret) = auth.split()
chap_cred = {"name": auth_username,
"password": auth_secret}
if not self.ra.is_target(target_name):
self._create_target_volume(volume)
return
if not self.ra.is_target_lun(target_name, volume.id):
vname = jcom.vname(volume.id)
self._attach_target_volume(target_name, vname)
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 err:
self.ra.delete_target(target_name)
raise exception.VolumeBackendAPIException(err)
def _remove_target_volume(self, volume):
"""_remove_target_volume
Ensure that volume is not attached to target and target do not exists.
"""
target_name = self.jovian_target_prefix + volume.id
LOG.debug("remove export")
LOG.debug("detach volume:%(vol)s from target:%(targ)s.", {
'vol': volume,
'targ': target_name})
try:
self.ra.detach_target_vol(target_name, jcom.vname(volume.id))
except jexc.JDSSResourceNotFoundException as ex:
LOG.debug('failed to remove resource %(t)s because of %(err)s', {
't': target_name,
'err': ex.args[0]})
except jexc.JDSSException as ex:
LOG.debug('failed to Terminate_connection for target %(targ)s'
'because of: %(err)s', {
'targ': target_name,
'err': ex.args[0]})
raise exception.VolumeBackendAPIException(ex)
LOG.debug("delete target: %s", target_name)
try:
self.ra.delete_target(target_name)
except jexc.JDSSResourceNotFoundException as ex:
LOG.debug('failed to remove resource %(target)s because '
'of %(err)s', {'target': target_name,
'err': ex.args[0]})
except jexc.JDSSException as ex:
LOG.debug('Failed to Terminate_connection for target %(targ)s'
'because of: %(err)s', {
'targ': target_name,
'err': ex.args[0]})
raise exception.VolumeBackendAPIException(ex)
def _get_iscsi_properties(self, volume, connector):
"""Return dict according to cinder/driver.py implementation.
:param volume:
:return:
"""
tname = self.jovian_target_prefix + volume.id
iface_info = []
multipath = connector.get('multipath', False)
if multipath:
iface_info = self._get_active_ifaces()
if not iface_info:
raise exception.InvalidConfigurationValue(
_('No available interfaces '
'or config excludes them'))
iscsi_properties = dict()
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.jovian_target_prefix +
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
auth = volume.provider_auth
if auth:
(auth_method, auth_username, auth_secret) = 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 initialize_connection(self, volume, connector):
"""Initialize the connection and returns connection info.
The iscsi driver returns a driver_volume_type of 'iscsi'.
the format of the driver data is defined in smis_get_iscsi_properties.
Example return value:
.. code-block:: json
{
'driver_volume_type': 'iscsi'
'data': {
'target_discovered': True,
'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001',
'target_portal': '127.0.0.0.1:3260',
'volume_id': '12345678-1234-1234-1234-123456789012',
}
}
"""
iscsi_properties = self._get_iscsi_properties(volume, connector)
LOG.debug("initialize_connection for %(volume)s %(ip)s.",
{'volume': volume.id,
'ip': connector['ip']})
return {
'driver_volume_type': 'iscsi',
'data': iscsi_properties,
}
def terminate_connection(self, volume, connector, force=False, **kwargs):
"""terminate_connection
"""
LOG.debug("terminate connection for %(volume)s ",
{'volume': volume.id})

View File

@ -0,0 +1,82 @@
# 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.
from cinder import exception
from cinder.i18n import _
class JDSSException(exception.VolumeDriverException):
"""Unknown error"""
message = _("%(reason)s")
class JDSSRESTException(JDSSException):
"""Unknown communication error"""
message = _("JDSS REST request %(request)s faild: %(reason)s.")
class JDSSRESTProxyException(JDSSException):
"""Connection with host failed"""
message = _("JDSS connection with %(host)s failed: %(reason)s.")
class JDSSResourceNotFoundException(JDSSException):
"""Resource does not exist"""
message = _("JDSS resource %(res)s DNE.")
class JDSSVolumeNotFoundException(JDSSResourceNotFoundException):
"""Volume does not exist"""
message = _("JDSS volume %(volume)s DNE.")
class JDSSSnapshotNotFoundException(JDSSResourceNotFoundException):
"""Snapshot does not exist"""
message = _("JDSS snapshot %(snapshot)s DNE.")
class JDSSResourceExistsException(JDSSException):
"""Resource with specified id exists"""
message = _("JDSS resource with id %(res)s exists.")
class JDSSSnapshotExistsException(JDSSResourceExistsException):
"""Snapshot with the same id exists"""
message = _("JDSS snapshot %(snapshot)s already exists.")
class JDSSVolumeExistsException(JDSSResourceExistsException):
"""Volume with same id exists"""
message = _("JDSS volume %(volume)s already exists.")
class JDSSResourceIsBusyException(JDSSException):
"""Resource have dependents"""
message = _("JDSS resource %(res)s is busy.")
class JDSSSnapshotIsBusyException(JDSSResourceIsBusyException):
"""Snapshot have dependent clones"""
message = _("JDSS snapshot %(snapshot)s is busy.")

View File

@ -0,0 +1,112 @@
# 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.
from cinder import exception
from cinder.i18n import _
def is_volume(name):
"""Return True if volume"""
return name.startswith("v_")
def is_snapshot(name):
"""Return True if volume"""
return name.startswith("s_")
def idname(name):
"""Convert id into snapshot name"""
if name.startswith(('s_', 'v_', 't_')):
return name[2:]
msg = _('Object name %s is incorrect') % name
raise exception.VolumeBackendAPIException(message=msg)
def vname(name):
"""Convert id into volume name"""
if name.startswith("v_"):
return name
if name.startswith('s_'):
msg = _('Attempt to use snapshot %s as a volume') % name
raise exception.VolumeBackendAPIException(message=msg)
if name.startswith('t_'):
msg = _('Attempt to use deleted object %s as a volume') % name
raise exception.VolumeBackendAPIException(message=msg)
return 'v_' + name
def sname(name):
"""Convert id into snapshot name"""
if name.startswith('s_'):
return name
if name.startswith('v_'):
msg = _('Attempt to use volume %s as a snapshot') % name
raise exception.VolumeBackendAPIException(message=msg)
if name.startswith('t_'):
msg = _('Attempt to use deleted object %s as a snapshot') % name
raise exception.VolumeBackendAPIException(message=msg)
return 's_' + name
def is_hidden(name):
"""Check if object is active or no"""
if len(name) < 2:
return False
if name.startswith('t_'):
return True
return False
def origin_snapshot(origin_str):
"""Extracts original phisical snapshot name from origin record"""
return origin_str.split("@")[1]
def origin_volume(origin_str):
"""Extracts original phisical volume name from origin record"""
return origin_str.split("@")[0].split("/")[1]
def full_name_volume(name):
"""Get volume id from full_name"""
return name.split('/')[1]
def hidden(name):
"""Get hidden version of a name"""
if len(name) < 2:
raise exception.VolumeDriverException("Incorrect volume name")
if name[:2] == 'v_' or name[:2] == 's_':
return 't_' + name[2:]
return 't_' + name

View File

@ -0,0 +1,893 @@
# 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.
"""REST cmd interoperation class for JovianDSS driver."""
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
LOG = logging.getLogger(__name__)
class JovianRESTAPI(object):
"""Jovian REST API proxy."""
def __init__(self, config):
self.target_p = config.get('iscsi_target_prefix',
'iqn.2020-04.com.open-e.cinder:')
self.pool = config.safe_get('jovian_pool')
self.rproxy = rest_proxy.JovianRESTProxy(config)
self.resource_dne_msg = (
re.compile(r'^Zfs resource: .* not found in this collection\.$'))
def _general_error(self, url, resp):
reason = "Request {} failure".format(url)
if 'error' in resp:
eclass = resp.get('class', 'Unknown')
code = resp.get('code', 'Unknown')
msg = resp.get('message', 'Unknown')
reason = ("Request to {url} failed with code:%{code} "
"of type:{eclass} reason:{message}")
reason = reason.format(eclass=eclass,
code=code,
message=msg,
url=url)
raise jexc.JDSSException(reason=reason)
def get_active_host(self):
"""Return address of currently used host."""
return self.rproxy.get_active_host()
def is_pool_exists(self):
"""is_pool_exists.
GET
/pools/<string:poolname>
:param pool_name:
:return: Bool
"""
req = ""
LOG.debug("check pool")
resp = self.rproxy.pool_request('GET', req)
if resp["code"] == 200 and not resp["error"]:
return True
return False
def get_iface_info(self):
"""get_iface_info
GET
/network/interfaces
:return list of internet ifaces
"""
req = '/network/interfaces'
LOG.debug("get network interfaces")
resp = self.rproxy.request('GET', req)
if (resp['error'] is None) and (resp['code'] == 200):
return resp['data']
raise jexc.JDSSRESTException(resp['error']['message'])
def get_luns(self):
"""get_all_pool_volumes.
GET
/pools/<string:poolname>/volumes
:param pool_name
:return list of all pool volumes
"""
req = '/volumes'
LOG.debug("get all volumes")
resp = self.rproxy.pool_request('GET', req)
if resp['error'] is None and resp['code'] == 200:
return resp['data']
raise jexc.JDSSRESTException(resp['error']['message'])
def create_lun(self, volume_name, volume_size, sparse=False,
block_size=None):
"""create_volume.
POST
.../volumes
:param volume_name:
:param volume_size:
:return:
"""
volume_size_str = str(volume_size)
jbody = {
'name': volume_name,
'size': volume_size_str,
'sparse': sparse
}
if block_size:
jbody['blocksize'] = block_size
req = '/volumes'
LOG.debug("create volume %s", str(jbody))
resp = self.rproxy.pool_request('POST', req, json_data=jbody)
if not resp["error"] and resp["code"] in (200, 201):
return
if resp["error"] is not None:
if resp["error"]["errno"] == str(5):
raise jexc.JDSSRESTException(
'Failed to create volume. {}.'.format(
resp['error']['message']))
raise jexc.JDSSRESTException('Failed to create volume.')
def extend_lun(self, volume_name, volume_size):
"""create_volume.
PUT /volumes/<string:volume_name>
"""
req = '/volumes/' + volume_name
volume_size_str = str(volume_size)
jbody = {
'size': volume_size_str
}
LOG.debug("jdss extend volume %(volume)s to %(size)s",
{"volume": volume_name,
"size": volume_size_str})
resp = self.rproxy.pool_request('PUT', req, json_data=jbody)
if not resp["error"] and resp["code"] == 201:
return
if resp["error"]:
raise jexc.JDSSRESTException(
'Failed to extend volume {}'.format(resp['error']['message']))
raise jexc.JDSSRESTException('Failed to extend volume.')
def is_lun(self, volume_name):
"""is_lun.
GET /volumes/<string:volumename>
Returns True if volume exists. Uses GET request.
:param pool_name:
:param volume_name:
:return:
"""
req = '/volumes/' + volume_name
LOG.debug("check volume %s", volume_name)
ret = self.rproxy.pool_request('GET', req)
if not ret["error"] and ret["code"] == 200:
return True
return False
def get_lun(self, volume_name):
"""get_lun.
GET /volumes/<volume_name>
:param volume_name:
:return:
{
"data":
{
"origin": null,
"referenced": "65536",
"primarycache": "all",
"logbias": "latency",
"creation": "1432730973",
"sync": "always",
"is_clone": false,
"dedup": "off",
"used": "1076101120",
"full_name": "Pool-0/v1",
"type": "volume",
"written": "65536",
"usedbyrefreservation": "1076035584",
"compression": "lz4",
"usedbysnapshots": "0",
"copies": "1",
"compressratio": "1.00x",
"readonly": "off",
"mlslabel": "none",
"secondarycache": "all",
"available": "976123452576",
"resource_name": "Pool-0/v1",
"volblocksize": "131072",
"refcompressratio": "1.00x",
"snapdev": "hidden",
"volsize": "1073741824",
"reservation": "0",
"usedbychildren": "0",
"usedbydataset": "65536",
"name": "v1",
"checksum": "on",
"refreservation": "1076101120"
},
"error": null
}
"""
req = '/volumes/' + volume_name
LOG.debug("get volume %s info", volume_name)
resp = self.rproxy.pool_request('GET', req)
if not resp['error'] and resp['code'] == 200:
return resp['data']
if resp['error']:
if 'message' in resp['error']:
if self.resource_dne_msg.match(resp['error']['message']):
raise jexc.JDSSResourceNotFoundException(res=volume_name)
self._general_error(req, resp)
def modify_lun(self, volume_name, prop=None):
"""Update volume properties
:prop volume_name: volume name
:prop prop: dictionary
{
<property>: <value>
}
"""
req = '/volumes/' + volume_name
resp = self.rproxy.pool_request('PUT', req, json_data=prop)
if resp["code"] in (200, 201, 204):
LOG.debug("volume %s updated", volume_name)
return
if resp["code"] == 500:
if resp["error"] is not None:
if resp["error"]["errno"] == 1:
raise jexc.JDSSResourceNotFoundException(
res=volume_name)
self._general_error(req, resp)
def make_readonly_lun(self, volume_name):
"""Set volume into read only mode
:param: volume_name: volume name
"""
prop = {"property_name": "readonly", "property_value": "on"}
self.modify_property_lun(volume_name, prop)
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
{ "property_name": "<name of property>",
"property_value":"<value of a property>"}
"""
req = '/volumes/{}/properties'.format(volume_name)
resp = self.rproxy.pool_request('PUT', req, json_data=prop)
if resp["code"] in (200, 201, 204):
LOG.debug(
"volume %s properties updated", volume_name)
return
if resp["code"] == 500:
if resp["error"] is not None:
if resp["error"]["errno"] == 1:
raise jexc.JDSSResourceNotFoundException(
res=volume_name)
raise jexc.JDSSRESTException(request=req,
reason=resp['error']['message'])
raise jexc.JDSSRESTException(request=req, reason="unknown")
def delete_lun(self, volume_name,
recursively_children=False,
recursively_dependents=False,
force_umount=False):
"""delete_volume.
DELETE /volumes/<string:volumename>
:param volume_name:
:return:
"""
jbody = {}
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"),
{'vol': volume_name,
'args': jbody})
if len(jbody) > 0:
resp = self.rproxy.pool_request('DELETE', req, json_data=jbody)
else:
resp = self.rproxy.pool_request('DELETE', req)
if resp["code"] == 204:
LOG.debug(
"volume %s deleted", volume_name)
return
# Handle DNE case
if resp["code"] == 500:
if 'message' in resp['error']:
if self.resource_dne_msg.match(resp['error']['message']):
LOG.debug("volume %s do not exists, delition success",
volume_name)
return
# 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)
raise jexc.JDSSRESTException('Failed to delete volume.')
def is_target(self, target_name):
"""is_target.
GET /san/iscsi/targets/ target_name
:param target_name:
:return: Bool
"""
req = '/san/iscsi/targets/' + target_name
LOG.debug("check if targe %s exists", target_name)
resp = self.rproxy.pool_request('GET', req)
if resp["error"] or resp["code"] not in (200, 201):
return False
if "name" in resp["data"]:
if resp["data"]["name"] == target_name:
LOG.debug(
"target %s exists", target_name)
return True
return False
def create_target(self,
target_name,
use_chap=True,
allow_ip=None,
deny_ip=None):
"""create_target.
POST /san/iscsi/targets
:param target_name:
:param chap_cred:
:param allow_ip:
"allow_ip": [
"192.168.2.30/0",
"192.168.3.45"
],
:return:
"""
req = '/san/iscsi/targets'
LOG.debug("create target %s", target_name)
jdata = {"name": target_name, "active": True}
jdata["incoming_users_active"] = use_chap
if allow_ip:
jdata["allow_ip"] = allow_ip
if deny_ip:
jdata["deny_ip"] = deny_ip
resp = self.rproxy.pool_request('POST', req, json_data=jdata)
if not resp["error"] and resp["code"] == 201:
return
if resp["code"] == 409:
raise jexc.JDSSResourceExistsException(res=target_name)
self._general_error(req, resp)
def delete_target(self, target_name):
"""delete_target.
DELETE /san/iscsi/targets/<target_name>
:param pool_name:
:param target_name:
:return:
"""
req = '/san/iscsi/targets/' + target_name
LOG.debug("delete target %s", target_name)
resp = self.rproxy.pool_request('DELETE', req)
if resp["code"] in (200, 201, 204):
LOG.debug(
"target %s deleted", target_name)
return
not_found_err = "opene.exceptions.ItemNotFoundError"
if (resp["code"] == 404) or \
(resp["error"]["class"] == not_found_err):
raise jexc.JDSSResourceNotFoundException(res=target_name)
self._general_error(req, resp)
def create_target_user(self, target_name, chap_cred):
"""Set CHAP credentials for accees specific target.
POST
/san/iscsi/targets/<target_name>/incoming-users
:param target_name:
:param chap_cred:
{
"name": "target_user",
"password": "3e21ewqdsacxz" --- 12 chars min
}
:return:
"""
req = '/san/iscsi/targets/' + target_name + "/incoming-users"
LOG.debug("add credentails to target %s", target_name)
resp = self.rproxy.pool_request('POST', req, json_data=chap_cred)
if not resp["error"] and resp["code"] in (200, 201, 204):
return
if resp['code'] == 404:
raise jexc.JDSSResourceNotFoundException(res=target_name)
self._general_error(req, resp)
def get_target_user(self, target_name):
"""Get name of CHAP user for accessing target
GET
/san/iscsi/targets/<target_name>/incoming-users
:param target_name:
"""
req = '/san/iscsi/targets/' + target_name + "/incoming-users"
LOG.debug("get chap cred for target %s", target_name)
resp = self.rproxy.pool_request('GET', req)
if not resp["error"] and resp["code"] == 200:
return resp['data']
if resp['code'] == 404:
raise jexc.JDSSResourceNotFoundException(res=target_name)
self._general_error(req, resp)
def delete_target_user(self, target_name, user_name):
"""Delete CHAP user for target
DELETE
/san/iscsi/targets/<target_name>/incoming-users/<user_name>
:param target_name: target name
:param user_name: user name
"""
req = '/san/iscsi/targets/{0}/incoming-users/{1}'.format(
target_name, user_name)
LOG.debug("remove credentails from target %s", target_name)
resp = self.rproxy.pool_request('DELETE', req)
if resp["error"] is None and resp["code"] == 204:
return
if resp['code'] == 404:
raise jexc.JDSSResourceNotFoundException(res=target_name)
self._general_error(req, resp)
def is_target_lun(self, target_name, lun_name):
"""is_target_lun.
GET /san/iscsi/targets/<target_name>/luns/<lun_name>
:param pool_name:
:param target_name:
:param lun_name:
:return: Bool
"""
req = '/san/iscsi/targets/' + target_name + "/luns/" + lun_name
LOG.debug("check if volume %(vol)s is associated with %(tar)s",
{'vol': lun_name,
'tar': target_name})
resp = self.rproxy.pool_request('GET', req)
if not resp["error"] and resp["code"] == 200:
LOG.debug("volume %(vol)s is associated with %(tar)s",
{'vol': lun_name,
'tar': target_name})
return True
if resp['code'] == 404:
LOG.debug("volume %(vol)s is not associated with %(tar)s",
{'vol': lun_name,
'tar': target_name})
return False
self._general_error(req, resp)
def attach_target_vol(self, target_name, lun_name, lun_id=0):
"""attach_target_vol.
POST /san/iscsi/targets/<target_name>/luns
:param target_name:
:param lun_name:
:return:
"""
req = '/san/iscsi/targets/{}/luns'.format(target_name)
jbody = {"name": lun_name, "lun": lun_id}
LOG.debug("atach volume %(vol)s to target %(tar)s",
{'vol': lun_name,
'tar': target_name})
resp = self.rproxy.pool_request('POST', req, json_data=jbody)
if not resp["error"] and resp["code"] == 201:
return
if resp['code'] == 409:
raise jexc.JDSSResourceExistsException(res=lun_name)
if resp['code'] == 404:
raise jexc.JDSSResourceNotFoundException(res=target_name)
self._general_error(req, resp)
def detach_target_vol(self, target_name, lun_name):
"""detach_target_vol.
DELETE /san/iscsi/targets/<target_name>/luns/
<lun_name>
:param target_name:
:param lun_name:
:return:
"""
req = '/san/iscsi/targets/' + target_name + "/luns/" + lun_name
LOG.debug("detach volume %(vol)s from target %(tar)s",
{'vol': lun_name,
'tar': target_name})
resp = self.rproxy.pool_request('DELETE', req)
if resp["code"] in (200, 201, 204):
return
if resp['code'] == 404:
raise jexc.JDSSResourceNotFoundException(res=lun_name)
self._general_error(req, resp)
def create_snapshot(self, volume_name, snapshot_name):
"""create_snapshot.
POST /pools/<string:poolname>/volumes/<string:volumename>/snapshots
:param pool_name:
:param volume_name: source volume
:param snapshot_name: snapshot name
:return:
"""
req = '/volumes/' + volume_name + '/snapshots'
jbody = {
'snapshot_name': snapshot_name
}
LOG.debug("create snapshot %s", snapshot_name)
resp = self.rproxy.pool_request('POST', req, json_data=jbody)
if not resp["error"] and resp["code"] in (200, 201, 204):
return
if resp["code"] == 500:
if resp["error"]:
if resp["error"]["errno"] == 1:
raise jexc.JDSSVolumeNotFoundException(
volume=volume_name)
if resp["error"]["errno"] == 5:
raise jexc.JDSSSnapshotExistsException(
snapshot=snapshot_name)
self._general_error(req, resp)
def create_volume_from_snapshot(self, volume_name, snapshot_name,
original_vol_name, **options):
"""create_volume_from_snapshot.
POST /volumes/<string:volumename>/clone
:param volume_name: volume that is going to be created
:param snapshot_name: slice of original volume
:param original_vol_name: sample copy
:return:
"""
req = '/volumes/' + original_vol_name + '/clone'
jbody = {
'name': volume_name,
'snapshot': snapshot_name,
'sparse': False
}
if 'sparse' in options:
jbody['sparse'] = options['sparse']
LOG.debug("create volume %(vol)s from snapshot %(snap)s",
{'vol': volume_name,
'snap': snapshot_name})
resp = self.rproxy.pool_request('POST', req, json_data=jbody)
if not resp["error"] and resp["code"] in (200, 201, 204):
return
if resp["code"] == 500:
if resp["error"]:
if resp["error"]["errno"] == 100:
raise jexc.JDSSVolumeExistsException(
volume=volume_name)
args = {"vol": volume_name, "e": resp['error']['message']}
msg = _('Failed to create volume %(vol)s, err: %(e)s') % args
raise jexc.JDSSRESTException(msg)
raise jexc.JDSSRESTException('unable to create volume')
def is_snapshot(self, volume_name, snapshot_name):
"""is_snapshots.
GET
/volumes/<string:volumename>/snapshots/<string:snapshotname>/clones
:param volume_name: that snapshot belongs to
:return: bool
"""
req = '/volumes/' + volume_name + '/snapshots/' + snapshot_name + \
'/clones'
LOG.debug("check if snapshot %(snap)s of volume %(vol)s exists",
{'snap': snapshot_name,
'vol': volume_name})
resp = self.rproxy.pool_request('GET', req)
if not resp["error"] and resp["code"] == 200:
return True
return False
def delete_snapshot(self,
volume_name,
snapshot_name,
recursively_children=False,
recursively_dependents=False,
force_umount=False):
"""delete_snapshot.
DELETE /volumes/<string:volumename>/snapshots/
<string:snapshotname>
:param volume_name: volume that snapshot belongs to
:param snapshot_name: snapshot name
:param recursively_children: boolean indicating if zfs should
recursively destroy all children of resource, in case of snapshot
remove all snapshots in descendant file system (default false).
:param recursively_dependents: boolean indicating if zfs should
recursively destroy all dependents, including cloned file systems
outside the target hierarchy (default false).
:param force_umount: boolean indicating if volume should be forced to
umount (defualt false).
:return:
"""
if not self.is_snapshot(volume_name, snapshot_name):
return
req = '/volumes/' + volume_name + '/snapshots/' + snapshot_name
LOG.debug("delete snapshot %(snap)s of volume %(vol)s",
{'snap': snapshot_name,
'vol': volume_name})
jbody = {}
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)
if resp["code"] in (200, 201, 204):
LOG.debug("snapshot %s deleted", snapshot_name)
return
if resp["code"] == 500:
if resp["error"]:
if resp["error"]["errno"] == 1000:
raise jexc.JDSSSnapshotIsBusyException(
snapshot=snapshot_name)
msg = 'Failed to delete snapshot {}, err: {}'.format(
snapshot_name, resp['error']['message'])
raise jexc.JDSSRESTException(msg)
msg = 'Failed to delete snapshot {}'.format(snapshot_name)
raise jexc.JDSSRESTException(msg)
def get_snapshots(self, volume_name):
"""get_snapshots.
GET
/volumes/<string:volumename>/
snapshots
:param volume_name: that snapshot belongs to
:return:
{
"data":
[
{
"referenced": "65536",
"name": "MySnapshot",
"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"
}
],
"error": null
}
"""
req = '/volumes/' + volume_name + '/snapshots'
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"]["entries"]
if resp['code'] == 500:
if 'message' in resp['error']:
if self.resource_dne_msg.match(resp['error']['message']):
raise jexc.JDSSResourceNotFoundException(volume_name)
raise jexc.JDSSRESTException('unable to get snapshots')
def get_pool_stats(self):
"""get_pool_stats.
GET /pools/<string:poolname>
:param pool_name:
:return:
{
"data": {
"available": "24433164288",
"status": 24,
"name": "Pool-0",
"scan": {
"errors": 0,
"repaired": "0",
"start_time": 1463476815,
"state": "finished",
"end_time": 1463476820,
"type": "scrub"
},
"iostats": {
"read": "0",
"write": "0",
"chksum": "0"
},
"vdevs": [
{
"name": "scsi-SSCST_BIOoWKF6TM0qafySQBUd1bb392e",
"iostats": {
"read": "0",
"write": "0",
"chksum": "0"
},
"disks": [
{
"led": "off",
"name": "sdb",
"iostats": {
"read": "0",
"write": "0",
"chksum": "0"
},
"health": "ONLINE",
"sn": "d1bb392e",
"path": "pci-0000:04:00.0-scsi-0:0:0:0",
"model": "oWKF6TM0qafySQBU",
"id": "scsi-SSCST_BIOoWKF6TM0qafySQBUd1bb392e",
"size": 30064771072
}
],
"health": "ONLINE",
"vdev_replacings": [],
"vdev_spares": [],
"type": ""
}
],
"health": "ONLINE",
"operation": "none",
"id": "11612982948930769833",
"size": "29796335616"
},
"error": null
}
"""
req = ""
LOG.debug("Get pool %s fsprops", self.pool)
resp = self.rproxy.pool_request('GET', req)
if not resp["error"] and resp["code"] == 200:
return resp["data"]
raise jexc.JDSSRESTException('Unable to get pool info')

View File

@ -0,0 +1,226 @@
# 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.
"""Network connection handling class for JovianDSS driver."""
import json
import time
from oslo_log import log as logging
from oslo_utils import netutils as o_netutils
import requests
import urllib3
from cinder import exception
from cinder.i18n import _
from cinder.volume.drivers.open_e.jovian_common import exception as jexc
LOG = logging.getLogger(__name__)
class JovianRESTProxy(object):
"""Jovian REST API proxy."""
def __init__(self, config):
""":param config: config is like dict."""
self.proto = 'http'
if config.get('driver_use_ssl', True):
self.proto = 'https'
self.hosts = config.safe_get('san_hosts')
self.port = str(config.get('san_api_port', 82))
self.active_host = 0
for host in self.hosts:
if o_netutils.is_valid_ip(host) is False:
err_msg = ('Invalid value of jovian_host property: '
'%(addr)s, IP address expected.' %
{'addr': host})
LOG.debug(err_msg)
raise exception.InvalidConfigurationValue(err_msg)
self.api_path = "/api/v3"
self.delay = config.get('jovian_recovery_delay', 40)
self.pool = config.safe_get('jovian_pool')
self.user = config.get('san_login', 'admin')
self.password = config.get('san_password', 'admin')
self.auth = requests.auth.HTTPBasicAuth(self.user, self.password)
self.verify = False
self.retry_n = config.get('jovian_rest_send_repeats', 3)
self.header = {'connection': 'keep-alive',
'Content-Type': 'application/json',
'authorization': 'Basic '}
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def _get_pool_url(self, host):
url = ('%(proto)s://%(host)s:%(port)s/api/v3/pools/%(pool)s' % {
'proto': self.proto,
'host': host,
'port': self.port,
'pool': self.pool})
return url
def _get_url(self, host):
url = ('%(proto)s://%(host)s:%(port)s/api/v3' % {
'proto': self.proto,
'host': host,
'port': self.port})
return url
def request(self, request_method, req, json_data=None):
"""Send request to the specific url.
:param request_method: GET, POST, DELETE
:param url: where to send
:param json_data: data
"""
for j in range(self.retry_n):
for i in range(len(self.hosts)):
host = self.hosts[self.active_host]
url = self._get_url(host) + req
LOG.debug(
"sending request of type %(type)s to %(url)s "
"attempt: %(num)s.",
{'type': request_method,
'url': url,
'num': j})
if json_data is not None:
LOG.debug(
"sending data: %s.", json_data)
try:
ret = self._request_routine(url, request_method, json_data)
if len(ret) == 0:
self.active_host = ((self.active_host + 1)
% len(self.hosts))
continue
return ret
except requests.ConnectionError as err:
LOG.debug("Connection error %s", err)
self.active_host = (self.active_host + 1) % len(self.hosts)
continue
time.sleep(self.delay)
msg = (_('%(times) faild in a row') % {'times': j})
raise jexc.JDSSRESTProxyException(host=url, reason=msg)
def pool_request(self, request_method, req, json_data=None):
"""Send request to the specific url.
:param request_method: GET, POST, DELETE
:param url: where to send
:param json_data: data
"""
url = ""
for j in range(self.retry_n):
for i in range(len(self.hosts)):
host = self.hosts[self.active_host]
url = self._get_pool_url(host) + req
LOG.debug(
"sending pool request of type %(type)s to %(url)s "
"attempt: %(num)s.",
{'type': request_method,
'url': url,
'num': j})
if json_data is not None:
LOG.debug(
"JovianDSS: Sending data: %s.", str(json_data))
try:
ret = self._request_routine(url, request_method, json_data)
if len(ret) == 0:
self.active_host = ((self.active_host + 1)
% len(self.hosts))
continue
return ret
except requests.ConnectionError as err:
LOG.debug("Connection error %s", err)
self.active_host = (self.active_host + 1) % len(self.hosts)
continue
time.sleep(int(self.delay))
msg = (_('%(times) faild in a row') % {'times': j})
raise jexc.JDSSRESTProxyException(host=url, reason=msg)
def _request_routine(self, url, request_method, json_data=None):
"""Make an HTTPS request and return the results."""
ret = None
for i in range(3):
ret = dict()
try:
response_obj = requests.request(request_method,
auth=self.auth,
url=url,
headers=self.header,
data=json.dumps(json_data),
verify=self.verify)
LOG.debug('response code: %s', response_obj.status_code)
LOG.debug('response data: %s', response_obj.text)
ret['code'] = response_obj.status_code
if '{' in response_obj.text and '}' in response_obj.text:
if "error" in response_obj.text:
ret["error"] = json.loads(response_obj.text)["error"]
else:
ret["error"] = None
if "data" in response_obj.text:
ret["data"] = json.loads(response_obj.text)["data"]
else:
ret["data"] = None
if ret["code"] == 500:
if ret["error"] is not None:
if (("errno" in ret["error"]) and
("class" in ret["error"])):
if (ret["error"]["class"] ==
"opene.tools.scstadmin.ScstAdminError"):
LOG.debug("ScstAdminError %(code)d %(msg)s", {
"code": ret["error"]["errno"],
"msg": ret["error"]["message"]})
continue
if (ret["error"]["class"] ==
"exceptions.OSError"):
LOG.debug("OSError %(code)d %(msg)s", {
"code": ret["error"]["errno"],
"msg": ret["error"]["message"]})
continue
break
except requests.HTTPError as err:
LOG.debug("HTTP parsing error %s", err)
self.active_host = (self.active_host + 1) % len(self.hosts)
return ret
def get_active_host(self):
"""Return address of currently used host."""
return self.hosts[self.active_host]

View File

@ -0,0 +1,51 @@
# 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.
from oslo_config import cfg
jdss_connection_opts = [
cfg.ListOpt('san_hosts',
default='',
help='IP address of Open-E JovianDSS SA'),
cfg.IntOpt('jovian_rest_send_repeats',
default=3,
help='Number of retries to send REST request.'),
cfg.IntOpt('jovian_recovery_delay',
default=60,
help='Time before HA cluster failure.'),
cfg.ListOpt('jovian_ignore_tpath',
default=[],
help='List of multipath ip addresses to ignore.'),
]
jdss_iscsi_opts = [
cfg.IntOpt('chap_password_len',
default=12,
help='Length of the random string for CHAP password.'),
cfg.StrOpt('jovian_pool',
default='Pool-0',
help='JovianDSS pool that holds all cinder volumes'),
]
jdss_volume_opts = [
cfg.StrOpt('jovian_block_size',
default='128K',
help='Block size for volumes (512 - 128K)'),
]
CONF = cfg.CONF
CONF.register_opts(jdss_connection_opts)
CONF.register_opts(jdss_iscsi_opts)
CONF.register_opts(jdss_volume_opts)

View File

@ -0,0 +1,194 @@
=============================
Open-E JovianDSS iSCSI driver
=============================
The ``JovianISCSIDriver`` allows usage of Open-E Jovian Data Storage
Solution to be used as Block Storage in OpenStack deployments.
Supported operations
~~~~~~~~~~~~~~~~~~~~
- Create, delete, attach, and detach volumes.
- Create, list, and delete volume snapshots.
- Create a volume from a snapshot.
- Copy an image to a volume.
- Copy a volume to an image.
- Clone a volume.
- Extend a volume.
- Migrate a volume with back-end assistance.
Configuring
~~~~~~~~~~~
Edit with your favourite editor Cinder config file. It can be found at
/etc/cinder/cinder.conf
Add the field enabled\_backends with value jdss-0:
::
enabled_backends = jdss-0
Provide settings to JovianDSS driver by adding 'jdss-0' description:
::
[jdss-0]
backend_name = jdss-0
chap_password_len = 14
driver_use_ssl = True
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
jovian_pool = Pool-0
jovian_block_size = 128K
jovian_rest_send_repeats = 4
san_api_port = 82
target_port = 3260
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
san_hosts = 192.168.0.40
san_login = admin
san_password = admin
san_thin_provision = True
.. list-table:: **Open-E JovianDSS configuration options**
:header-rows: 1
* - Option
- Default value
- Description
* - ``backend_name``
- JovianDSS-iSCSI
- Name of the back end
* - ``chap_password_len``
- 12
- Length of the unique generated CHAP password.
* - ``driver_use_ssl``
- True
- Use SSL to send requests to JovianDSS[1]
* - ``iscsi_target_prefix``
- iqn.2016-04.com.open-e:01:cinder-
- Prefix that will be used to form target name for volume
* - ``jovian_pool``
- Pool-0
- Pool name that is going to be used. Must be created in [2]
* - ``jovian_block_size``
- 128K
- Block size for newly created volumes
* - ``jovian_rest_send_repeats``
- 3
- Number of times that driver will try to send REST request
* - ``san_api_port``
- 82
- Rest port according to the settings in [1]
* - ``target_port``
- 3260
- Port for iSCSI connections
* - ``volume_driver``
-
- Location of the driver source code
* - ``san_hosts``
-
- Comma separated list of IP address of the JovianDSS
* - ``san_login``
- admin
- Must be set according to the settings in [1]
* - ``san_password``
- admin
- Jovian password [1], **should be changed** for security purpouses
* - ``san_thin_provision``
- False
- Using thin provisioning for new volumes
1. JovianDSS Web interface/System Settings/REST Access
2. Pool can be created by going to JovianDSS Web interface/Storage
.. _interface/Storage:
`More info about Open-E JovianDSS <http://blog.open-e.com/?s=how+to>`__
Multiple Pools
~~~~~~~~~~~~~~
In order to add another JovianDSS Pool, create a copy of
JovianDSS config in cinder.conf file.
For instance if you want to add ``Pool-1`` located on the same host as
``Pool-0``. You extend ``cinder.conf`` file like:
::
enabled_backends = jdss-0, jdss-1
[jdss-0]
backend_name = jdss-0
chap_password_len = 14
driver_use_ssl = True
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
jovian_pool = Pool-0
jovian_block_size = 128K
jovian_rest_send_repeats = 4
san_api_port = 82
target_port = 3260
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
san_hosts = 192.168.0.40
san_login = admin
san_password = admin
san_thin_provision = True
[jdss-1]
backend_name = jdss-1
chap_password_len = 14
driver_use_ssl = True
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
jovian_pool = Pool-1
jovian_block_size = 128K
jovian_rest_send_repeats = 4
san_api_port = 82
target_port = 3260
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
san_hosts = 192.168.0.50
san_login = admin
san_password = admin
san_thin_provision = True
HA Cluster
~~~~~~~~~~
To utilize High Availability feature of JovianDSS:
1. `Guide`_ on configuring Pool to high availability cluster
.. _Guide: https://www.youtube.com/watch?v=juWIQT_bAfM
2. Set ``jovian_hosts`` with list of ``virtual IPs`` associated with this Pool
For instance if you have ``Pool-2`` with 2 virtual IPs 192.168.21.100
and 192.168.31.100 the configuration file will look like:
::
[jdss-2]
backend_name = jdss-2
chap_password_len = 14
driver_use_ssl = True
iscsi_target_prefix = iqn.2016-04.com.open-e.cinder:
jovian_pool = Pool-0
jovian_block_size = 128K
jovian_rest_send_repeats = 4
san_api_port = 82
target_port = 3260
volume_driver = cinder.volume.drivers.open_e.iscsi.JovianISCSIDriver
san_hosts = 192.168.21.100, 192.168.31.100
san_login = admin
san_password = admin
san_thin_provision = True
Feedback
--------
Please address problems and proposals to andrei.perepiolkin@open-e.com

View File

@ -0,0 +1,6 @@
---
features:
- |
Added support for Open-E JovianDSS data storage.
Driver supports Open-E disaster recovery feature and cascade volume
deletion in addition to support minimum required functions.