Merge "Add Cinder driver for Open-E JovianDSS data storage"
This commit is contained in:
commit
27959daf2b
@ -139,6 +139,8 @@ from cinder.volume.drivers.nexenta import options as \
|
|||||||
cinder_volume_drivers_nexenta_options
|
cinder_volume_drivers_nexenta_options
|
||||||
from cinder.volume.drivers import nfs as cinder_volume_drivers_nfs
|
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 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 \
|
from cinder.volume.drivers.prophetstor import options as \
|
||||||
cinder_volume_drivers_prophetstor_options
|
cinder_volume_drivers_prophetstor_options
|
||||||
from cinder.volume.drivers import pure as cinder_volume_drivers_pure
|
from cinder.volume.drivers import pure as cinder_volume_drivers_pure
|
||||||
@ -268,6 +270,9 @@ def list_opts():
|
|||||||
instorage_mcs_opts,
|
instorage_mcs_opts,
|
||||||
cinder_volume_drivers_inspur_instorage_instorageiscsi.
|
cinder_volume_drivers_inspur_instorage_instorageiscsi.
|
||||||
instorage_mcs_iscsi_opts,
|
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_sandstone_sdsdriver.sds_opts,
|
||||||
cinder_volume_drivers_veritas_access_veritasiscsi.VA_VOL_OPTS,
|
cinder_volume_drivers_veritas_access_veritasiscsi.VA_VOL_OPTS,
|
||||||
cinder_volume_manager.volume_manager_opts,
|
cinder_volume_manager.volume_manager_opts,
|
||||||
|
0
cinder/tests/unit/volume/drivers/open_e/__init__.py
Normal file
0
cinder/tests/unit/volume/drivers/open_e/__init__.py
Normal file
1461
cinder/tests/unit/volume/drivers/open_e/test_iscsi.py
Normal file
1461
cinder/tests/unit/volume/drivers/open_e/test_iscsi.py
Normal file
File diff suppressed because it is too large
Load Diff
997
cinder/tests/unit/volume/drivers/open_e/test_rest.py
Normal file
997
cinder/tests/unit/volume/drivers/open_e/test_rest.py
Normal 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)
|
0
cinder/volume/drivers/open_e/__init__.py
Normal file
0
cinder/volume/drivers/open_e/__init__.py
Normal file
975
cinder/volume/drivers/open_e/iscsi.py
Normal file
975
cinder/volume/drivers/open_e/iscsi.py
Normal 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})
|
82
cinder/volume/drivers/open_e/jovian_common/exception.py
Normal file
82
cinder/volume/drivers/open_e/jovian_common/exception.py
Normal 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.")
|
112
cinder/volume/drivers/open_e/jovian_common/jdss_common.py
Normal file
112
cinder/volume/drivers/open_e/jovian_common/jdss_common.py
Normal 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
|
893
cinder/volume/drivers/open_e/jovian_common/rest.py
Normal file
893
cinder/volume/drivers/open_e/jovian_common/rest.py
Normal 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')
|
226
cinder/volume/drivers/open_e/jovian_common/rest_proxy.py
Normal file
226
cinder/volume/drivers/open_e/jovian_common/rest_proxy.py
Normal 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]
|
51
cinder/volume/drivers/open_e/options.py
Normal file
51
cinder/volume/drivers/open_e/options.py
Normal 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)
|
@ -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
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user