Merge "Added initial backend ISCSI driver for Reduxio"
This commit is contained in:
commit
344f230990
@ -1321,3 +1321,12 @@ class SynoAuthError(VolumeDriverException):
|
||||
|
||||
class SynoLUNNotExist(VolumeDriverException):
|
||||
message = _("LUN not found by UUID: %(uuid)s.")
|
||||
|
||||
|
||||
# Reduxio driver
|
||||
class RdxAPICommandException(VolumeDriverException):
|
||||
message = _("Reduxio API Command Exception")
|
||||
|
||||
|
||||
class RdxAPIConnectionException(VolumeDriverException):
|
||||
message = _("Reduxio API Connection Exception")
|
||||
|
649
cinder/tests/unit/test_reduxio.py
Normal file
649
cinder/tests/unit/test_reduxio.py
Normal file
@ -0,0 +1,649 @@
|
||||
# Copyright (c) 2016 Reduxio Systems
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import copy
|
||||
import random
|
||||
import string
|
||||
|
||||
import mock
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.volume.drivers.reduxio import rdx_cli_api
|
||||
from cinder.volume.drivers.reduxio import rdx_iscsi_driver
|
||||
|
||||
DRIVER_PATH = ("cinder.volume.drivers."
|
||||
"reduxio.rdx_iscsi_driver.ReduxioISCSIDriver")
|
||||
API_PATH = "cinder.volume.drivers.reduxio.rdx_cli_api.ReduxioAPI"
|
||||
|
||||
TARGET = "mock_target"
|
||||
TARGET_USER = "rdxadmin"
|
||||
TARGET_PASSWORD = "mock_password"
|
||||
VOLUME_BACKEND_NAME = "REDUXIO_VOLUME_TYPE"
|
||||
CINDER_ID_LENGTH = 36
|
||||
VOLUME_ID = "abcdabcd-1234-abcd-1234-abcdeffedcba"
|
||||
VOLUME = {
|
||||
"name": "volume-" + VOLUME_ID,
|
||||
"id": VOLUME_ID,
|
||||
"display_name": "fake_volume",
|
||||
"size": 2,
|
||||
"host": "irrelevant",
|
||||
"volume_type": None,
|
||||
"volume_type_id": None,
|
||||
"consistencygroup_id": None,
|
||||
'metadata': {}
|
||||
}
|
||||
|
||||
VOLUME_RDX_NAME = "abcdabcd1234abcd1234abcdeffedcb"
|
||||
VOLUME_RDX_DESC = "openstack_" + VOLUME["name"]
|
||||
|
||||
SRC_VOL_ID = "4c7a294d-5964-4379-a15f-ce5554734efc"
|
||||
SRC_VOL_RDX_NAME = "ac7a294d59644379a15fce5554734ef"
|
||||
|
||||
SRC_VOL = {
|
||||
"name": "volume-" + SRC_VOL_ID,
|
||||
"id": SRC_VOL_ID,
|
||||
"display_name": 'fake_src',
|
||||
"size": 2,
|
||||
"host": "irrelevant",
|
||||
"volume_type": None,
|
||||
"volume_type_id": None,
|
||||
"consistencygroup_id": None,
|
||||
}
|
||||
|
||||
SNAPSHOT_ID = "04fe2f9a-d0c4-4564-a30d-693cc3657b47"
|
||||
SNAPSHOT_RDX_NAME = "a4fe2f9ad0c44564a30d693cc3657b4"
|
||||
|
||||
SNAPSHOT = {
|
||||
"name": "snapshot-" + SNAPSHOT_ID,
|
||||
"id": SNAPSHOT_ID,
|
||||
"volume_id": SRC_VOL_ID,
|
||||
"volume_name": "volume-" + SRC_VOL_ID,
|
||||
"volume_size": 2,
|
||||
"display_name": "fake_snapshot",
|
||||
"cgsnapshot_id": None,
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
CONNECTOR = {
|
||||
"initiator": "iqn.2013-12.com.stub:af4032f00014000e",
|
||||
}
|
||||
|
||||
LS_SETTINGS = {
|
||||
"system_configuration": [
|
||||
{
|
||||
"name": "host_name",
|
||||
"value": "reduxio"
|
||||
},
|
||||
{
|
||||
"name": "serial_number",
|
||||
"value": "af4032f00014000e"
|
||||
},
|
||||
{
|
||||
"name": "primary_ntp",
|
||||
"value": "mickey.il.reduxio"
|
||||
},
|
||||
{
|
||||
"name": "secondary_ntp",
|
||||
"value": "minnie.il.reduxio"
|
||||
},
|
||||
{
|
||||
"name": "timezone",
|
||||
"value": "Asia/Jerusalem"
|
||||
},
|
||||
{
|
||||
"name": "capacity_threshold",
|
||||
"value": "93%"
|
||||
},
|
||||
{
|
||||
"name": "storsense_enabled",
|
||||
"value": True
|
||||
}
|
||||
],
|
||||
"network_configuration": [
|
||||
{
|
||||
"name": "iscsi_target_iqn",
|
||||
"value": "iqn.2013-12.com.reduxio:af4032f00014000e"
|
||||
},
|
||||
{
|
||||
"name": "iscsi_target_tcp_port",
|
||||
"value": "3260"
|
||||
},
|
||||
{
|
||||
"name": "mtu",
|
||||
"value": "9000"
|
||||
}
|
||||
],
|
||||
"iscsi_network1": [
|
||||
{
|
||||
"name": "controller_1_port_1",
|
||||
"value": "10.46.93.11"
|
||||
},
|
||||
{
|
||||
"name": "controller_2_port_1",
|
||||
"value": "10.46.93.22"
|
||||
},
|
||||
{
|
||||
"name": "subnet_mask",
|
||||
"value": "255.0.0.0"
|
||||
},
|
||||
{
|
||||
"name": "default_gateway",
|
||||
"value": None
|
||||
},
|
||||
{
|
||||
"name": "vlan_tag",
|
||||
"value": None
|
||||
}
|
||||
],
|
||||
"iscsi_network2": [
|
||||
{
|
||||
"name": "controller_1_port_2",
|
||||
"value": "10.64.93.11"
|
||||
},
|
||||
{
|
||||
"name": "controller_2_port_2",
|
||||
"value": "10.64.93.22"
|
||||
},
|
||||
{
|
||||
"name": "subnet_mask",
|
||||
"value": "255.0.0.0"
|
||||
},
|
||||
{
|
||||
"name": "default_gateway",
|
||||
"value": None
|
||||
},
|
||||
{
|
||||
"name": "vlan_tag",
|
||||
"value": None
|
||||
}
|
||||
],
|
||||
"management_settings": [
|
||||
{
|
||||
"name": "floating_ip",
|
||||
"value": "172.17.46.93"
|
||||
},
|
||||
{
|
||||
"name": "management_ip1",
|
||||
"value": "172.17.46.91"
|
||||
},
|
||||
{
|
||||
"name": "management_ip2",
|
||||
"value": "172.17.46.92"
|
||||
},
|
||||
{
|
||||
"name": "subnet_mask",
|
||||
"value": "255.255.254.0"
|
||||
},
|
||||
{
|
||||
"name": "default_gateway",
|
||||
"value": "172.17.47.254"
|
||||
},
|
||||
{
|
||||
"name": "primary_dns",
|
||||
"value": "172.17.32.11"
|
||||
},
|
||||
{
|
||||
"name": "secondary_dns",
|
||||
"value": "8.8.8.8"
|
||||
},
|
||||
{
|
||||
"name": "domain_name",
|
||||
"value": "il.reduxio"
|
||||
}
|
||||
],
|
||||
"snmp": [
|
||||
{
|
||||
"Name": "trap_destination",
|
||||
"value": None
|
||||
},
|
||||
{
|
||||
"Name": "udp_port",
|
||||
"value": "162"
|
||||
},
|
||||
{
|
||||
"Name": "community",
|
||||
"value": "public"
|
||||
}
|
||||
],
|
||||
"email_notification": [
|
||||
{
|
||||
"Name": "smtp_server",
|
||||
"value": None
|
||||
},
|
||||
{
|
||||
"Name": "tcp_port",
|
||||
"value": None
|
||||
},
|
||||
{
|
||||
"Name": "smtp_authentication",
|
||||
"value": "None"
|
||||
},
|
||||
{
|
||||
"Name": "user_name",
|
||||
"value": None
|
||||
},
|
||||
{
|
||||
"Name": "sender_address",
|
||||
"value": None
|
||||
}
|
||||
],
|
||||
"email_recipient_list": [
|
||||
{
|
||||
"email": None
|
||||
}
|
||||
],
|
||||
"directories": [
|
||||
{
|
||||
"name": "history_policies/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
TEST_ASSIGN_LUN_NUM = 7
|
||||
|
||||
ISCSI_CONNECTION_INFO_NO_MULTIPATH = {
|
||||
"driver_volume_type": "iscsi",
|
||||
"data": {
|
||||
"target_discovered": False,
|
||||
"discard": False,
|
||||
"volume_id": VOLUME["id"],
|
||||
"target_lun": TEST_ASSIGN_LUN_NUM,
|
||||
"target_iqn": "iqn.2013-12.com.reduxio:af4032f00014000e",
|
||||
"target_portal": "10.46.93.11:3260",
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
connection_copied = copy.deepcopy(
|
||||
ISCSI_CONNECTION_INFO_NO_MULTIPATH["data"]
|
||||
)
|
||||
connection_copied.update({
|
||||
"target_luns": [TEST_ASSIGN_LUN_NUM] * 4,
|
||||
"target_iqns": ["iqn.2013-12.com.reduxio:af4032f00014000e",
|
||||
"iqn.2013-12.com.reduxio:af4032f00014000e",
|
||||
"iqn.2013-12.com.reduxio:af4032f00014000e",
|
||||
"iqn.2013-12.com.reduxio:af4032f00014000e"],
|
||||
"target_portals": ["10.46.93.11:3260", "10.46.93.22:3260",
|
||||
"10.64.93.11:3260", "10.64.93.22:3260"]
|
||||
})
|
||||
|
||||
ISCSI_CONNECTION_INFO = {
|
||||
"driver_volume_type": "iscsi",
|
||||
"data": connection_copied
|
||||
}
|
||||
|
||||
|
||||
def mock_api(to_mock=False):
|
||||
def client_mock_wrapper(func):
|
||||
|
||||
def inner_client_mock(self, *args, **kwargs):
|
||||
rdx_cli_api.ReduxioAPI._connect = mock.Mock()
|
||||
if to_mock:
|
||||
self.driver = rdx_iscsi_driver.ReduxioISCSIDriver(
|
||||
configuration=self.mock_config)
|
||||
self.mock_api = mock.Mock(spec=rdx_cli_api.ReduxioAPI)
|
||||
self.driver.rdxApi = self.mock_api
|
||||
else:
|
||||
self.driver = rdx_iscsi_driver.ReduxioISCSIDriver(
|
||||
configuration=self.mock_config)
|
||||
self.driver.do_setup(None)
|
||||
func(self, *args)
|
||||
|
||||
return inner_client_mock
|
||||
|
||||
return client_mock_wrapper
|
||||
|
||||
|
||||
class ReduxioISCSIDriverTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(ReduxioISCSIDriverTestCase, self).setUp()
|
||||
self.mock_config = mock.Mock()
|
||||
self.mock_config.san_ip = TARGET
|
||||
self.mock_config.san_login = TARGET_USER
|
||||
self.mock_config.san_password = TARGET_PASSWORD
|
||||
self.mock_config.volume_backend_name = VOLUME_BACKEND_NAME
|
||||
self.driver = None # type: ReduxioISCSIDriver
|
||||
|
||||
@staticmethod
|
||||
def generate_random_uuid():
|
||||
return ''.join(
|
||||
random.choice(string.ascii_uppercase + string.digits) for _ in
|
||||
range(rdx_iscsi_driver.RDX_CLI_MAX_VOL_LENGTH))
|
||||
|
||||
@mock_api(False)
|
||||
def test_cinder_id_to_rdx(self):
|
||||
random_uuid1 = self.generate_random_uuid()
|
||||
random_uuid2 = self.generate_random_uuid()
|
||||
result1 = self.driver._cinder_id_to_rdx(random_uuid1)
|
||||
result2 = self.driver._cinder_id_to_rdx(random_uuid2)
|
||||
self.assertEqual(rdx_iscsi_driver.RDX_CLI_MAX_VOL_LENGTH, len(result1))
|
||||
self.assertEqual(rdx_iscsi_driver.RDX_CLI_MAX_VOL_LENGTH, len(result2))
|
||||
self.assertNotEqual(result1, result2)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_create_volume(self, mock_run_cmd):
|
||||
self.driver.create_volume(VOLUME)
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd("volumes new",
|
||||
argument=VOLUME_RDX_NAME,
|
||||
flags=[
|
||||
["size", VOLUME["size"]],
|
||||
["description",
|
||||
VOLUME_RDX_DESC]
|
||||
])
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_manage_existing(self, mock_run_cmd):
|
||||
source_name = 'test-source'
|
||||
self.driver.rdxApi.find_volume_by_name = mock.Mock()
|
||||
self.driver.rdxApi.find_volume_by_name.return_value = {
|
||||
'name': source_name,
|
||||
'description': None
|
||||
|
||||
}
|
||||
self.driver.manage_existing(VOLUME, {'source-name': source_name})
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd("volumes update",
|
||||
argument=source_name,
|
||||
flags=[
|
||||
["new-name", VOLUME_RDX_NAME],
|
||||
["description",
|
||||
VOLUME_RDX_DESC]
|
||||
])
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
self.driver.rdxApi.find_volume_by_name.return_value = {
|
||||
'name': source_name,
|
||||
'description': "openstack_1234"
|
||||
}
|
||||
|
||||
self.assertRaises(
|
||||
exception.ManageExistingAlreadyManaged,
|
||||
self.driver.manage_existing,
|
||||
VOLUME, {'source-name': source_name}
|
||||
)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_manage_existing_get_size(self, mock_run_cmd):
|
||||
source_name = 'test-source'
|
||||
self.driver.rdxApi.find_volume_by_name = mock.Mock()
|
||||
|
||||
vol_cli_ret = {
|
||||
'name': source_name,
|
||||
'description': None,
|
||||
"size": units.Gi * 10
|
||||
}
|
||||
source_vol = {'source-name': source_name}
|
||||
|
||||
self.driver.rdxApi.find_volume_by_name.return_value = vol_cli_ret
|
||||
ret = self.driver.manage_existing_get_size(VOLUME, source_vol)
|
||||
self.assertEqual(10, ret)
|
||||
|
||||
vol_cli_ret["size"] = units.Gi * 9
|
||||
self.driver.rdxApi.find_volume_by_name.return_value = vol_cli_ret
|
||||
ret = self.driver.manage_existing_get_size(VOLUME, source_vol)
|
||||
self.assertNotEqual(10, ret)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_unmanage(self, mock_run_cmd):
|
||||
source_name = 'test-source'
|
||||
self.driver.rdxApi.find_volume_by_name = mock.Mock()
|
||||
self.driver.rdxApi.find_volume_by_name.return_value = {
|
||||
'name': source_name,
|
||||
'description': "openstack_1234"
|
||||
|
||||
}
|
||||
self.driver.unmanage(VOLUME)
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes update",
|
||||
argument=VOLUME_RDX_NAME,
|
||||
flags=[["description", ""]])
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_delete_volume(self, mock_run_cmd):
|
||||
self.driver.delete_volume(VOLUME)
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes delete {} -force".format(VOLUME_RDX_NAME))
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_create_volume_from_snapshot(self, mock_run_cmd):
|
||||
self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT)
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes clone",
|
||||
argument=SRC_VOL_RDX_NAME,
|
||||
flags={
|
||||
"name": VOLUME_RDX_NAME,
|
||||
"bookmark": SNAPSHOT_RDX_NAME,
|
||||
"description": VOLUME_RDX_DESC}
|
||||
)
|
||||
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
# Test resize
|
||||
bigger_vol = copy.deepcopy(VOLUME)
|
||||
bigger_size = SNAPSHOT['volume_size'] + 10
|
||||
bigger_vol['size'] = bigger_size
|
||||
|
||||
self.driver.create_volume_from_snapshot(bigger_vol, SNAPSHOT)
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd("volumes update",
|
||||
argument=VOLUME_RDX_NAME,
|
||||
flags={"size": bigger_size})
|
||||
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_create_cloned_volume(self, mock_run_cmd):
|
||||
self.driver.create_cloned_volume(VOLUME, SRC_VOL)
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes clone",
|
||||
argument=SRC_VOL_RDX_NAME,
|
||||
flags={"name": VOLUME_RDX_NAME, "description": VOLUME_RDX_DESC})
|
||||
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
# Test clone from date
|
||||
backdated_clone = copy.deepcopy(VOLUME)
|
||||
clone_date = "02/17/2015-11:39:00"
|
||||
backdated_clone["metadata"]["backdate"] = clone_date
|
||||
|
||||
self.driver.create_cloned_volume(backdated_clone, SRC_VOL)
|
||||
expected_cmd.add_flag("timestamp", clone_date)
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_create_snapshot(self, mock_run_cmd):
|
||||
self.driver.create_snapshot(SNAPSHOT)
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes bookmark",
|
||||
argument=SRC_VOL_RDX_NAME,
|
||||
flags={"name": SNAPSHOT_RDX_NAME, "type": "manual"})
|
||||
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
backdated_snap = copy.deepcopy(SNAPSHOT)
|
||||
clone_date = "02/17/2015-11:39:00"
|
||||
backdated_snap["metadata"]["backdate"] = clone_date
|
||||
|
||||
self.driver.create_snapshot(backdated_snap)
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes bookmark",
|
||||
argument=SRC_VOL_RDX_NAME,
|
||||
flags={
|
||||
"name": SNAPSHOT_RDX_NAME,
|
||||
"type": "manual",
|
||||
"timestamp": clone_date}
|
||||
)
|
||||
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_delete_snapshot(self, mock_run_cmd):
|
||||
self.driver.delete_snapshot(SNAPSHOT)
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd("volumes delete-bookmark",
|
||||
argument=SRC_VOL_RDX_NAME,
|
||||
flags={"name": SNAPSHOT_RDX_NAME})
|
||||
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_get_volume_stats(self, mock_run_cmd):
|
||||
pass
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_extend_volume(self, mock_run_cmd):
|
||||
new_size = VOLUME["size"] + 1
|
||||
self.driver.extend_volume(VOLUME, new_size)
|
||||
|
||||
expected_cmd = rdx_cli_api.RdxApiCmd("volumes update",
|
||||
argument=VOLUME_RDX_NAME,
|
||||
flags={"size": new_size})
|
||||
|
||||
mock_run_cmd.assert_called_with(expected_cmd)
|
||||
|
||||
def settings_side_effect(*args):
|
||||
if args[0].cmd == "settings ls":
|
||||
return LS_SETTINGS
|
||||
else:
|
||||
return mock.Mock()
|
||||
|
||||
def get_single_assignment_side_effect(*args, **kwargs):
|
||||
if "raise_on_non_exists" in kwargs:
|
||||
raise_given = kwargs["raise_on_non_exists"]
|
||||
else:
|
||||
raise_given = True
|
||||
if (raise_given is True) or (raise_given is None):
|
||||
return {
|
||||
"host": kwargs["host"],
|
||||
"vol": kwargs["vol"],
|
||||
"lun": TEST_ASSIGN_LUN_NUM
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd",
|
||||
side_effect=settings_side_effect)
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "get_single_assignment",
|
||||
side_effect=get_single_assignment_side_effect)
|
||||
@mock_api(False)
|
||||
def test_initialize_connection(self, mock_list_assignmnet, mock_run_cmd):
|
||||
generated_host_name = "openstack-123456789012"
|
||||
self.driver.rdxApi.list_hosts = mock.Mock()
|
||||
self.driver.rdxApi.list_hosts.return_value = []
|
||||
self.driver._generate_initiator_name = mock.Mock()
|
||||
self.driver._generate_initiator_name.return_value = generated_host_name
|
||||
|
||||
ret_connection_info = self.driver.initialize_connection(VOLUME,
|
||||
CONNECTOR)
|
||||
|
||||
create_host_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"hosts new",
|
||||
argument=generated_host_name,
|
||||
flags={"iscsi-name": CONNECTOR["initiator"]}
|
||||
)
|
||||
assign_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes assign",
|
||||
argument=VOLUME_RDX_NAME,
|
||||
flags={"host": generated_host_name}
|
||||
)
|
||||
|
||||
calls = [
|
||||
mock.call.driver._run_cmd(create_host_cmd),
|
||||
mock.call.driver._run_cmd(assign_cmd)
|
||||
]
|
||||
|
||||
mock_run_cmd.assert_has_calls(calls)
|
||||
self.assertDictMatch(
|
||||
ret_connection_info,
|
||||
ISCSI_CONNECTION_INFO_NO_MULTIPATH
|
||||
)
|
||||
|
||||
connector = copy.deepcopy(CONNECTOR)
|
||||
connector["multipath"] = True
|
||||
|
||||
ret_connection_info = self.driver.initialize_connection(VOLUME,
|
||||
connector)
|
||||
|
||||
create_host_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"hosts new",
|
||||
argument=generated_host_name,
|
||||
flags={"iscsi-name": CONNECTOR["initiator"]})
|
||||
|
||||
assign_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes assign",
|
||||
argument=VOLUME_RDX_NAME,
|
||||
flags={"host": generated_host_name}
|
||||
)
|
||||
|
||||
calls = [
|
||||
mock.call.driver._run_cmd(create_host_cmd),
|
||||
mock.call.driver._run_cmd(assign_cmd)
|
||||
]
|
||||
|
||||
mock_run_cmd.assert_has_calls(calls)
|
||||
self.assertDictMatch(ret_connection_info, ISCSI_CONNECTION_INFO)
|
||||
|
||||
self.driver.rdxApi.list_hosts.return_value = [{
|
||||
"iscsi_name": CONNECTOR["initiator"],
|
||||
"name": generated_host_name
|
||||
}]
|
||||
|
||||
ret_connection_info = self.driver.initialize_connection(VOLUME,
|
||||
connector)
|
||||
|
||||
mock_run_cmd.assert_has_calls([mock.call.driver._run_cmd(assign_cmd)])
|
||||
|
||||
self.assertDictMatch(ISCSI_CONNECTION_INFO, ret_connection_info)
|
||||
|
||||
@mock.patch.object(rdx_cli_api.ReduxioAPI, "_run_cmd")
|
||||
@mock_api(False)
|
||||
def test_terminate_connection(self, mock_run_cmd):
|
||||
generated_host_name = "openstack-123456789012"
|
||||
self.driver.rdxApi.list_hosts = mock.Mock()
|
||||
self.driver.rdxApi.list_hosts.return_value = [{
|
||||
"iscsi_name": CONNECTOR["initiator"],
|
||||
"name": generated_host_name
|
||||
}]
|
||||
|
||||
self.driver.terminate_connection(VOLUME, CONNECTOR)
|
||||
|
||||
unassign_cmd = rdx_cli_api.RdxApiCmd(
|
||||
"volumes unassign",
|
||||
argument=VOLUME_RDX_NAME,
|
||||
flags={"host": generated_host_name}
|
||||
)
|
||||
|
||||
mock_run_cmd.assert_has_calls(
|
||||
[mock.call.driver._run_cmd(unassign_cmd)])
|
0
cinder/volume/drivers/reduxio/__init__.py
Normal file
0
cinder/volume/drivers/reduxio/__init__.py
Normal file
542
cinder/volume/drivers/reduxio/rdx_cli_api.py
Normal file
542
cinder/volume/drivers/reduxio/rdx_cli_api.py
Normal file
@ -0,0 +1,542 @@
|
||||
# Copyright (c) 2016 Reduxio Systems
|
||||
# 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.
|
||||
"""Reduxio CLI intrface class for Reduxio Cinder Driver."""
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import eventlet
|
||||
from oslo_log import log as logging
|
||||
import paramiko
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder import utils
|
||||
from cinder.i18n import _, _LE, _LI
|
||||
|
||||
|
||||
CONNECTION_RETRY_NUM = 5
|
||||
|
||||
VOLUMES = "volumes"
|
||||
HOSTS = "hosts"
|
||||
HG_DIR = "hostgroups"
|
||||
NEW_COMMAND = "new"
|
||||
UPDATE_COMMAND = "update"
|
||||
LS_COMMAND = "ls"
|
||||
DELETE_COMMAND = "delete"
|
||||
LIST_ASSIGN_CMD = "list-assignments"
|
||||
CLI_DATE_FORMAT = "%m-%Y-%d %H:%M:%S"
|
||||
CONNECT_LOCK_NAME = "reduxio_cli_Lock"
|
||||
CLI_CONNECTION_RETRY_SLEEP = 5
|
||||
CLI_SSH_CMD_TIMEOUT = 20
|
||||
CLI_CONNECT_TIMEOUT = 50
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RdxApiCmd(object):
|
||||
"""A Builder class for Reduxio CLI Command."""
|
||||
|
||||
def __init__(self, cmd_prefix, argument=None, flags=None,
|
||||
boolean_flags=None, force=None):
|
||||
"""Initialize a command object."""
|
||||
if isinstance(cmd_prefix, list):
|
||||
cmd_prefix = map(lambda x: x.strip(), cmd_prefix)
|
||||
self.cmd = " ".join(cmd_prefix)
|
||||
else:
|
||||
self.cmd = cmd_prefix
|
||||
|
||||
self.arg = None
|
||||
self.flags = {}
|
||||
self.booleanFlags = {}
|
||||
|
||||
if argument is not None:
|
||||
self.set_argument(argument)
|
||||
|
||||
if flags is not None:
|
||||
if isinstance(flags, list):
|
||||
for flag in flags:
|
||||
self.add_flag(flag[0], flag[1])
|
||||
else:
|
||||
for key in flags:
|
||||
self.add_flag(key, flags[key])
|
||||
|
||||
if boolean_flags is not None:
|
||||
for flag in boolean_flags:
|
||||
self.add_boolean_flag(flag)
|
||||
|
||||
if force:
|
||||
self.force_command()
|
||||
|
||||
def set_argument(self, value):
|
||||
"""Set a command argument."""
|
||||
self.arg = value
|
||||
|
||||
def add_flag(self, name, value):
|
||||
"""Set a flag and its value."""
|
||||
if value is not None:
|
||||
self.flags[name.strip()] = value
|
||||
|
||||
def add_boolean_flag(self, name):
|
||||
"""Set a boolean flag."""
|
||||
if name is not None:
|
||||
self.booleanFlags[name.strip()] = True
|
||||
|
||||
def build(self):
|
||||
"""Return the command line which represents the command object."""
|
||||
argument_str = "" if self.arg is None else self.arg
|
||||
flags_str = ""
|
||||
|
||||
for key in sorted(self.flags):
|
||||
flags_str += (" -%(flag)s \"%(value)s\"" %
|
||||
{"flag": key, "value": self.flags[key]})
|
||||
|
||||
for booleanFlag in sorted(self.booleanFlags):
|
||||
flags_str += " -%s" % booleanFlag
|
||||
|
||||
return ("%(cmd)s %(arg)s%(flag)s" %
|
||||
{"cmd": self.cmd, "arg": argument_str, "flag": flags_str})
|
||||
|
||||
def force_command(self):
|
||||
"""Add a force flag."""
|
||||
self.add_boolean_flag("force")
|
||||
|
||||
def set_json_output(self):
|
||||
"""Add a json output flag."""
|
||||
self.add_flag("output", "json")
|
||||
|
||||
def __str__(self):
|
||||
"""Override toString."""
|
||||
return self.build()
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Compare commands based on their str command representations."""
|
||||
if isinstance(other, self.__class__):
|
||||
return six.text_type(self).strip() == six.text_type(other).strip()
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class ReduxioAPI(object):
|
||||
def __init__(self, host, user, password):
|
||||
"""Get credentials and connects to Reduxio CLI."""
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.ssh = None # type: paramiko.SSHClient
|
||||
self._connect()
|
||||
|
||||
def _reconnect_if_needed(self):
|
||||
if not self.connected:
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
self.connected = False
|
||||
self.ssh = paramiko.SSHClient()
|
||||
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
self.ssh.connect(self.host, username=self.user,
|
||||
password=self.password,
|
||||
timeout=CLI_CONNECT_TIMEOUT)
|
||||
self.connected = True
|
||||
except paramiko.ssh_exception.AuthenticationException:
|
||||
raise exception.RdxAPIConnectionException(_(
|
||||
"Authentication Error. Check login credentials"))
|
||||
except Exception:
|
||||
LOG.exception(_LE("Exception in connecting to Reduxio CLI"))
|
||||
raise exception.RdxAPIConnectionException(_(
|
||||
"Failed to create ssh connection to Reduxio."
|
||||
" Please check network connection or Reduxio hostname/IP."))
|
||||
|
||||
@utils.synchronized(CONNECT_LOCK_NAME, external=True)
|
||||
def _run_cmd(self, cmd):
|
||||
"""Run the command and returns a dictionary of the response.
|
||||
|
||||
On failure, the function retries the command. After retry threshold
|
||||
the function throws an error.
|
||||
"""
|
||||
cmd.set_json_output()
|
||||
LOG.info(_LI("Running cmd: %s"), cmd)
|
||||
success = False
|
||||
for x in range(1, CONNECTION_RETRY_NUM):
|
||||
try:
|
||||
self._reconnect_if_needed()
|
||||
stdin, stdout, stderr = self.ssh.exec_command(
|
||||
command=six.text_type(cmd), timeout=CLI_SSH_CMD_TIMEOUT)
|
||||
success = True
|
||||
break
|
||||
except Exception:
|
||||
LOG.exception(_LE("Error in running Reduxio CLI command"))
|
||||
LOG.error(
|
||||
_LE("retrying(%(cur)s/%(overall)s)"),
|
||||
{'cur': x, 'overall': CONNECTION_RETRY_NUM}
|
||||
)
|
||||
self.connected = False
|
||||
eventlet.sleep(CLI_CONNECTION_RETRY_SLEEP)
|
||||
|
||||
if not success:
|
||||
raise exception.RdxAPIConnectionException(_(
|
||||
"Failed to connect to Redxuio CLI."
|
||||
" Check your username, password or Reduxio Hostname/IP"))
|
||||
|
||||
str_out = stdout.read()
|
||||
# Python 2.7/3.4 compatibility with the decode method
|
||||
if hasattr(str_out, "decode"):
|
||||
data = json.loads(str_out.decode("utf8"))
|
||||
else:
|
||||
data = json.loads(str_out)
|
||||
|
||||
if stdout.channel.recv_exit_status() != 0:
|
||||
LOG.error(_LE("Failed running cli command: %s"), data["msg"])
|
||||
raise exception.RdxAPICommandException(data["msg"])
|
||||
|
||||
LOG.debug("Command output is: %s", str_out)
|
||||
|
||||
return data["data"]
|
||||
|
||||
@staticmethod
|
||||
def _utc_to_cli_date(utc_date):
|
||||
if utc_date is None:
|
||||
return None
|
||||
date = datetime.datetime.fromtimestamp(utc_date)
|
||||
return date.strftime(CLI_DATE_FORMAT)
|
||||
|
||||
# Volumes
|
||||
|
||||
def create_volume(self, name, size, description=None, historypolicy=None,
|
||||
blocksize=None):
|
||||
"""Create a new volume."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, NEW_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("size", size)
|
||||
cmd.add_flag("description", description)
|
||||
cmd.add_flag("policy", historypolicy)
|
||||
cmd.add_flag("blocksize", blocksize)
|
||||
|
||||
self._run_cmd(cmd)
|
||||
|
||||
def list_volumes(self):
|
||||
"""List all volumes."""
|
||||
return self._run_cmd(RdxApiCmd(cmd_prefix=[VOLUMES, LS_COMMAND]))[
|
||||
"volumes"]
|
||||
|
||||
def list_clones(self, name):
|
||||
"""List all clones of a volume."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "list-clones"])
|
||||
|
||||
cmd.set_argument(name)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def find_volume_by_name(self, name):
|
||||
"""Get a single volume by its name."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[LS_COMMAND, VOLUMES + "/" + name])
|
||||
|
||||
return self._run_cmd(cmd)["volumes"][0]
|
||||
|
||||
def find_volume_by_wwid(self, wwid):
|
||||
"""Get a single volume by its WWN."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "find-by-wwid"])
|
||||
|
||||
cmd.set_argument(wwid)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def delete_volume(self, name):
|
||||
"""Delete a volume."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, DELETE_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.force_command()
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def update_volume(self, name, new_name=None, description=None, size=None,
|
||||
history_policy=None):
|
||||
"""Update volume's properties. None value keeps the current value."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, UPDATE_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("size", size)
|
||||
cmd.add_flag("new-name", new_name)
|
||||
cmd.add_flag("policy", history_policy)
|
||||
cmd.add_flag("size", size)
|
||||
cmd.add_flag("description", description)
|
||||
|
||||
self._run_cmd(cmd)
|
||||
|
||||
def revert_volume(self, name, utc_date=None, bookmark_name=None):
|
||||
"""Revert a volume to a specific date or by a bookmark."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "revert"])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("timestamp", ReduxioAPI._utc_to_cli_date(utc_date))
|
||||
cmd.add_flag("bookmark", bookmark_name)
|
||||
cmd.force_command()
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def clone_volume(self, parent_name, clone_name, utc_date=None,
|
||||
str_date=None, bookmark_name=None, description=None):
|
||||
"""Clone a volume our of an existing volume."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "clone"])
|
||||
|
||||
cmd.set_argument(parent_name)
|
||||
cmd.add_flag("name", clone_name)
|
||||
if str_date is not None:
|
||||
cmd.add_flag("timestamp", str_date)
|
||||
else:
|
||||
cmd.add_flag("timestamp", ReduxioAPI._utc_to_cli_date(utc_date))
|
||||
cmd.add_flag("bookmark", bookmark_name)
|
||||
cmd.add_flag("description", description)
|
||||
|
||||
self._run_cmd(cmd)
|
||||
|
||||
def list_vol_bookmarks(self, vol):
|
||||
"""List all bookmarks of a volume."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "list-bookmarks"])
|
||||
|
||||
cmd.set_argument(vol)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def add_vol_bookmark(self, vol, bm_name, utc_date=None, str_date=None,
|
||||
bm_type=None):
|
||||
"""Create a new bookmark for a given volume."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "bookmark"])
|
||||
|
||||
cmd.set_argument(vol)
|
||||
cmd.add_flag("name", bm_name)
|
||||
if str_date is not None:
|
||||
cmd.add_flag("timestamp", str_date)
|
||||
else:
|
||||
cmd.add_flag("timestamp", ReduxioAPI._utc_to_cli_date(utc_date))
|
||||
cmd.add_flag("type", bm_type)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def delete_vol_bookmark(self, vol, bm_name):
|
||||
"""Delete a volume's bookmark."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "delete-bookmark"])
|
||||
|
||||
cmd.set_argument(vol)
|
||||
cmd.add_flag("name", bm_name)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
# Hosts
|
||||
|
||||
def list_hosts(self):
|
||||
"""List all hosts."""
|
||||
return self._run_cmd(RdxApiCmd(cmd_prefix=[HOSTS, LS_COMMAND]))[
|
||||
"hosts"]
|
||||
|
||||
def create_host(self, name, iscsi_name, description=None, user_chap=None,
|
||||
pwd_chap=None):
|
||||
"""Create a new host."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HOSTS, NEW_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("iscsi-name", iscsi_name)
|
||||
cmd.add_flag("description", description)
|
||||
cmd.add_flag("user-chap", user_chap)
|
||||
cmd.add_flag("pwd-chap", pwd_chap)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def delete_host(self, name):
|
||||
"""Delete an existing host."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HOSTS, DELETE_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.force_command()
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def update_host(self, name, new_name=None, description=None,
|
||||
user_chap=None, pwd_chap=None):
|
||||
"""Update host's attributes."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HOSTS, UPDATE_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("new-name", new_name)
|
||||
cmd.add_flag("user-chap", user_chap)
|
||||
cmd.add_flag("pwd-chap", pwd_chap)
|
||||
cmd.add_flag("description", description)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
# HostGroups
|
||||
|
||||
def list_hostgroups(self):
|
||||
"""List all hostgroups."""
|
||||
return self._run_cmd(RdxApiCmd(cmd_prefix=[HG_DIR, LS_COMMAND]))[
|
||||
"hostgroups"]
|
||||
|
||||
def create_hostgroup(self, name, description=None):
|
||||
"""Create a new hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HG_DIR, NEW_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("description", description)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def delete_hostgroup(self, name):
|
||||
"""Delete an existing hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HG_DIR, DELETE_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.force_command()
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def update_hostgroup(self, name, new_name=None, description=None):
|
||||
"""Update an existing hostgroup's attributes."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HG_DIR, UPDATE_COMMAND])
|
||||
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("new-name", new_name)
|
||||
cmd.add_flag("description", description)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def list_hosts_in_hostgroup(self, name):
|
||||
"""List all hosts that are part of the given hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HG_DIR, "list-hosts"])
|
||||
cmd.set_argument(name)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def add_host_to_hostgroup(self, name, host_name):
|
||||
"""Join a host to a hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HG_DIR, "add-host"])
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("host", host_name)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def remove_host_from_hostgroup(self, name, host_name):
|
||||
"""Remove a host from a hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HG_DIR, "remove-host"])
|
||||
cmd.set_argument(name)
|
||||
cmd.add_flag("host", host_name)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def add_hg_bookmark(self, hg_name, bm_name, utc_date=None, str_date=None,
|
||||
bm_type=None):
|
||||
"""Bookmark all volumes that are assigned to the hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[HG_DIR, "add-bookmark"])
|
||||
|
||||
cmd.set_argument(hg_name)
|
||||
cmd.add_flag("name", bm_name)
|
||||
if str_date is not None:
|
||||
cmd.add_flag("timestamp", str_date)
|
||||
else:
|
||||
cmd.add_flag("timestamp", ReduxioAPI._utc_to_cli_date(utc_date))
|
||||
cmd.add_flag("type", bm_type)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
# Assignments
|
||||
|
||||
def assign(self, vol_name, host_name=None, hostgroup_name=None, lun=None):
|
||||
"""Create an assignment between a volume to host/hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "assign"])
|
||||
|
||||
cmd.set_argument(vol_name)
|
||||
cmd.add_flag("host", host_name)
|
||||
cmd.add_flag("group", hostgroup_name)
|
||||
cmd.add_flag("lun", lun)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def unassign(self, vol_name, host_name=None, hostgroup_name=None):
|
||||
"""Unassign a volume from a host/hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, "unassign"])
|
||||
|
||||
cmd.set_argument(vol_name)
|
||||
cmd.add_flag("host", host_name)
|
||||
cmd.add_flag("group", hostgroup_name)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def list_assignments(self, vol=None, host=None, hg=None):
|
||||
"""List all assignments for a given volume/host/hostgroup."""
|
||||
cmd = RdxApiCmd(cmd_prefix=[VOLUMES, LIST_ASSIGN_CMD])
|
||||
if vol is not None:
|
||||
cmd.set_argument(vol)
|
||||
elif host is not None:
|
||||
cmd = RdxApiCmd(cmd_prefix=[HOSTS, LIST_ASSIGN_CMD])
|
||||
cmd.set_argument(host)
|
||||
elif host is not None:
|
||||
cmd = RdxApiCmd(cmd_prefix=[HG_DIR, LIST_ASSIGN_CMD])
|
||||
cmd.set_argument(hg)
|
||||
|
||||
return self._run_cmd(cmd)
|
||||
|
||||
def get_single_assignment(self, vol, host, raise_on_non_exists=True):
|
||||
"""Get a single assignment details between a host and a volume."""
|
||||
for assign in self.list_assignments(vol=vol):
|
||||
if assign["host"] == host:
|
||||
return assign
|
||||
if raise_on_non_exists:
|
||||
raise exception.RdxAPICommandException(_(
|
||||
"No such assignment vol:%(vol)s, host:%(host)s") %
|
||||
{'vol': vol, 'host': host}
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
# Settings
|
||||
|
||||
def get_settings(self):
|
||||
"""List all Reduxio settings."""
|
||||
cli_hash = self._run_cmd(
|
||||
RdxApiCmd(cmd_prefix=["settings", LS_COMMAND]))
|
||||
return self._translate_settings_to_hash(cli_hash)
|
||||
|
||||
@staticmethod
|
||||
def _translate_settings_to_hash(cli_hash):
|
||||
new_hash = {}
|
||||
for key, value in cli_hash.items():
|
||||
if key == "directories":
|
||||
continue
|
||||
if key == "email_recipient_list":
|
||||
continue
|
||||
|
||||
new_hash[key] = {}
|
||||
for inter_hash in value:
|
||||
if "Name" in inter_hash:
|
||||
new_hash[key][inter_hash["Name"]] = inter_hash["value"]
|
||||
else:
|
||||
new_hash[key][inter_hash["name"]] = inter_hash["value"]
|
||||
return new_hash
|
||||
|
||||
# Statistics
|
||||
|
||||
def get_savings_ratio(self):
|
||||
"""Get current savings ratio."""
|
||||
return self._run_cmd(RdxApiCmd(cmd_prefix=["system", "status"]))[0][
|
||||
"savings-ratio"]
|
||||
|
||||
def get_current_space_usage(self):
|
||||
"""Get current space usage."""
|
||||
cmd = RdxApiCmd(cmd_prefix=["statistics", "space-usage"])
|
||||
return self._run_cmd(cmd)[0]
|
502
cinder/volume/drivers/reduxio/rdx_iscsi_driver.py
Normal file
502
cinder/volume/drivers/reduxio/rdx_iscsi_driver.py
Normal file
@ -0,0 +1,502 @@
|
||||
# Copyright (c) 2016 Reduxio Systems
|
||||
# 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 Reduxio."""
|
||||
import random
|
||||
import string
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import units
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder import utils as cinder_utils
|
||||
from cinder.i18n import _, _LI, _LW
|
||||
import cinder.interface as cinder_interface
|
||||
from cinder.volume.drivers.reduxio import rdx_cli_api
|
||||
from cinder.volume.drivers.san import san
|
||||
|
||||
|
||||
# Constants
|
||||
REDUXIO_NAME_PREFIX_NUMERIC_REPLACEMENT = "a"
|
||||
REDUXIO_CLI_HOST_RAND_LENGTH = 12
|
||||
REDUXIO_CLI_HOST_PREFIX = 'openstack-'
|
||||
REDUXIO_STORAGE_PROTOCOL = 'iSCSI'
|
||||
REDUXIO_VENDOR_NAME = 'Reduxio'
|
||||
AGENT_TYPE_KEY = "agent-type"
|
||||
AGENT_TYPE_OPENSTACK = "openstack"
|
||||
EXTERNAL_VOL_ID_KEY = "external-vol-id"
|
||||
METADATA_KEY = "metadata"
|
||||
BACKDATE_META_FIELD = "backdate"
|
||||
RDX_CLI_MAX_VOL_LENGTH = 31
|
||||
DRIVER_VERSION = '1.0.1'
|
||||
HX550_INITIAL_PHYSICAL_CAPACITY = 32 * 1024
|
||||
HX550_CAPACITY_LIMIT = 200 * 1024
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@cinder_interface.volumedriver
|
||||
class ReduxioISCSIDriver(san.SanISCSIDriver):
|
||||
"""OpenStack driver to support Reduxio storage systems.
|
||||
|
||||
Version history:
|
||||
1.0.0 - Initial version - volume management, snapshots, BackDating(TM).
|
||||
1.0.1 - Capacity stats, fixed error handling for volume deletions.
|
||||
"""
|
||||
VERSION = '1.0.1'
|
||||
CI_WIKI_NAME = "Reduxio_HX550_CI"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize Reduxio ISCSI Driver."""
|
||||
LOG.info(_LI("Initializing Reduxio ISCSI Driver"))
|
||||
super(ReduxioISCSIDriver, self).__init__(*args, **kwargs)
|
||||
self.rdxApi = None # type: rdx_cli_api.ReduxioAPI
|
||||
self._stats = {}
|
||||
|
||||
def _check_config(self):
|
||||
"""Ensure that the flags we care about are set."""
|
||||
required_config = ['san_ip', 'san_login', 'san_password']
|
||||
for attr in required_config:
|
||||
if not getattr(self.configuration, attr, None):
|
||||
raise exception.InvalidInput(reason=_('%s is not set.') % attr)
|
||||
|
||||
def do_setup(self, context):
|
||||
"""Set up the driver."""
|
||||
self._check_config()
|
||||
self.rdxApi = rdx_cli_api.ReduxioAPI(
|
||||
user=self.configuration.san_login,
|
||||
password=self.configuration.san_password,
|
||||
host=self.configuration.san_ip)
|
||||
|
||||
# Reduxio entities names (which are also ids) are restricted to at most
|
||||
# 31 chars. The following function maps cinder unique id to reduxio name.
|
||||
# Reduxio name also cannot begin with a number, so we replace this number
|
||||
# with a constant letter. The probability of a uuid conflict is still low.
|
||||
def _cinder_id_to_rdx(self, cinder_id):
|
||||
normalized = cinder_id.replace("-", "")[:RDX_CLI_MAX_VOL_LENGTH]
|
||||
if normalized[0].isalpha():
|
||||
return normalized
|
||||
else:
|
||||
return REDUXIO_NAME_PREFIX_NUMERIC_REPLACEMENT + normalized[1:]
|
||||
|
||||
# We use Reduxio volume description to represent metadata regarding
|
||||
# the cinder agent, in order to avoid multi managing the same volume
|
||||
# from multiple cinder volume.
|
||||
def _create_vol_managed_description(self, volume):
|
||||
return AGENT_TYPE_OPENSTACK + "_" + volume["name"]
|
||||
|
||||
# This function parses the cli volume description and returns a dictionary
|
||||
# containing the managed data (agent, cinder_volume_id)
|
||||
def _get_managed_info(self, cli_vol):
|
||||
try:
|
||||
splited = cli_vol["description"].split("_")
|
||||
if len(splited) == 0:
|
||||
return {AGENT_TYPE_KEY: None}
|
||||
return {AGENT_TYPE_KEY: splited[0],
|
||||
EXTERNAL_VOL_ID_KEY: splited[1]}
|
||||
except Exception:
|
||||
return {AGENT_TYPE_KEY: None}
|
||||
|
||||
def _get_existing_volume_ref_name(self, existing_ref):
|
||||
"""Return the volume name of an existing ref."""
|
||||
if 'source-name' in existing_ref:
|
||||
vol_name = existing_ref['source-name']
|
||||
else:
|
||||
reason = _("Reference must contain source-name.")
|
||||
raise exception.ManageExistingInvalidReference(
|
||||
existing_ref=existing_ref,
|
||||
reason=reason)
|
||||
|
||||
return vol_name
|
||||
|
||||
@cinder_utils.trace
|
||||
def create_volume(self, volume):
|
||||
"""Create a new volume."""
|
||||
LOG.info(_LI(
|
||||
"Creating a new volume(%(name)s) with size(%(size)s)"),
|
||||
{'name': volume["name"], 'size': volume["size"]})
|
||||
vol_name = self._cinder_id_to_rdx(volume["id"])
|
||||
self.rdxApi.create_volume(
|
||||
name=vol_name,
|
||||
size=volume["size"],
|
||||
description=self._create_vol_managed_description(volume)
|
||||
)
|
||||
|
||||
@cinder_utils.trace
|
||||
def manage_existing(self, volume, external_ref):
|
||||
"""Create a new Cinder volume out of an existing Reduxio volume."""
|
||||
LOG.info(_LI("Manage existing volume(%(cinder_vol)s) "
|
||||
"from Reduxio Volume(%(rdx_vol)s)"),
|
||||
{'cinder_vol': volume["id"],
|
||||
'rdx_vol': external_ref["source-name"]})
|
||||
# Get the volume name from the external reference
|
||||
target_vol_name = self._get_existing_volume_ref_name(external_ref)
|
||||
|
||||
# Get vol info from the volume name obtained from the reference
|
||||
cli_vol = self.rdxApi.find_volume_by_name(target_vol_name)
|
||||
managed_info = self._get_managed_info(cli_vol)
|
||||
|
||||
# Check if volume is already managed by Openstack
|
||||
if managed_info[AGENT_TYPE_KEY] == AGENT_TYPE_OPENSTACK:
|
||||
raise exception.ManageExistingAlreadyManaged(
|
||||
volume_ref=volume['id'])
|
||||
|
||||
# If agent-type is not None then raise exception
|
||||
if not managed_info[AGENT_TYPE_KEY] is None:
|
||||
msg = _('Volume should have agent-type set as None.')
|
||||
raise exception.InvalidVolume(reason=msg)
|
||||
|
||||
new_vol_name = self._cinder_id_to_rdx(volume['id'])
|
||||
|
||||
# edit the volume
|
||||
self.rdxApi.update_volume(
|
||||
target_vol_name,
|
||||
new_name=new_vol_name,
|
||||
description=self._create_vol_managed_description(volume)
|
||||
)
|
||||
|
||||
@cinder_utils.trace
|
||||
def manage_existing_get_size(self, volume, external_ref):
|
||||
"""Return size of an existing volume."""
|
||||
target_vol_name = self._get_existing_volume_ref_name(external_ref)
|
||||
cli_vol = self.rdxApi.find_volume_by_name(target_vol_name)
|
||||
|
||||
return int(cli_vol['size'] / units.Gi)
|
||||
|
||||
@cinder_utils.trace
|
||||
def unmanage(self, volume):
|
||||
"""Remove the specified volume from Cinder management."""
|
||||
LOG.info(_LI("Unmanaging volume(%s)"), volume["id"])
|
||||
vol_name = self._cinder_id_to_rdx(volume['id'])
|
||||
cli_vol = self.rdxApi.find_volume_by_name(vol_name)
|
||||
managed_info = self._get_managed_info(cli_vol)
|
||||
|
||||
if managed_info['agent-type'] != AGENT_TYPE_OPENSTACK:
|
||||
msg = _('Only volumes managed by Openstack can be unmanaged.')
|
||||
raise exception.InvalidVolume(reason=msg)
|
||||
|
||||
# update the agent-type to None
|
||||
self.rdxApi.update_volume(name=vol_name, description="")
|
||||
|
||||
@cinder_utils.trace
|
||||
def delete_volume(self, volume):
|
||||
"""Delete the specified volume."""
|
||||
LOG.info(_LI("Deleting volume(%s)"), volume["id"])
|
||||
try:
|
||||
self.rdxApi.delete_volume(
|
||||
name=self._cinder_id_to_rdx(volume["id"]))
|
||||
except exception.RdxAPICommandException as e:
|
||||
if "No such volume" not in six.text_type(e):
|
||||
raise
|
||||
|
||||
@cinder_utils.trace
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Clone volume from snapshot.
|
||||
|
||||
Extend the volume if the size of the volume is more than the snapshot.
|
||||
"""
|
||||
LOG.info(_LI(
|
||||
"cloning new volume(%(new_vol)s) from snapshot(%(snapshot)s),"
|
||||
" src volume(%(src_vol)s)"),
|
||||
{'new_vol': volume["name"],
|
||||
'snapshot': snapshot["name"],
|
||||
'src_vol': snapshot["volume_name"]}
|
||||
)
|
||||
|
||||
parent_name = self._cinder_id_to_rdx(snapshot["volume_id"])
|
||||
clone_name = self._cinder_id_to_rdx(volume["id"])
|
||||
bookmark_name = self._cinder_id_to_rdx(snapshot["id"])
|
||||
|
||||
self.rdxApi.clone_volume(
|
||||
parent_name=parent_name,
|
||||
clone_name=clone_name,
|
||||
bookmark_name=bookmark_name,
|
||||
description=self._create_vol_managed_description(volume)
|
||||
)
|
||||
|
||||
if volume['size'] > snapshot['volume_size']:
|
||||
self.rdxApi.update_volume(name=clone_name, size=volume["size"])
|
||||
|
||||
@cinder_utils.trace
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Clone volume from existing cinder volume.
|
||||
|
||||
:param volume: The clone volume object.
|
||||
If the volume 'metadata' field contains a 'backdate' key
|
||||
(If using Cinder CLI, should be provided by --meta flag),
|
||||
then we create a clone from the specified time.
|
||||
The 'backdate' metadata value should be in the format of
|
||||
Reduxio CLI date: mm/dd/yyyy-hh:mm:ss.
|
||||
for example: '02/17/2015-11:39:00.
|
||||
Note: Different timezones might be configured
|
||||
for Reduxio and Openstack.
|
||||
The specified date must be related to Reduxio time settings.
|
||||
|
||||
If meta key 'backdate' was not specified,
|
||||
then we create a clone from the volume's current state.
|
||||
:param src_vref: The source volume to clone from
|
||||
:return: None
|
||||
"""
|
||||
LOG.info(_LI("cloning new volume(%(clone)s) from src(%(src)s)"),
|
||||
{'clone': volume['name'], 'src': src_vref['name']})
|
||||
parent_name = self._cinder_id_to_rdx(src_vref["id"])
|
||||
clone_name = self._cinder_id_to_rdx(volume["id"])
|
||||
description = self._create_vol_managed_description(volume)
|
||||
if BACKDATE_META_FIELD in volume[METADATA_KEY]:
|
||||
LOG.info(_LI("Cloning from backdate %s"),
|
||||
volume[METADATA_KEY][BACKDATE_META_FIELD])
|
||||
|
||||
self.rdxApi.clone_volume(
|
||||
parent_name=parent_name,
|
||||
clone_name=clone_name,
|
||||
description=description,
|
||||
str_date=volume[METADATA_KEY][BACKDATE_META_FIELD]
|
||||
)
|
||||
else:
|
||||
LOG.info(_LI("Cloning from now"))
|
||||
self.rdxApi.clone_volume(
|
||||
parent_name=parent_name,
|
||||
clone_name=clone_name,
|
||||
description=description
|
||||
)
|
||||
|
||||
if src_vref['size'] < volume['size']:
|
||||
self.rdxApi.update_volume(name=clone_name, size=volume["size"])
|
||||
|
||||
@cinder_utils.trace
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Create a snapshot from an existing Cinder volume.
|
||||
|
||||
We use Reduxio manual bookmark to represent a snapshot.
|
||||
|
||||
:param snapshot: The snapshot object.
|
||||
If the snapshot 'metadata' field contains a 'backdate' key
|
||||
(If using Cinder CLI, should be provided by --meta flag),
|
||||
then we create a snapshot from the specified time.
|
||||
The 'backdate' metadata value should be in the format of
|
||||
Reduxio CLI date: mm/dd/yyyy-hh:mm:ss.
|
||||
for example: '02/17/2015-11:39:00'.
|
||||
Note: Different timezones might be configured
|
||||
for Reduxio and Openstack.
|
||||
The specified date must be related to Reduxio time settings.
|
||||
|
||||
If meta key 'backdate' was not specified, then we create a snapshot
|
||||
from the volume's current state.
|
||||
:return: None
|
||||
"""
|
||||
LOG.info(_LI(
|
||||
"Creating snapshot(%(snap)s) from volume(%(vol)s)"),
|
||||
{'snap': snapshot['name'], 'vol': snapshot['volume_name']})
|
||||
cli_vol_name = self._cinder_id_to_rdx(snapshot['volume_id'])
|
||||
cli_bookmark_name = self._cinder_id_to_rdx(snapshot['id'])
|
||||
bookmark_type = "manual"
|
||||
if BACKDATE_META_FIELD in snapshot[METADATA_KEY]:
|
||||
self.rdxApi.add_vol_bookmark(vol=cli_vol_name,
|
||||
bm_name=cli_bookmark_name,
|
||||
bm_type=bookmark_type,
|
||||
str_date=snapshot[METADATA_KEY][
|
||||
BACKDATE_META_FIELD]
|
||||
)
|
||||
else:
|
||||
self.rdxApi.add_vol_bookmark(vol=cli_vol_name,
|
||||
bm_name=cli_bookmark_name,
|
||||
bm_type=bookmark_type)
|
||||
|
||||
@cinder_utils.trace
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Delete a snapshot."""
|
||||
LOG.info(_LI("Deleting snapshot(%(snap)s) from volume(%(vol)s)"),
|
||||
{'snap': snapshot['name'], 'vol': snapshot['volume_name']})
|
||||
|
||||
volume_name = self._cinder_id_to_rdx(snapshot['volume_id'])
|
||||
bookmark_name = self._cinder_id_to_rdx(snapshot['id'])
|
||||
try:
|
||||
self.rdxApi.delete_vol_bookmark(vol=volume_name,
|
||||
bm_name=bookmark_name)
|
||||
except exception.RdxAPICommandException as e:
|
||||
if "No such bookmark" not in six.text_type(e):
|
||||
raise
|
||||
|
||||
@cinder_utils.trace
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get Reduxio Storage attributes."""
|
||||
if refresh:
|
||||
backend_name = self.configuration.safe_get(
|
||||
'volume_backend_name') or self.__class__.__name__
|
||||
ratio = self.rdxApi.get_savings_ratio()
|
||||
total = HX550_INITIAL_PHYSICAL_CAPACITY * ratio
|
||||
|
||||
if total > HX550_CAPACITY_LIMIT:
|
||||
total = HX550_CAPACITY_LIMIT
|
||||
|
||||
current_space_usage = self.rdxApi.get_current_space_usage()
|
||||
physical_used = current_space_usage["physical_total"] / units.Gi
|
||||
free = (HX550_INITIAL_PHYSICAL_CAPACITY - physical_used) * ratio
|
||||
|
||||
if free > HX550_CAPACITY_LIMIT:
|
||||
free = HX550_CAPACITY_LIMIT
|
||||
|
||||
self._stats = {
|
||||
'volume_backend_name': backend_name,
|
||||
'vendor_name': REDUXIO_VENDOR_NAME,
|
||||
'driver_version': DRIVER_VERSION,
|
||||
'storage_protocol': REDUXIO_STORAGE_PROTOCOL,
|
||||
'consistencygroup_support': False,
|
||||
'pools': [{
|
||||
"pool_name": backend_name,
|
||||
"total_capacity_gb": total,
|
||||
"free_capacity_gb": free,
|
||||
"reserved_percentage":
|
||||
self.configuration.reserved_percentage,
|
||||
"QoS_support": False,
|
||||
'multiattach': True
|
||||
}]}
|
||||
|
||||
return self._stats
|
||||
|
||||
@cinder_utils.trace
|
||||
def extend_volume(self, volume, new_size):
|
||||
"""Extend an existing volume."""
|
||||
volume_name = self._cinder_id_to_rdx(volume['id'])
|
||||
self.rdxApi.update_volume(volume_name, size=new_size)
|
||||
|
||||
@cinder_utils.trace
|
||||
def _generate_initiator_name(self):
|
||||
"""Generates random host name for reduxio cli."""
|
||||
char_set = string.ascii_lowercase
|
||||
rand_str = ''.join(
|
||||
random.sample(char_set, REDUXIO_CLI_HOST_RAND_LENGTH))
|
||||
return "%s%s" % (REDUXIO_CLI_HOST_PREFIX, rand_str)
|
||||
|
||||
@cinder_utils.trace
|
||||
def _get_target_portal(self, settings, controller, port):
|
||||
network = "iscsi_network%s" % port
|
||||
iscsi_port = six.text_type(
|
||||
settings["network_configuration"]["iscsi_target_tcp_port"])
|
||||
controller_port_key = ("controller_%(controller)s_port_%(port)s"
|
||||
% {"controller": controller, "port": port})
|
||||
return settings[network][controller_port_key] + ":" + iscsi_port
|
||||
|
||||
@cinder_utils.trace
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Driver entry point to attach a volume to an instance."""
|
||||
LOG.info(_LI(
|
||||
"Assigning volume(%(vol)s) with initiator(%(initiator)s)"),
|
||||
{'vol': volume['name'], 'initiator': connector['initiator']})
|
||||
|
||||
initiator_iqn = connector['initiator']
|
||||
vol_rdx_name = self._cinder_id_to_rdx(volume["id"])
|
||||
initiator_name = None
|
||||
found = False
|
||||
|
||||
# Get existing cli initiator name by its iqn, or create a new one
|
||||
# if it doesnt exist
|
||||
for host in self.rdxApi.list_hosts():
|
||||
if host["iscsi_name"] == initiator_iqn:
|
||||
LOG.info(_LI("initiator exists in Reduxio"))
|
||||
found = True
|
||||
initiator_name = host["name"]
|
||||
break
|
||||
if not found:
|
||||
LOG.info(_LI("Initiator doesn't exist in Reduxio, Creating it"))
|
||||
initiator_name = self._generate_initiator_name()
|
||||
self.rdxApi.create_host(name=initiator_name,
|
||||
iscsi_name=initiator_iqn)
|
||||
|
||||
existing_assignment = self.rdxApi.get_single_assignment(
|
||||
vol=vol_rdx_name, host=initiator_name, raise_on_non_exists=False)
|
||||
|
||||
if existing_assignment is None:
|
||||
# Create assignment between the host and the volume
|
||||
LOG.info(_LI("Creating assignment"))
|
||||
self.rdxApi.assign(vol_rdx_name, host_name=initiator_name)
|
||||
else:
|
||||
LOG.debug("Assignment already exists")
|
||||
|
||||
# Query cli settings in order to fill requested output
|
||||
settings = self.rdxApi.get_settings()
|
||||
|
||||
target_iqn = settings["network_configuration"]["iscsi_target_iqn"]
|
||||
target_portal = self._get_target_portal(settings, 1, 1)
|
||||
|
||||
if existing_assignment is None:
|
||||
target_lun = self.rdxApi.get_single_assignment(
|
||||
vol=vol_rdx_name,
|
||||
host=initiator_name)["lun"]
|
||||
else:
|
||||
target_lun = existing_assignment["lun"]
|
||||
|
||||
properties = {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': {
|
||||
'target_discovered': False,
|
||||
'discard': False,
|
||||
'volume_id': volume['id'],
|
||||
'target_iqn': target_iqn,
|
||||
'target_portal': target_portal,
|
||||
'target_lun': target_lun,
|
||||
}
|
||||
}
|
||||
|
||||
# if iscsi_network2 is not available,
|
||||
# than multipath is disabled (ReduxioVE)
|
||||
connector_multipath = connector.get("multipath", False)
|
||||
rdx_multipath = "iscsi_network2" in settings
|
||||
if rdx_multipath and connector_multipath:
|
||||
target_portal2 = self._get_target_portal(settings, 2, 1)
|
||||
target_portal3 = self._get_target_portal(settings, 1, 2)
|
||||
target_portal4 = self._get_target_portal(settings, 2, 2)
|
||||
|
||||
properties['data']['target_portals'] = [
|
||||
target_portal,
|
||||
target_portal2,
|
||||
target_portal3,
|
||||
target_portal4
|
||||
]
|
||||
# Reduxio is a single iqn storage
|
||||
properties['data']['target_iqns'] = [target_iqn] * 4
|
||||
# Lun num is the same for each path
|
||||
properties['data']['target_luns'] = [target_lun] * 4
|
||||
|
||||
LOG.info(_LI("Assignment complete. Assignment details: %s"),
|
||||
properties)
|
||||
|
||||
return properties
|
||||
|
||||
@cinder_utils.trace
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
"""Driver entry point to unattach a volume from an instance."""
|
||||
iqn = connector['initiator']
|
||||
LOG.info(_LI("Deleting assignment volume(%(vol)s) with "
|
||||
"initiator(%(initiator)s)"),
|
||||
{'vol': volume['name'], 'initiator': iqn})
|
||||
|
||||
for cli_host in self.rdxApi.list_hosts():
|
||||
if cli_host["iscsi_name"] == iqn:
|
||||
try:
|
||||
self.rdxApi.unassign(
|
||||
self._cinder_id_to_rdx(volume["id"]),
|
||||
host_name=cli_host["name"]
|
||||
)
|
||||
except exception.RdxAPICommandException as e:
|
||||
error_msg = six.text_type(e)
|
||||
if "No such assignment" not in error_msg:
|
||||
raise
|
||||
else:
|
||||
LOG.debug("Assignment doesn't exist")
|
||||
return
|
||||
|
||||
LOG.warning(_LW("Did not find matching reduxio host for initiator %s"),
|
||||
iqn)
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added backend ISCSI driver for Reduxio.
|
Loading…
Reference in New Issue
Block a user