Merge "Added initial backend ISCSI driver for Reduxio"

This commit is contained in:
Jenkins 2016-12-13 06:14:12 +00:00 committed by Gerrit Code Review
commit 344f230990
6 changed files with 1705 additions and 0 deletions

View File

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

View 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)])

View 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]

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

View File

@ -0,0 +1,3 @@
---
features:
- Added backend ISCSI driver for Reduxio.