From 5bb68e49d3323c7a73166aef147c248c69503e9a Mon Sep 17 00:00:00 2001 From: mattanShalev Date: Tue, 21 Jun 2016 12:31:16 +0300 Subject: [PATCH] Added initial backend ISCSI driver for Reduxio The driver implements the basic ISCSI driver functionality and supports volumes backdating. DocImpact Change-Id: Ic95026497ab3721a559b5229a9e51221c375a4b3 Implements: blueprint reduxio-iscsi-volume-driver --- cinder/exception.py | 9 + cinder/tests/unit/test_reduxio.py | 649 ++++++++++++++++++ cinder/volume/drivers/reduxio/__init__.py | 0 cinder/volume/drivers/reduxio/rdx_cli_api.py | 542 +++++++++++++++ .../drivers/reduxio/rdx_iscsi_driver.py | 502 ++++++++++++++ ...eduxio-iscsci-driver-5827c32a0c498949.yaml | 3 + 6 files changed, 1705 insertions(+) create mode 100644 cinder/tests/unit/test_reduxio.py create mode 100644 cinder/volume/drivers/reduxio/__init__.py create mode 100644 cinder/volume/drivers/reduxio/rdx_cli_api.py create mode 100644 cinder/volume/drivers/reduxio/rdx_iscsi_driver.py create mode 100644 releasenotes/notes/reduxio-iscsci-driver-5827c32a0c498949.yaml diff --git a/cinder/exception.py b/cinder/exception.py index cbd60898fac..3975cff44d8 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -1309,3 +1309,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") diff --git a/cinder/tests/unit/test_reduxio.py b/cinder/tests/unit/test_reduxio.py new file mode 100644 index 00000000000..f972216f6ec --- /dev/null +++ b/cinder/tests/unit/test_reduxio.py @@ -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)]) diff --git a/cinder/volume/drivers/reduxio/__init__.py b/cinder/volume/drivers/reduxio/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/reduxio/rdx_cli_api.py b/cinder/volume/drivers/reduxio/rdx_cli_api.py new file mode 100644 index 00000000000..ccc414f5dc7 --- /dev/null +++ b/cinder/volume/drivers/reduxio/rdx_cli_api.py @@ -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] diff --git a/cinder/volume/drivers/reduxio/rdx_iscsi_driver.py b/cinder/volume/drivers/reduxio/rdx_iscsi_driver.py new file mode 100644 index 00000000000..5ceab736291 --- /dev/null +++ b/cinder/volume/drivers/reduxio/rdx_iscsi_driver.py @@ -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) diff --git a/releasenotes/notes/reduxio-iscsci-driver-5827c32a0c498949.yaml b/releasenotes/notes/reduxio-iscsci-driver-5827c32a0c498949.yaml new file mode 100644 index 00000000000..799a9d4830b --- /dev/null +++ b/releasenotes/notes/reduxio-iscsci-driver-5827c32a0c498949.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added backend ISCSI driver for Reduxio.