From 0d8e026dab830d0a1e836f7e3f1d84ea7ccfd6a4 Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Sun, 15 Jul 2012 15:53:36 +0300 Subject: [PATCH] Driver for IBM Storwize and SVC storage. Volume driver for IBM Storwize and SVC storage systems, along with unit tests and updated sample config file. The unit tests include a Storwize/SVC management simulator to allow for testing without controller hardware. Also added a new exception for volume drivers. Change-Id: Id7e3e79cd6e62fac4b10937b6f1b5f6bcb7908fe --- cinder/exception.py | 5 + cinder/tests/test_storwize_svc.py | 939 ++++++++++++++++++++++ cinder/volume/storwize_svc.py | 1200 +++++++++++++++++++++++++++++ etc/cinder/cinder.conf.sample | 19 +- 4 files changed, 2162 insertions(+), 1 deletion(-) create mode 100644 cinder/tests/test_storwize_svc.py create mode 100644 cinder/volume/storwize_svc.py diff --git a/cinder/exception.py b/cinder/exception.py index 7ee6eb502..5cf10a15f 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -936,3 +936,8 @@ class InvalidInstanceIDMalformed(Invalid): class CouldNotFetchImage(CinderException): message = _("Could not fetch image %(image)s") + + +class VolumeBackendAPIException(CinderException): + message = _("Bad or unexpected response from the storage volume " + "backend API: data=%(data)s") diff --git a/cinder/tests/test_storwize_svc.py b/cinder/tests/test_storwize_svc.py new file mode 100644 index 000000000..24fd1d93d --- /dev/null +++ b/cinder/tests/test_storwize_svc.py @@ -0,0 +1,939 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 IBM, Inc. +# Copyright (c) 2012 OpenStack LLC. +# 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. + +""" +Tests for the IBM Storwize V7000 and SVC volume driver. +""" + +import random + +from cinder import exception +from cinder.openstack.common import excutils +from cinder.openstack.common import log as logging +from cinder import test +from cinder import utils +from cinder.volume import storwize_svc + +LOG = logging.getLogger(__name__) + + +class StorwizeSVCManagementSimulator: + def __init__(self, pool_name): + self._flags = {"storwize_svc_volpool_name": pool_name} + self._volumes_list = {} + self._hosts_list = {} + self._mappings_list = {} + self._fcmappings_list = {} + self._next_cmd_error = {} + self._errors = { + "CMMVC5701E": ("", "CMMVC5701E No object ID was specified."), + "CMMVC6035E": ("", "CMMVC6035E The action failed as the " + + "object already exists."), + "CMMVC5753E": ("", "CMMVC5753E The specified object does not " + + "exist or is not a suitable candidate."), + "CMMVC5707E": ("", "CMMVC5707E Required parameters are missing."), + "CMMVC6581E": ("", "CMMVC6581E The command has failed because " + + "the maximum number of allowed iSCSI " + + "qualified names (IQNs) has been reached, " + + "or the IQN is already assigned or is not " + + "valid."), + "CMMVC5708E": ("", "CMMVC5708E The [XXX] parameter is missing " + + "its associated arguments."), + "CMMVC5754E": ("", "CMMVC5754E The specified object does not " + + "exist, or the name supplied does not meet " + + "the naming rules."), + "CMMVC6071E": ("", "CMMVC6071E The VDisk-to-host mapping was " + + "not created because the VDisk is already " + + "mapped to a host."), + "CMMVC5879E": ("", "CMMVC5879E The VDisk-to-host mapping was " + + "not created because a VDisk is already " + + "mapped to this host with this SCSI LUN."), + "CMMVC5840E": ("", "CMMVC5840E The virtual disk (VDisk) was " + + "not deleted because it is mapped to a " + + "host or because it is part of a FlashCopy " + + "or Remote Copy mapping, or is involved in " + + "an image mode migrate."), + "CMMVC6070E": ("", "CMMVC6070E An invalid or duplicated " + + "parameter, unaccompanied argument, or " + + "incorrect argument sequence has been " + + "detected. Ensure that the input is as per " + + "the help."), + "CMMVC6527E": ("", "CMMVC6527E The name that you have entered " + + "is not valid. The name can contain letters, " + + "numbers, spaces, periods, dashes, and " + + "underscores. The name must begin with a " + + "letter or an underscore. The name must not " + + "begin or end with a space."), + "CMMVC5871E": ("", "CMMVC5871E The action failed because one or " + + "more of the configured port names is in a " + + "mapping."), + "CMMVC5924E": ("", "CMMVC5924E The FlashCopy mapping was not " + + "created because the source and target " + + "virtual disks (VDisks) are different sizes."), + "CMMVC6303E": ("", "CMMVC6303E The create failed because the " + + "source and target VDisks are the same."), + } + + # Find an unused ID + def _find_unused_id(self, d): + ids = [] + for k, v in d.iteritems(): + ids.append(int(v["id"])) + ids.sort() + for index, n in enumerate(ids): + if n > index: + return str(index) + return str(len(ids)) + + # Check if name is valid + def _is_invalid_name(self, name): + if (name[0] == " ") or (name[-1] == " "): + return True + for c in name: + if ((not c.isalnum()) and (c != " ") and (c != ".") + and (c != "-") and (c != "_")): + return True + return False + + # Check if name is valid + def _strip_quotes(self, str): + if ((str[0] == '\"' and str[-1] == '\"') or + (str[0] == '\'' and str[-1] == '\'')): + return str[1:-1] + return str + + # Generic function for printing information + def _print_info_cmd(self, arg_list, rows): + for arg in arg_list: + if arg == "-nohdr": + del rows[0] + + delimeter = " " + try: + arg_index = arg_list.index("-delim") + delimeter = arg_list[arg_index + 1] + except ValueError: + pass + except IndexError: + return self._errors["CMMVC5707E"] + + for index in range(len(rows)): + rows[index] = delimeter.join(rows[index]) + return ("%s" % "\n".join(rows), "") + + # Print mostly made-up stuff in the correct syntax + def _cmd_lsmdiskgrp(self, arg_list): + rows = [None] * 2 + rows[0] = ["id", "name", "status", "mdisk_count", + "vdisk_count capacity", "extent_size", "free_capacity", + "virtual_capacity", "used_capacity", "real_capacity", + "overallocation", "warning", "easy_tier", + "easy_tier_status"] + rows[1] = ["0", self._flags["storwize_svc_volpool_name"], "online", + "1", str(len(self._volumes_list)), "3.25TB", "256", + "3.21TB", "1.54TB", "264.97MB", "35.58GB", "47", "80", + "auto", "inactive"] + return self._print_info_cmd(arg_list, rows) + + # Print mostly made-up stuff in the correct syntax + def _cmd_lsnodecanister(self, arg_list): + rows = [None] * 3 + rows[0] = ["id", "name", "UPS_serial_number", "WWNN", "status", + "IO_group_id", "IO_group_name", "config_node", + "UPS_unique_id", "hardware", "iscsi_name", "iscsi_alias", + "panel_name", "enclosure_id", "canister_id", + "enclosure_serial_number"] + rows[1] = ["5", "node1", "", "123456789ABCDEF0", "online", "0", + "io_grp0", + "yes", "123456789ABCDEF0", "100", + "iqn.1982-01.com.ibm:1234.sim.node1", "", "01-1", "1", "1", + "0123ABC"] + rows[2] = ["6", "node2", "", "123456789ABCDEF1", "online", "0", + "io_grp0", + "no", "123456789ABCDEF1", "100", + "iqn.1982-01.com.ibm:1234.sim.node2", "", "01-2", "1", "2", + "0123ABC"] + return self._print_info_cmd(arg_list, rows) + + # Print mostly made-up stuff in the correct syntax + def _cmd_lsportip(self, arg_list): + if (("lsportip" in self._next_cmd_error) and + (self._next_cmd_error["lsportip"] == "ip_no_config")): + self._next_cmd_error["lsportip"] = None + ip_addr1 = "" + ip_addr2 = "" + gw = "" + else: + ip_addr1 = "1.234.56.78" + ip_addr2 = "1.234.56.79" + gw = "1.234.56.1" + + rows = [None] * 17 + rows[0] = ["id", "node_id", "node_name", "IP_address", "mask", + "gateway", "IP_address_6", "prefix_6", "gateway_6", "MAC", + "duplex", "state", "speed", "failover"] + rows[1] = ["1", "5", "node1", ip_addr1, "255.255.255.0", + gw, "", "", "", "01:23:45:67:89:00", "Full", + "online", "1Gb/s", "no"] + rows[2] = ["1", "5", "node1", "", "", "", "", "", "", + "01:23:45:67:89:00", "Full", "online", "1Gb/s", "yes"] + rows[3] = ["2", "5", "node1", "", "", "", "", "", "", + "01:23:45:67:89:01", "Full", "unconfigured", "1Gb/s", "no"] + rows[4] = ["2", "5", "node1", "", "", "", "", "", "", + "01:23:45:67:89:01", "Full", "unconfigured", "1Gb/s", "yes"] + rows[5] = ["3", "5", "node1", "", "", "", "", "", "", "", "", + "unconfigured", "", "no"] + rows[6] = ["3", "5", "node1", "", "", "", "", "", "", "", "", + "unconfigured", "", "yes"] + rows[7] = ["4", "5", "node1", "", "", "", "", "", "", "", "", + "unconfigured", "", "no"] + rows[8] = ["4", "5", "node1", "", "", "", "", "", "", "", "", + "unconfigured", "", "yes"] + rows[9] = ["1", "6", "node2", ip_addr2, "255.255.255.0", + gw, "", "", "", "01:23:45:67:89:02", "Full", + "online", "1Gb/s", "no"] + rows[10] = ["1", "6", "node2", "", "", "", "", "", "", + "01:23:45:67:89:02", "Full", "online", "1Gb/s", "yes"] + rows[11] = ["2", "6", "node2", "", "", "", "", "", "", + "01:23:45:67:89:03", "Full", "unconfigured", "1Gb/s", "no"] + rows[12] = ["2", "6", "node2", "", "", "", "", "", "", + "01:23:45:67:89:03", "Full", "unconfigured", "1Gb/s", + "yes"] + rows[13] = ["3", "6", "node2", "", "", "", "", "", "", "", "", + "unconfigured", "", "no"] + rows[14] = ["3", "6", "node2", "", "", "", "", "", "", "", "", + "unconfigured", "", "yes"] + rows[15] = ["4", "6", "node2", "", "", "", "", "", "", "", "", + "unconfigured", "", "no"] + rows[16] = ["4", "6", "node2", "", "", "", "", "", "", "", "", + "unconfigured", "", "yes"] + + return self._print_info_cmd(arg_list, rows) + + # Create a vdisk + def _cmd_mkvdisk(self, arg_list): + # We only save the id/uid, name, and size - all else will be made up + volume_info = {} + volume_info["id"] = self._find_unused_id(self._volumes_list) + volume_info["uid"] = ("ABCDEF" * 3) + ("0" * 14) + volume_info["id"] + try: + arg_index = arg_list.index("-name") + 1 + volume_info["name"] = self._strip_quotes(arg_list[arg_index]) + except ValueError: + volume_info["name"] = "vdisk" + str(len(self._volumes_list)) + except IndexError: + return self._errors["CMMVC5707E"] + + # Assume size and unit are given, store it in bytes + capacity = int(arg_list[arg_list.index("-size") + 1]) + unit = arg_list[arg_list.index("-unit") + 1] + + if unit == "b": + volume_info["capacity"] = capacity + elif unit == "kb": + volume_info["capacity"] = capacity * pow(1024, 1) + elif unit == "mb": + volume_info["capacity"] = capacity * pow(1024, 2) + elif unit == "gb": + volume_info["capacity"] = capacity * pow(1024, 3) + elif unit == "tb": + volume_info["capacity"] = capacity * pow(1024, 4) + elif unit == "pb": + volume_info["capacity"] = capacity * pow(1024, 5) + + if volume_info["name"] in self._volumes_list: + return self._errors["CMMVC6035E"] + else: + self._volumes_list[volume_info["name"]] = volume_info + return ("Virtual Disk, id [%s], successfully created" % + (volume_info["id"]), "") + + # Delete a vdisk + def _cmd_rmvdisk(self, arg_list): + if len(arg_list) == 1: + return self._errors["CMMVC5701E"] + elif len(arg_list) == 2: + force = 0 + vol_name = arg_list[1] + elif len(arg_list) == 3: + if (arg_list[1] == "-force"): + force = 1 + else: + return self._errors["CMMVC6070E"] + vol_name = arg_list[2] + else: + return self._errors["CMMVC6070E"] + + vol_name = self._strip_quotes(vol_name) + + if not vol_name in self._volumes_list: + return self._errors["CMMVC5753E"] + + if force == 0: + for k, mapping in self._mappings_list.iteritems(): + if mapping["vol"] == vol_name: + return self._errors["CMMVC5840E"] + for k, fcmap in self._fcmappings_list.iteritems(): + if ((fcmap["source"] == vol_name) or + (fcmap["target"] == vol_name)): + return self._errors["CMMVC5840E"] + + del self._volumes_list[vol_name] + return ("", "") + + def _generic_parse_ls_args(self, arg_list): + index = 1 + ret_vals = { + "no_hdr": 0, + "delim": "", + "obj_name": "", + "filter": "", + } + + while index < len(arg_list): + try: + if arg_list[index] == "-delim": + ret_vals["delim"] = arg_list[index + 1] + index += 2 + elif arg_list[index] == "-nohdr": + ret_vals["no_hdr"] = 1 + index += 1 + elif arg_list[index] == "-filtervalue": + ret_vals["filter"] = arg_list[index + 1].split("=")[1] + index += 2 + else: + ret_vals["obj_name"] = arg_list[index] + index += 1 + except IndexError: + return self._errors["CMMVC5708E"] + + return ret_vals + + def _get_fcmap_info(self, vol_name): + ret_vals = { + "fc_id": "", + "fc_name": "", + "fc_map_count": "0", + } + for k, fcmap in self._fcmappings_list.iteritems(): + if ((fcmap["source"] == vol_name) or + (fcmap["target"] == vol_name)): + ret_vals["fc_id"] = fcmap["id"] + ret_vals["fc_name"] = fcmap["name"] + ret_vals["fc_map_count"] = "1" + return ret_vals + + # List information about vdisks + def _cmd_lsvdisk(self, arg_list): + arg_dict = self._generic_parse_ls_args(arg_list) + + if arg_dict["obj_name"] == "": + rows = [] + rows.append(["id", "name", "IO_group_id", "IO_group_name", + "status", "mdisk_grp_id", "mdisk_grp_name", + "capacity", "type", "FC_id", "FC_name", "RC_id", + "RC_name", "vdisk_UID", "fc_map_count", "copy_count", + "fast_write_state", "se_copy_count", "RC_change"]) + + for k, vol in self._volumes_list.iteritems(): + if ((arg_dict["filter"] == "") or + (arg_dict["filter"] == vol["name"])): + fcmap_info = self._get_fcmap_info(vol["name"]) + + rows.append([str(vol["id"]), vol["name"], "0", "io_grp0", + "online", "0", + self._flags["storwize_svc_volpool_name"], + str(vol["capacity"]), "striped", + fcmap_info["fc_id"], fcmap_info["fc_name"], + "", "", vol["uid"], + fcmap_info["fc_map_count"], "1", "empty", + "1", "no"]) + + return self._print_info_cmd(arg_list, rows) + + else: + if arg_dict["obj_name"] not in self._volumes_list: + return self._errors["CMMVC5754E"] + vol = self._volumes_list[arg_dict["obj_name"]] + fcmap_info = self._get_fcmap_info(vol["name"]) + rows = [] + rows.append(["id", str(vol["id"])]) + rows.append(["name", vol["name"]]) + rows.append(["IO_group_id", "0"]) + rows.append(["IO_group_name", "io_grp0"]) + rows.append(["status", "online"]) + rows.append(["mdisk_grp_id", "0"]) + rows.append(["mdisk_grp_name", + self._flags["storwize_svc_volpool_name"]]) + rows.append(["capacity", str(vol["capacity"])]) + rows.append(["type", "striped"]) + rows.append(["formatted", "no"]) + rows.append(["mdisk_id", ""]) + rows.append(["mdisk_name", ""]) + rows.append(["FC_id", fcmap_info["fc_id"]]) + rows.append(["FC_name", fcmap_info["fc_name"]]) + rows.append(["RC_id", ""]) + rows.append(["RC_name", ""]) + rows.append(["vdisk_UID", vol["uid"]]) + rows.append(["throttling", "0"]) + rows.append(["preferred_node_id", "2"]) + rows.append(["fast_write_state", "empty"]) + rows.append(["cache", "readwrite"]) + rows.append(["udid", ""]) + rows.append(["fc_map_count", fcmap_info["fc_map_count"]]) + rows.append(["sync_rate", "50"]) + rows.append(["copy_count", "1"]) + rows.append(["se_copy_count", "0"]) + rows.append(["mirror_write_priority", "latency"]) + rows.append(["RC_change", "no"]) + + if arg_dict["no_hdr"] == 1: + for index in range(len(rows)): + rows[index] = " ".join(rows[index][1:]) + + if arg_dict["delim"] != "": + for index in range(len(rows)): + rows[index] = arg_dict["delim"].join(rows[index]) + + return ("%s" % "\n".join(rows), "") + + # Make a host + def _cmd_mkhost(self, arg_list): + try: + arg_index = arg_list.index("-name") + 1 + host_name = self._strip_quotes(arg_list[arg_index]) + except ValueError: + host_name = "host" + str(self._num_host()) + except IndexError: + return self._errors["CMMVC5707E"] + + try: + arg_index = arg_list.index("-iscsiname") + 1 + iscsi_name = self._strip_quotes(arg_list[arg_index]) + except ValueError: + return self._errors["CMMVC5707E"] + except IndexError: + return self._errors["CMMVC5708E"].replace("XXX", "-iscsiname") + + if self._is_invalid_name(host_name): + return self._errors["CMMVC6527E"] + + if host_name in self._hosts_list: + return self._errors["CMMVC6035E"] + + for k, v in self._hosts_list.iteritems(): + if v["iscsi_name"] == iscsi_name: + return self._errors["CMMVC6581E"] + + host_info = {} + host_info["host_name"] = host_name + host_info["iscsi_name"] = iscsi_name + host_info["id"] = self._find_unused_id(self._hosts_list) + + self._hosts_list[host_name] = host_info + return ("Host, id [%s], successfully created" % + (host_info["id"]), "") + + # Remove a host + def _cmd_rmhost(self, arg_list): + if len(arg_list) == 1: + return self._errors["CMMVC5701E"] + + host_name = self._strip_quotes(arg_list[1]) + if host_name not in self._hosts_list: + return self._errors["CMMVC5753E"] + + for k, v in self._mappings_list.iteritems(): + if (v["host"] == host_name): + return self._errors["CMMVC5871E"] + + del self._hosts_list[host_name] + return ("", "") + + # List information about hosts + def _cmd_lshost(self, arg_list): + arg_dict = self._generic_parse_ls_args(arg_list) + + if arg_dict["obj_name"] == "": + rows = [] + rows.append(["id", "name", "port_count", "iogrp_count", "status"]) + + for k, host in self._hosts_list.iteritems(): + if ((arg_dict["filter"] == "") or + (arg_dict["filter"] == host["host_name"])): + rows.append([host["id"], host["host_name"], "1", "4", + "offline"]) + return self._print_info_cmd(arg_list, rows) + else: + if arg_dict["obj_name"] not in self._hosts_list: + return self._errors["CMMVC5754E"] + host = self._hosts_list[arg_dict["obj_name"]] + rows = [] + rows.append(["id", host["id"]]) + rows.append(["name", host["host_name"]]) + rows.append(["port_count", "1"]) + rows.append(["type", "generic"]) + rows.append(["mask", "1111"]) + rows.append(["iogrp_count", "4"]) + rows.append(["status", "offline"]) + rows.append(["iscsi_name", host["iscsi_name"]]) + rows.append(["node_logged_in_count", "0"]) + rows.append(["state", "offline"]) + + if arg_dict["no_hdr"] == 1: + for index in range(len(rows)): + rows[index] = " ".join(rows[index][1:]) + + if arg_dict["delim"] != "": + for index in range(len(rows)): + rows[index] = arg_dict["delim"].join(rows[index]) + + return ("%s" % "\n".join(rows), "") + + # Create a vdisk-host mapping + def _cmd_mkvdiskhostmap(self, arg_list): + mapping_info = {} + mapping_info["id"] = self._find_unused_id(self._mappings_list) + try: + arg_index = arg_list.index("-host") + 1 + mapping_info["host"] = self._strip_quotes(arg_list[arg_index]) + except (ValueError, IndexError): + return self._errors["CMMVC5707E"] + + try: + arg_index = arg_list.index("-scsi") + 1 + mapping_info["lun"] = self._strip_quotes(arg_list[arg_index]) + except (ValueError, IndexError): + return self._errors["CMMVC5707E"] + + mapping_info["vol"] = self._strip_quotes(arg_list[-1]) + + if not mapping_info["vol"] in self._volumes_list: + return self._errors["CMMVC5753E"] + + if not mapping_info["host"] in self._hosts_list: + return self._errors["CMMVC5754E"] + + if mapping_info["vol"] in self._mappings_list: + return self._errors["CMMVC6071E"] + + for k, v in self._mappings_list.iteritems(): + if ((v["host"] == mapping_info["host"]) and + (v["lun"] == mapping_info["lun"])): + return self._errors["CMMVC5879E"] + + self._mappings_list[mapping_info["vol"]] = mapping_info + return ("Virtual Disk to Host map, id [%s], successfully created" + % (mapping_info["id"]), "") + + # Delete a vdisk-host mapping + def _cmd_rmvdiskhostmap(self, arg_list): + try: + host = self._strip_quotes(arg_list[arg_list.index("-host") + 1]) + except (ValueError, IndexError): + return self._errors["CMMVC5707E"] + + vol = self._strip_quotes(arg_list[-1]) + + if not vol in self._mappings_list: + return self._errors["CMMVC5753E"] + + if self._mappings_list[vol]["host"] != host: + return self._errors["CMMVC5753E"] + + del self._mappings_list[vol] + return ("", "") + + # List information about vdisk-host mappings + def _cmd_lshostvdiskmap(self, arg_list): + index = 1 + no_hdr = 0 + delimeter = "" + host_name = "" + while index < len(arg_list): + try: + if arg_list[index] == "-delim": + delimeter = arg_list[index + 1] + index += 2 + elif arg_list[index] == "-nohdr": + no_hdr = 1 + index += 1 + else: + host_name = arg_list[index] + index += 1 + except IndexError: + return self._errors["CMMVC5708E"] + + if host_name not in self._hosts_list: + return self._errors["CMMVC5754E"] + + rows = [] + rows.append(["id", "name", "SCSI_id", "vdisk_id", "vdisk_name", + "vdisk_UID"]) + + for k, mapping in self._mappings_list.iteritems(): + if (host_name == "") or (mapping["host"] == host_name): + volume = self._volumes_list[mapping["vol"]] + rows.append([mapping["id"], mapping["host"], + mapping["lun"], volume["id"], + volume["name"], volume["uid"]]) + + return self._print_info_cmd(arg_list, rows) + + # Create a FlashCopy mapping + def _cmd_mkfcmap(self, arg_list): + source = "" + target = "" + + try: + arg_index = arg_list.index("-source") + 1 + source = self._strip_quotes(arg_list[arg_index]) + except (ValueError, IndexError): + return self._errors["CMMVC5707E"] + if not source in self._volumes_list: + return self._errors["CMMVC5754E"] + + try: + arg_index = arg_list.index("-target") + 1 + target = self._strip_quotes(arg_list[arg_index]) + except (ValueError, IndexError): + return self._errors["CMMVC5707E"] + if not target in self._volumes_list: + return self._errors["CMMVC5754E"] + + if source == target: + return self._errors["CMMVC6303E"] + + if (self._volumes_list[source]["capacity"] != + self._volumes_list[target]["capacity"]): + return ("", "%s != %s" % (self._volumes_list[source]["capacity"], + self._volumes_list[target]["capacity"])) + + fcmap_info = {} + fcmap_info["source"] = source + fcmap_info["target"] = target + fcmap_info["id"] = self._find_unused_id(self._fcmappings_list) + fcmap_info["name"] = "fcmap" + fcmap_info["id"] + fcmap_info["status"] = "idle_or_copied" + fcmap_info["progress"] = "0" + self._fcmappings_list[target] = fcmap_info + + return("FlashCopy Mapping, id [" + fcmap_info["id"] + + "], successfully created", "") + + # Same function used for both prestartfcmap and startfcmap + def _cmd_gen_startfcmap(self, arg_list, mode): + if len(arg_list) == 1: + return self._errors["CMMVC5701E"] + elif len(arg_list) > 2: + return self._errors["CMMVC6070E"] + id_num = arg_list[1] + + for k, fcmap in self._fcmappings_list.iteritems(): + if fcmap["id"] == id_num: + if mode == "pre": + fcmap["status"] = "preparing" + else: + fcmap["status"] = "copying" + fcmap["progress"] = "0" + return ("", "") + return self._errors["CMMVC5753E"] + + def _cmd_lsfcmap(self, arg_list): + rows = [] + rows.append(["id", "name", "source_vdisk_id", "source_vdisk_name", + "target_vdisk_id", "target_vdisk_name", "group_id", + "group_name", "status", "progress", "copy_rate", + "clean_progress", "incremental", "partner_FC_id", + "partner_FC_name", "restoring", "start_time", + "rc_controlled"]) + + # Assume we always get a filtervalue argument + arg_index = arg_list.index("-filtervalue") + filter_key = arg_list[arg_index + 1].split("=")[0] + filter_value = arg_list[arg_index + 1].split("=")[1] + to_delete = [] + for k, v in self._fcmappings_list.iteritems(): + if str(v[filter_key]) == filter_value: + source = self._volumes_list[v["source"]] + target = self._volumes_list[v["target"]] + rows.append([v["id"], v["name"], source["id"], + source["name"], target["id"], target["name"], "", + "", v["status"], v["progress"], "50", "100", + "off", "", "", "no", "", "no"]) + if v["status"] == "preparing": + v["status"] = "prepared" + elif (v["status"] == "copying") and (v["progress"] == "0"): + v["progress"] = "50" + elif (v["status"] == "copying") and (v["progress"] == "50"): + to_delete.append(k) + + for d in to_delete: + del self._fcmappings_list[k] + + return self._print_info_cmd(arg_list, rows) + + # The main function to run commands on the management simulator + def execute_command(self, cmd, check_exit_code=True): + arg_list = cmd.split() + + if arg_list[0] == "lsmdiskgrp": + out, err = self._cmd_lsmdiskgrp(arg_list) + elif arg_list[0] == "lsnodecanister": + out, err = self._cmd_lsnodecanister(arg_list) + elif arg_list[0] == "lsportip": + out, err = self._cmd_lsportip(arg_list) + elif arg_list[0] == "mkvdisk": + out, err = self._cmd_mkvdisk(arg_list) + elif arg_list[0] == "rmvdisk": + out, err = self._cmd_rmvdisk(arg_list) + elif arg_list[0] == "lsvdisk": + out, err = self._cmd_lsvdisk(arg_list) + elif arg_list[0] == "mkhost": + out, err = self._cmd_mkhost(arg_list) + elif arg_list[0] == "rmhost": + out, err = self._cmd_rmhost(arg_list) + elif arg_list[0] == "lshost": + out, err = self._cmd_lshost(arg_list) + elif arg_list[0] == "mkvdiskhostmap": + out, err = self._cmd_mkvdiskhostmap(arg_list) + elif arg_list[0] == "rmvdiskhostmap": + out, err = self._cmd_rmvdiskhostmap(arg_list) + elif arg_list[0] == "lshostvdiskmap": + out, err = self._cmd_lshostvdiskmap(arg_list) + elif arg_list[0] == "mkfcmap": + out, err = self._cmd_mkfcmap(arg_list) + elif arg_list[0] == "prestartfcmap": + out, err = self._cmd_gen_startfcmap(arg_list, "pre") + elif arg_list[0] == "startfcmap": + out, err = self._cmd_gen_startfcmap(arg_list, "start") + elif arg_list[0] == "lsfcmap": + out, err = self._cmd_lsfcmap(arg_list) + else: + out, err = ("", "ERROR: Unsupported command") + + if (check_exit_code) and (len(err) != 0): + raise exception.ProcessExecutionError(exit_code=1, + stdout=out, + stderr=err, + cmd=' '.join(cmd)) + + return (out, err) + + # After calling this function, the next call to the specified command will + # result in in the error specified + def error_injection(self, cmd, error): + self._next_cmd_error[cmd] = error + + +class StorwizeSVCFakeDriver(storwize_svc.StorwizeSVCDriver): + def __init__(self, *args, **kwargs): + super(StorwizeSVCFakeDriver, self).__init__(*args, **kwargs) + + def set_fake_storage(self, fake): + self.fake_storage = fake + + def _run_ssh(self, cmd, check_exit_code=True): + try: + LOG.debug(_('Run CLI command: %s') % cmd) + ret = self.fake_storage.execute_command(cmd, check_exit_code) + (stdout, stderr) = ret + LOG.debug(_('CLI output:\n stdout: %(out)s\n stderr: %(err)s') % + {'out': stdout, 'err': stderr}) + + except exception.ProcessExecutionError as e: + with excutils.save_and_reraise_exception(): + LOG.debug(_('CLI Exception output:\n stdout: %(out)s\n ' + 'stderr: %(err)s') % {'out': e.stdout, + 'err': e.stderr}) + + return ret + + +class StorwizeSVCDriverTestCase(test.TestCase): + def setUp(self): + super(StorwizeSVCDriverTestCase, self).setUp() + self.USESIM = 1 + if self.USESIM == 1: + self.sim = StorwizeSVCManagementSimulator("volpool") + driver = StorwizeSVCFakeDriver() + driver.set_fake_storage(self.sim) + storwize_svc.FLAGS.san_ip = "hostname" + storwize_svc.FLAGS.san_login = "user" + storwize_svc.FLAGS.san_password = "pass" + storwize_svc.FLAGS.storwize_svc_volpool_name = "volpool" + storwize_svc.FLAGS.storwize_svc_flashcopy_timeout = "20" + else: + driver = storwize_svc.StorwizeSVCDriver() + storwize_svc.FLAGS.san_ip = "-1.-1.-1.-1" + storwize_svc.FLAGS.san_login = "user" + storwize_svc.FLAGS.san_password = "password" + storwize_svc.FLAGS.storwize_svc_volpool_name = "pool" + + self.driver = driver + self.driver.do_setup(None) + self.driver.check_for_setup_error() + + def test_storwize_svc_volume_non_space_efficient(self): + storwize_svc.FLAGS.storwize_svc_vol_rsize = "-1" + volume = {} + volume["name"] = "test1_volume%s" % random.randint(10000, 99999) + volume["size"] = 10 + volume["id"] = 1 + self.driver.create_volume(volume) + # Make sure that the volume has been created + is_volume_defined = self.driver._is_volume_defined(volume["name"]) + self.assertEqual(is_volume_defined, True) + + self.driver.delete_volume(volume) + + def test_storwize_svc_connectivity(self): + # Make sure we detect if the pool doesn't exist + orig_pool = getattr(storwize_svc.FLAGS, "storwize_svc_volpool_name") + no_exist_pool = "i-dont-exist-%s" % random.randint(10000, 99999) + storwize_svc.FLAGS.storwize_svc_volpool_name = no_exist_pool + self.assertRaises(exception.InvalidParameterValue, + self.driver.check_for_setup_error) + storwize_svc.FLAGS.storwize_svc_volpool_name = orig_pool + + # Check the case where the user didn't configure IP addresses + if self.USESIM == 1: + self.sim.error_injection("lsportip", "ip_no_config") + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.check_for_setup_error) + + # Finally, check with good parameters + self.driver.check_for_setup_error() + + def test_storwize_svc_flashcopy(self): + volume1 = {} + volume1["name"] = "test_volume%s" % random.randint(10000, 99999) + volume1["size"] = 10 + volume1["id"] = 10 + self.driver.create_volume(volume1) + + snapshot = {} + snapshot["name"] = "snap_volume%s" % random.randint(10000, 99999) + snapshot["volume_name"] = volume1["name"] + self.driver.create_snapshot(snapshot) + + is_volume_defined = self.driver._is_volume_defined(snapshot["name"]) + self.assertEqual(is_volume_defined, True) + + self.driver._delete_snapshot(snapshot, True) + self.driver._delete_volume(volume1, True) + + def test_storwize_svc_volumes(self): + # Create a first volume + volume = {} + volume["name"] = "test1_volume%s" % random.randint(10000, 99999) + volume["size"] = 10 + volume["id"] = 1 + self.driver.create_volume(volume) + + # Make sure that the volume has been created + is_volume_defined = self.driver._is_volume_defined(volume["name"]) + self.assertEqual(is_volume_defined, True) + + # Make sure volume attributes are as they should be + attributes = self.driver._get_volume_attributes(volume["name"]) + attr_size = float(attributes["capacity"]) / 1073741824 # bytes to GB + self.assertEqual(attr_size, float(volume["size"])) + pool = getattr(storwize_svc.FLAGS, "storwize_svc_volpool_name") + self.assertEqual(attributes["mdisk_grp_name"], pool) + vtype = getattr(storwize_svc.FLAGS, "storwize_svc_vol_vtype") + self.assertEqual(attributes["type"], vtype) + + # Try to create the volume again (should fail) + self.assertRaises(exception.ProcessExecutionError, + self.driver.create_volume, volume) + + # Try to delete a volume that doesn't exist (should not fail) + vol_no_exist = {"name": "i_dont_exist"} + self.driver.delete_volume(vol_no_exist) + + # Delete the volume + self.driver.delete_volume(volume) + + def test_storwize_svc_host_maps(self): + # Create two volumes to be used in mappings + volume1 = {} + volume1["name"] = "test1_volume%s" % random.randint(10000, 99999) + volume1["size"] = 2 + volume1["id"] = 1 + self.driver.create_volume(volume1) + volume2 = {} + volume2["name"] = "test2_volume%s" % random.randint(10000, 99999) + volume2["size"] = 2 + volume2["id"] = 1 + self.driver.create_volume(volume2) + + # Make sure that the volumes have been created + is_volume_defined = self.driver._is_volume_defined(volume1["name"]) + self.assertEqual(is_volume_defined, True) + is_volume_defined = self.driver._is_volume_defined(volume2["name"]) + self.assertEqual(is_volume_defined, True) + + # Initialize connection from the first volume to a host + # Add some characters to the initiator name that should be converted + # when used for the host name + conn = {} + conn["initiator"] = "test:init:%s" % random.randint(10000, 99999) + conn["ip"] = "10.10.10.10" # Bogus ip for testing + self.driver.initialize_connection(volume1, conn) + + # Initialize connection from the second volume to the host + self.driver.initialize_connection(volume2, conn) + + # Try to delete the 1st volume (should fail because it is mapped) + self.assertRaises(exception.ProcessExecutionError, + self.driver.delete_volume, volume1) + + # Try to remove connection from host that doesn't exist (should fail) + conn_no_exist = {"initiator": "i_dont_exist"} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, volume1, conn_no_exist) + + # Try to remove connection from volume that isn't mapped (should print + # message but NOT fail) + vol_no_exist = {"name": "i_dont_exist"} + self.driver.terminate_connection(vol_no_exist, conn) + + # Remove the mapping from the 1st volume and delete it + self.driver.terminate_connection(volume1, conn) + self.driver.delete_volume(volume1) + vol_def = self.driver._is_volume_defined(volume1["name"]) + self.assertEqual(vol_def, False) + + # Make sure our host still exists + host_name = self.driver._get_host_from_iscsiname(conn["initiator"]) + host_def = self.driver._is_host_defined(host_name) + self.assertEquals(host_def, True) + + # Remove the mapping from the 2nd volume and delete it. The host should + # be automatically removed because there are no more mappings. + self.driver.terminate_connection(volume2, conn) + self.driver.delete_volume(volume2) + vol_def = self.driver._is_volume_defined(volume2["name"]) + self.assertEqual(vol_def, False) + + # Check if our host still exists (it should not) + ret = self.driver._get_host_from_iscsiname(conn["initiator"]) + self.assertEquals(ret, None) + ret = self.driver._is_host_defined(host_name) + self.assertEquals(ret, False) diff --git a/cinder/volume/storwize_svc.py b/cinder/volume/storwize_svc.py new file mode 100644 index 000000000..c7d34e901 --- /dev/null +++ b/cinder/volume/storwize_svc.py @@ -0,0 +1,1200 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 IBM, Inc. +# Copyright (c) 2012 OpenStack LLC. +# 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. + +""" +Volume driver for IBM Storwize V7000 and SVC storage systems. + +Notes: +1. If you specify both a password and a key file, this driver will use the + key file only. +2. When using a key file for authentication, it is up to the user or + system administrator to store the private key in a safe manner. +3. The defaults for creating volumes are "-vtype striped -rsize 2% -autoexpand + -grainsize 256 -warning 0". These can be changed in the configuration file + (recommended only for advanced users). + +Limitations: +1. The driver was not tested with SVC or clustered configurations of Storwize + V7000. +2. The driver expects CLI output in English, error messages may be in a + localized format. +""" + +import random +import re +import string +import time + +import paramiko + +from cinder import exception +from cinder import flags +from cinder.openstack.common import cfg +from cinder.openstack.common import excutils +from cinder.openstack.common import log as logging +from cinder import utils +from cinder.volume import san + +LOG = logging.getLogger(__name__) + +storwize_svc_opts = [ + cfg.StrOpt('storwize_svc_volpool_name', + default='volpool', + help='Storage system storage pool for volumes'), + cfg.StrOpt('storwize_svc_vol_vtype', + default='striped', + help='Storage system volume type for volumes'), + cfg.StrOpt('storwize_svc_vol_rsize', + default='2%', + help='Storage system space-efficiency parameter for volumes'), + cfg.StrOpt('storwize_svc_vol_warning', + default='0', + help='Storage system threshold for volume capacity warnings'), + cfg.BoolOpt('storwize_svc_vol_autoexpand', + default=True, + help='Storage system autoexpand parameter for volumes ' + '(True/False)'), + cfg.StrOpt('storwize_svc_vol_grainsize', + default='256', + help='Storage system grain size parameter for volumes ' + '(32/64/128/256)'), + cfg.StrOpt('storwize_svc_flashcopy_timeout', + default='120', + help='Maximum number of seconds to wait for FlashCopy to be' + 'prepared. Maximum value is 600 seconds (10 minutes).'), +] + +FLAGS = flags.FLAGS +FLAGS.register_opts(storwize_svc_opts) + + +class StorwizeSVCDriver(san.SanISCSIDriver): + """IBM Storwize V7000 and SVC iSCSI volume driver.""" + + def __init__(self, *args, **kwargs): + super(StorwizeSVCDriver, self).__init__(*args, **kwargs) + self.iscsi_ipv4_conf = None + self.iscsi_ipv6_conf = None + + invalid_ch_in_host = '' + for num in range(0, 128): + ch = chr(num) + if ((not ch.isalnum()) and (ch != ' ') and (ch != '.') + and (ch != '-') and (ch != '_')): + invalid_ch_in_host = invalid_ch_in_host + ch + self._string_host_name_filter = string.maketrans(invalid_ch_in_host, + '-' * len(invalid_ch_in_host)) + + self._unicode_host_name_filter = dict((ord(unicode(char)), u'-') + for char in invalid_ch_in_host) + + def _get_hdr_dic(self, header, row, delim): + """Return CLI row data as a dictionary indexed by names from header. + + Create a dictionary object from the data row string using the header + string. The strings are converted to columns using the delimiter in + delim. + """ + + attributes = header.split(delim) + values = row.split(delim) + self._driver_assert(len(values) == len(attributes), + _('_get_hdr_dic: attribute headers and values do not match.\n ' + 'Headers: %(header)s\n Values: %(row)s') + % {'header': str(header), + 'row': str(row)}) + dic = {} + for attribute, value in map(None, attributes, values): + dic[attribute] = value + return dic + + def _driver_assert(self, assert_condition, exception_message): + """Internal assertion mechanism for CLI output.""" + if not assert_condition: + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + def check_for_setup_error(self): + """Check that we have all configuration details from the storage.""" + + LOG.debug(_('enter: check_for_setup_error')) + + # Validate that the pool exists + ssh_cmd = 'lsmdiskgrp -delim ! -nohdr' + out, err = self._run_ssh(ssh_cmd) + self._driver_assert(len(out) > 0, + _('check_for_setup_error: failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + search_text = '!%s!' % getattr(FLAGS, 'storwize_svc_volpool_name') + if search_text not in out: + raise exception.InvalidParameterValue( + err=_('pool %s doesn\'t exist') + % getattr(FLAGS, 'storwize_svc_volpool_name')) + + storage_nodes = {} + # Get the iSCSI names of the Storwize/SVC nodes + ssh_cmd = 'lsnodecanister -delim !' + out, err = self._run_ssh(ssh_cmd) + self._driver_assert(len(out) > 0, + _('check_for_setup_error: failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + + nodes = out.strip().split('\n') + self._driver_assert(len(nodes) > 0, + _('check_for_setup_error: failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + header = nodes.pop(0) + for node_line in nodes: + try: + node_data = self._get_hdr_dic(header, node_line, '!') + except exception.VolumeBackendAPIException as e: + with excutils.save_and_reraise_exception(): + LOG.error(_('check_for_setup_error: ' + 'failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s\n') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + node = {} + try: + node['id'] = node_data['id'] + node['name'] = node_data['name'] + node['iscsi_name'] = node_data['iscsi_name'] + node['status'] = node_data['status'] + node['ipv4'] = [] + node['ipv6'] = [] + if node['iscsi_name'] != '': + storage_nodes[node['id']] = node + except KeyError as e: + LOG.error(_('Did not find expected column name in ' + 'lsnodecanister: %s') % str(e)) + exception_message = ( + _('check_for_setup_error: Unexpected CLI output.\n ' + 'Details: %(msg)s\n' + 'Command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s') + % {'msg': str(e), + 'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + raise exception.VolumeBackendAPIException( + data=exception_message) + + # Get the iSCSI IP addresses of the Storwize/SVC nodes + ssh_cmd = 'lsportip -delim !' + out, err = self._run_ssh(ssh_cmd) + self._driver_assert(len(out) > 0, + _('check_for_setup_error: failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + + portips = out.strip().split('\n') + self._driver_assert(len(portips) > 0, + _('check_for_setup_error: failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + header = portips.pop(0) + for portip_line in portips: + try: + port_data = self._get_hdr_dic(header, portip_line, '!') + except exception.VolumeBackendAPIException as e: + with excutils.save_and_reraise_exception(): + LOG.error(_('check_for_setup_error: ' + 'failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s\n') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + try: + port_node_id = port_data['node_id'] + port_ipv4 = port_data['IP_address'] + port_ipv6 = port_data['IP_address_6'] + except KeyError as e: + LOG.error(_('Did not find expected column name in ' + 'lsportip: %s') % str(e)) + exception_message = ( + _('check_for_setup_error: Unexpected CLI output.\n ' + 'Details: %(msg)s\n' + 'Command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s') + % {'msg': str(e), + 'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + raise exception.VolumeBackendAPIException( + data=exception_message) + + if port_node_id in storage_nodes: + node = storage_nodes[port_node_id] + if len(port_ipv4) > 0: + node['ipv4'].append(port_ipv4) + if len(port_ipv6) > 0: + node['ipv6'].append(port_ipv6) + else: + raise exception.VolumeBackendAPIException( + data=_('check_for_setup_error: ' + 'fail to storage configuration: unknown ' + 'storage node %(node_id)s from CLI output.\n ' + 'stdout: %(out)s\n stderr: %(err)s\n') + % {'node_id': port_node_id, + 'out': str(out), + 'err': str(err)}) + + iscsi_ipv4_conf = [] + iscsi_ipv6_conf = [] + for node_key in storage_nodes: + node = storage_nodes[node_key] + if 'ipv4' in node and len(node['iscsi_name']) > 0: + iscsi_ipv4_conf.append({'iscsi_name': node['iscsi_name'], + 'ip': node['ipv4'], + 'node_id': node['id']}) + if 'ipv6' in node and len(node['iscsi_name']) > 0: + iscsi_ipv6_conf.append({'iscsi_name': node['iscsi_name'], + 'ip': node['ipv6'], + 'node_id': node['id']}) + if (len(node['ipv4']) == 0) and (len(node['ipv6']) == 0): + raise exception.VolumeBackendAPIException( + data=_('check_for_setup_error: ' + 'fail to storage configuration: storage ' + 'node %s has no IP addresses configured') + % node['id']) + + #Make sure we have at least one IPv4 address with a iSCSI name + #TODO(ronenkat) need to expand this to support IPv6 + self._driver_assert(len(iscsi_ipv4_conf) > 0, + _('could not obtain IP address and iSCSI name from the storage. ' + 'Please verify that the storage is configured for iSCSI.\n ' + 'Storage nodes: %(nodes)s\n portips: %(portips)s') + % {'nodes': nodes, 'portips': portips}) + + self.iscsi_ipv4_conf = iscsi_ipv4_conf + self.iscsi_ipv6_conf = iscsi_ipv6_conf + + LOG.debug(_('leave: check_for_setup_error')) + + def _check_num_perc(self, value): + """Return True if value is either a number or a percentage.""" + if value.endswith('%'): + value = value[0:-1] + return value.isdigit() + + def _check_flags(self): + """Ensure that the flags are set properly.""" + + required_flags = ['san_ip', 'san_ssh_port', 'san_login', + 'storwize_svc_volpool_name'] + for flag in required_flags: + if not getattr(FLAGS, flag, None): + raise exception.InvalidParameterValue( + err=_('%s is not set') % flag) + + # Ensure that either password or keyfile were set + if not (getattr(FLAGS, 'san_password', None) + or getattr(FLAGS, 'san_private_key', None)): + raise exception.InvalidParameterValue( + err=_('Password or SSH private key is required for ' + 'authentication: set either san_password or ' + 'san_private_key option')) + + # vtype should either be 'striped' or 'seq' + vtype = getattr(FLAGS, 'storwize_svc_vol_vtype') + if vtype not in ['striped', 'seq']: + raise exception.InvalidParameterValue( + err=_('Illegal value specified for storwize_svc_vol_vtype: ' + 'set to either \'striped\' or \'seq\'')) + + # Check that rsize is a number or percentage + rsize = getattr(FLAGS, 'storwize_svc_vol_rsize') + if not self._check_num_perc(rsize) and (rsize not in ['auto', '-1']): + raise exception.InvalidParameterValue( + err=_('Illegal value specified for storwize_svc_vol_rsize: ' + 'set to either a number or a percentage')) + + # Check that warning is a number or percentage + warning = getattr(FLAGS, 'storwize_svc_vol_warning') + if not self._check_num_perc(warning): + raise exception.InvalidParameterValue( + err=_('Illegal value specified for storwize_svc_vol_warning: ' + 'set to either a number or a percentage')) + + # Check that autoexpand is a boolean + autoexpand = getattr(FLAGS, 'storwize_svc_vol_autoexpand') + if type(autoexpand) != type(True): + raise exception.InvalidParameterValue( + err=_('Illegal value specified for ' + 'storwize_svc_vol_autoexpand: set to either ' + 'True or False')) + + # Check that grainsize is 32/64/128/256 + grainsize = getattr(FLAGS, 'storwize_svc_vol_grainsize') + if grainsize not in ['32', '64', '128', '256']: + raise exception.InvalidParameterValue( + err=_('Illegal value specified for ' + 'storwize_svc_vol_grainsize: set to either ' + '\'32\', \'64\', \'128\', or \'256\'')) + + # Check that flashcopy_timeout is numeric and 32/64/128/256 + flashcopy_timeout = getattr(FLAGS, 'storwize_svc_flashcopy_timeout') + if not (flashcopy_timeout.isdigit() and int(flashcopy_timeout) > 0 and + int(flashcopy_timeout) <= 600): + raise exception.InvalidParameterValue( + err=_('Illegal value %s specified for ' + 'storwize_svc_flashcopy_timeout: ' + 'valid values are between 0 and 600') + % flashcopy_timeout) + + def do_setup(self, context): + """Validate the flags.""" + LOG.debug(_('enter: do_setup')) + self._check_flags() + LOG.debug(_('leave: do_setup')) + + def create_volume(self, volume): + """Create a new volume - uses the internal method.""" + return self._create_volume(volume, units='gb') + + def _create_volume(self, volume, units='gb'): + """Create a new volume.""" + + name = volume['name'] + model_update = None + + LOG.debug(_('enter: create_volume: volume %s ') % name) + + size = int(volume['size']) + + if getattr(FLAGS, 'storwize_svc_vol_autoexpand') == True: + autoexpand = '-autoexpand' + else: + autoexpand = '' + + #Set space-efficient options + if getattr(FLAGS, 'storwize_svc_vol_rsize').strip() == '-1': + ssh_cmd_se_opt = '' + else: + ssh_cmd_se_opt = ('-rsize %(rsize)s %(autoexpand)s ' + '-grainsize %(grain)s' % + {'rsize': getattr(FLAGS, 'storwize_svc_vol_rsize'), + 'autoexpand': autoexpand, + 'grain': + getattr(FLAGS, 'storwize_svc_vol_grainsize')}) + + ssh_cmd = ('mkvdisk -name %(name)s -mdiskgrp %(mdiskgrp)s ' + '-iogrp 0 -vtype %(vtype)s -size %(size)s -unit ' + '%(unit)s %(ssh_cmd_se_opt)s' + % {'name': name, + 'mdiskgrp': getattr(FLAGS, 'storwize_svc_volpool_name'), + 'vtype': getattr(FLAGS, 'storwize_svc_vol_vtype'), + 'size': size, 'unit': units, + 'ssh_cmd_se_opt': ssh_cmd_se_opt}) + out, err = self._run_ssh(ssh_cmd) + self._driver_assert(len(out.strip()) > 0, + _('create volume %(name)s - did not find ' + 'success message in CLI output.\n ' + 'stdout: %(out)s\n stderr: %(err)s') + % {'name': name, 'out': str(out), 'err': str(err)}) + + # Ensure that the output is as expected + match_obj = re.search('Virtual Disk, id \[([0-9]+)\], ' + 'successfully created', out) + # Make sure we got a "successfully created" message with vdisk id + self._driver_assert(match_obj is not None, + _('create volume %(name)s - did not find ' + 'success message in CLI output.\n ' + 'stdout: %(out)s\n stderr: %(err)s') + % {'name': name, 'out': str(out), 'err': str(err)}) + + LOG.debug(_('leave: create_volume: volume %(name)s ') % {'name': name}) + + def delete_volume(self, volume): + self._delete_volume(volume, False) + + def _delete_volume(self, volume, force_opt): + """Driver entry point for destroying existing volumes.""" + + name = volume['name'] + LOG.debug(_('enter: delete_volume: volume %(name)s ') % {'name': name}) + + if force_opt: + force_flag = '-force' + else: + force_flag = '' + + volume_defined = self._is_volume_defined(name) + # Try to delete volume only if found on the storage + if volume_defined: + out, err = self._run_ssh('rmvdisk %(force)s %(name)s' + % {'force': force_flag, + 'name': name}) + # No output should be returned from rmvdisk + self._driver_assert(len(out.strip()) == 0, + _('delete volume %(name)s - non empty output from CLI.\n ' + 'stdout: %(out)s\n stderr: %(err)s') + % {'name': name, + 'out': str(out), + 'err': str(err)}) + else: + # Log that volume does not exist + LOG.info(_('warning: tried to delete volume %(name)s but ' + 'it does not exist.') % {'name': name}) + + LOG.debug(_('leave: delete_volume: volume %(name)s ') % {'name': name}) + + def ensure_export(self, context, volume): + """Check that the volume exists on the storage. + + The system does not "export" volumes as a Linux iSCSI target does, + and therefore we just check that the volume exists on the storage. + """ + volume_defined = self._is_volume_defined(volume['name']) + if not volume_defined: + LOG.error(_('ensure_export: volume %s not found on storage') + % volume['name']) + + def create_export(self, context, volume): + model_update = None + return model_update + + def remove_export(self, context, volume): + pass + + def check_for_export(self, context, volume_id): + raise NotImplementedError() + + def initialize_connection(self, volume, connector): + """Perform the necessary work so that an iSCSI connection can be made. + + To be able to create an iSCSI connection from a given iSCSI name to a + volume, we must: + 1. Translate the given iSCSI name to a host name + 2. Create new host on the storage system if it does not yet exist + 2. Map the volume to the host if it is not already done + 3. Return iSCSI properties, including the IP address of the preferred + node for this volume and the LUN number. + """ + LOG.debug(_('enter: initialize_connection: volume %(vol)s with ' + 'connector %(conn)s') % {'vol': str(volume), + 'conn': str(connector)}) + + initiator_name = connector['initiator'] + volume_name = volume['name'] + + host_name = self._get_host_from_iscsiname(initiator_name) + # Check if a host is defined for the iSCSI initiator name + if host_name is None: + # Host does not exist - add a new host to Storwize/SVC + host_name = self._create_new_host('host%s' % initiator_name, + initiator_name) + # Verify that create_new_host succeeded + self._driver_assert(host_name is not None, + _('_create_new_host failed to return the host name.')) + + lun_id = self._map_vol_to_host(volume_name, host_name) + + # Get preferred path + # Only IPv4 for now because lack of OpenStack support + # TODO(ronenkat): Add support for IPv6 + volume_attributes = self._get_volume_attributes(volume_name) + if (volume_attributes is not None and + 'preferred_node_id' in volume_attributes): + preferred_node = volume_attributes['preferred_node_id'] + preferred_node_entry = None + for node in self.iscsi_ipv4_conf: + if node['node_id'] == preferred_node: + preferred_node_entry = node + break + if preferred_node_entry is None: + preferred_node_entry = self.iscsi_ipv4_conf[0] + LOG.error(_('initialize_connection: did not find preferred ' + 'node %(node)s for volume %(vol)s in iSCSI ' + 'configuration') % {'node': preferred_node, + 'vol': volume_name}) + else: + # Get 1st node + preferred_node_entry = self.iscsi_ipv4_conf[0] + LOG.error( + _('initialize_connection: did not find a preferred node ' + 'for volume %s in iSCSI configuration') % volume_name) + + properties = {} + # We didn't use iSCSI discover, as in server-based iSCSI + properties['target_discovered'] = False + # We take the first IP address for now. Ideally, OpenStack will + # support multipath for improved performance. + properties['target_portal'] = ('%s:%s' % + (preferred_node_entry['ip'][0], '3260')) + properties['target_iqn'] = preferred_node_entry['iscsi_name'] + properties['target_lun'] = lun_id + properties['volume_id'] = volume['id'] + + LOG.debug(_('leave: initialize_connection:\n volume: %(vol)s\n ' + 'connector %(conn)s\n properties: %(prop)s') + % {'vol': str(volume), + 'conn': str(connector), + 'prop': str(properties)}) + + return {'driver_volume_type': 'iscsi', 'data': properties, } + + def terminate_connection(self, volume, connector): + """Cleanup after an iSCSI connection has been terminated. + + When we clean up a terminated connection between a given iSCSI name + and volume, we: + 1. Translate the given iSCSI name to a host name + 2. Remove the volume-to-host mapping if it exists + 3. Delete the host if it has no more mappings (hosts are created + automatically by this driver when mappings are created) + """ + LOG.debug(_('enter: terminate_connection: volume %(vol)s with ' + 'connector %(conn)s') % {'vol': str(volume), + 'conn': str(connector)}) + + vol_name = volume['name'] + initiator_name = connector['initiator'] + host_name = self._get_host_from_iscsiname(initiator_name) + # Verify that _get_host_from_iscsiname returned the host. + # This should always succeed as we terminate an existing connection. + self._driver_assert(host_name is not None, + _('_get_host_from_iscsiname failed to return the host name ' + 'for iscsi name %s') % initiator_name) + + # Check if vdisk-host mapping exists, remove if it does + mapping_data = self._get_hostvdisk_mappings(host_name) + if vol_name in mapping_data: + out, err = self._run_ssh('rmvdiskhostmap -host %s %s' + % (host_name, vol_name)) + # Verify CLI behaviour - no output is returned from + # rmvdiskhostmap + self._driver_assert(len(out.strip()) == 0, + _('delete mapping of volume %(vol)s to host %(host)s ' + '- non empty output from CLI.\n ' + 'stdout: %(out)s\n stderr: %(err)s') + % {'vol': vol_name, + 'host': host_name, + 'out': str(out), + 'err': str(err)}) + del mapping_data[vol_name] + else: + LOG.error(_('terminate_connection: no mapping of volume ' + '%(vol)s to host %(host)s found') % + {'vol': vol_name, 'host': host_name}) + + # If this host has no more mappings, delete it + if not mapping_data: + self._delete_host(host_name) + + LOG.debug(_('leave: terminate_connection: volume %(vol)s with ' + 'connector %(conn)s') % {'vol': str(volume), + 'conn': str(connector)}) + + def _run_flashcopy(self, source, target): + """Create a FlashCopy mapping from the source to the target.""" + + LOG.debug( + _('enter: _run_flashcopy: execute FlashCopy from source ' + '%(source)s to target %(target)s') % {'source': source, + 'target': target}) + + fc_map_cli_cmd = ('mkfcmap -source %s -target %s -autodelete ' + '-cleanrate 0' % (source, target)) + out, err = self._run_ssh(fc_map_cli_cmd) + self._driver_assert(len(out.strip()) > 0, + _('create FC mapping from %(source)s to %(target)s - ' + 'did not find success message in CLI output.\n' + ' stdout: %(out)s\n stderr: %(err)s\n') + % {'source': source, + 'target': target, + 'out': str(out), + 'err': str(err)}) + + # Ensure that the output is as expected + match_obj = re.search('FlashCopy Mapping, id \[([0-9]+)\], ' + 'successfully created', out) + # Make sure we got a "successfully created" message with vdisk id + self._driver_assert(match_obj is not None, + _('create FC mapping from %(source)s to %(target)s - ' + 'did not find success message in CLI output.\n' + ' stdout: %(out)s\n stderr: %(err)s\n') + % {'source': source, + 'target': target, + 'out': str(out), + 'err': str(err)}) + + try: + fc_map_id = match_obj.group(1) + self._driver_assert(fc_map_id is not None, + _('create FC mapping from %(source)s to %(target)s - ' + 'did not find mapping id in CLI output.\n' + ' stdout: %(out)s\n stderr: %(err)s\n') + % {'source': source, + 'target': target, + 'out': str(out), + 'err': str(err)}) + except IndexError: + self._driver_assert(False, + _('create FC mapping from %(source)s to %(target)s - ' + 'did not find mapping id in CLI output.\n' + ' stdout: %(out)s\n stderr: %(err)s\n') + % {'source': source, + 'target': target, + 'out': str(out), + 'err': str(err)}) + try: + out, err = self._run_ssh('prestartfcmap %s' % fc_map_id) + except exception.ProcessExecutionError as e: + with excutils.save_and_reraise_exception(): + LOG.error(_('_run_flashcopy: fail to prepare FlashCopy ' + 'from %(source)s to %(target)s.\n' + 'stdout: %(out)s\n stderr: %(err)s') + % {'source': source, + 'target': target, + 'out': e.stdout, + 'err': e.stderr}) + + mapping_ready = False + wait_time = 5 + # Allow waiting of up to timeout (set as parameter) + exception_msg = (_('mapping %(id)s prepare failed to complete ' + 'within the alloted %(to)s seconds timeout. ' + 'Terminating') % {'id': fc_map_id, + 'to': getattr( + FLAGS, 'storwize_svc_flashcopy_timeout')}) + max_retries = (int(getattr(FLAGS, + 'storwize_svc_flashcopy_timeout')) / wait_time) + 1 + for try_number in range(1, max_retries): + mapping_attributes = self._get_flashcopy_mapping_attributes( + fc_map_id) + if (mapping_attributes is None or + 'status' not in mapping_attributes): + break + if mapping_attributes['status'] == 'prepared': + mapping_ready = True + break + elif mapping_attributes['status'] != 'preparing': + # Unexpected mapping status + exception_msg = (_('unexecpted mapping status %(status)s ' + 'for mapping %(id)s. Attributes: ' + '%(attr)s') + % {'status': mapping_attributes['status'], + 'id': fc_map_id, + 'attr': mapping_attributes}) + break + # Need to wait for mapping to be prepared, wait a few seconds + time.sleep(wait_time) + + if not mapping_ready: + LOG.error(_('_run_flashcopy: fail to start FlashCopy ' + 'from %(source)s to %(target)s with ' + 'exception %(ex)s') + % {'source': source, + 'target': target, + 'ex': exception_msg}) + raise exception.InvalidSnapshot( + reason=_('_run_flashcopy: %s') % exception_msg) + + try: + out, err = self._run_ssh('startfcmap %s' % fc_map_id) + except exception.ProcessExecutionError as e: + with excutils.save_and_reraise_exception(): + LOG.error(_('_run_flashcopy: fail to start FlashCopy ' + 'from %(source)s to %(target)s.\n' + 'stdout: %(out)s\n stderr: %(err)s') + % {'source': source, + 'target': target, + 'out': e.stdout, + 'err': e.stderr}) + + LOG.debug(_('leave: _run_flashcopy: FlashCopy started from ' + '%(source)s to %(target)s') % {'source': source, + 'target': target}) + + def create_volume_from_snapshot(self, volume, snapshot): + """Create a new snapshot from volume.""" + + source_volume = volume['name'] + tgt_volume = snapshot['name'] + + LOG.debug(_('enter: create_volume_from_snapshot: snapshot %(tgt)s ' + 'from volume %(src)s') % {'tgt': tgt_volume, + 'src': source_volume}) + + src_volume_attributes = self._get_volume_attributes(source_volume) + if src_volume_attributes is None: + exception_msg = (_('create_volume_from_snapshot: source volume %s ' + 'does not exist') % source_volume) + LOG.error(exception_msg) + raise exception.VolumeNotFound(exception_msg, + volume_id=source_volume) + if 'capacity' not in src_volume_attributes: + exception_msg = ( + _('create_volume_from_snapshot: cannot get source ' + 'volume %(src)s capacity from volume attributes ' + '%(attr)s') % {'src': source_volume, + 'attr': src_volume_attributes}) + LOG.error(exception_msg) + raise exception.VolumeBackendAPIException(data=exception_msg) + src_volume_size = src_volume_attributes['capacity'] + + tgt_volume_attributes = self._get_volume_attributes(tgt_volume) + # Does the snapshot target exist? + if tgt_volume_attributes is not None: + exception_msg = (_('create_volume_from_snapshot: target volume %s ' + 'already exists, cannot create') % tgt_volume) + LOG.error(exception_msg) + raise exception.InvalidSnapshot(reason=exception_msg) + + snapshot_volume = {} + snapshot_volume['name'] = tgt_volume + snapshot_volume['size'] = src_volume_size + + self.create_volume(snapshot_volume) + + self._run_flashcopy(source_volume, tgt_volume) + + LOG.debug( + _('leave: create_volume_from_snapshot: %s created successfully') + % tgt_volume) + + def create_snapshot(self, snapshot): + """Create a new snapshot using FlashCopy.""" + + src_volume = snapshot['volume_name'] + tgt_volume = snapshot['name'] + + LOG.debug(_('enter: create_snapshot: snapshot %(tgt)s from ' + 'volume %(src)s') % {'tgt': tgt_volume, + 'src': src_volume}) + + src_volume_attributes = self._get_volume_attributes(src_volume) + if src_volume_attributes is None: + exception_msg = ( + _('create_snapshot: source volume %s does not exist') + % src_volume) + LOG.error(exception_msg) + raise exception.VolumeNotFound(exception_msg, + volume_id=src_volume) + if 'capacity' not in src_volume_attributes: + exception_msg = ( + _('create_snapshot: cannot get source volume %(src)s ' + 'capacity from volume attributes %(attr)s') + % {'src': src_volume, + 'attr': src_volume_attributes}) + LOG.error(exception_msg) + raise exception.VolumeBackendAPIException(data=exception_msg) + source_volume_size = src_volume_attributes['capacity'] + + tgt_volume_attributes = self._get_volume_attributes(tgt_volume) + # Does the snapshot target exist? + if tgt_volume_attributes is None: + # No, create a new snapshot volume + snapshot_volume = {} + snapshot_volume['name'] = tgt_volume + snapshot_volume['size'] = source_volume_size + self._create_volume(snapshot_volume, units='b') + else: + # Yes, target exists, verify exact same size as source + if 'capacity' not in src_volume_attributes: + exception_msg = ( + _('create_snapshot: cannot get target volume ' + '%(tgt)s capacity from volume attributes ' + '%(attr)s') % {'tgt': tgt_volume, + 'attr': tgt_volume_attributes}) + LOG.error(exception_msg) + raise exception.VolumeBackendAPIException(data=exception_msg) + target_volume_size = tgt_volume_attributes['capacity'] + if target_volume_size != source_volume_size: + exception_msg = ( + _('create_snapshot: source %(src)s and target ' + 'volume %(tgt)s have different capacities ' + '(source:%(ssize)s target:%(tsize)s)') % + {'src': src_volume, + 'tgt': tgt_volume, + 'ssize': source_volume_size, + 'tsize': target_volume_size}) + LOG.error(exception_msg) + raise exception.InvalidSnapshot(reason=exception_msg) + + self._run_flashcopy(src_volume, tgt_volume) + + LOG.debug(_('leave: create_snapshot: %s created successfully') + % tgt_volume) + + def delete_snapshot(self, snapshot): + self._delete_snapshot(snapshot, False) + + def _delete_snapshot(self, snapshot, force_opt): + """Delete a snapshot from the storage.""" + LOG.debug(_('enter: delete_snapshot: snapshot %s') % snapshot) + + snapshot_defined = self._is_volume_defined(snapshot['name']) + if snapshot_defined: + if force_opt: + self._delete_volume(snapshot, force_opt) + else: + self.delete_volume(snapshot) + + LOG.debug(_('leave: delete_snapshot: snapshot %s') % snapshot) + + def _get_host_from_iscsiname(self, iscsi_name): + """List the hosts defined in the storage. + + Return the host name with the given iSCSI name, or None if there is + no host name with that iSCSI name. + """ + + LOG.debug(_('enter: _get_host_from_iscsiname: iSCSI initiator %s') + % iscsi_name) + + # Get list of host in the storage + ssh_cmd = 'lshost -delim !' + out, err = self._run_ssh(ssh_cmd) + + if (len(out.strip()) == 0): + return None + + err_msg = _('_get_host_from_iscsiname: ' + 'failed with unexpected CLI output.\n' + ' command: %(cmd)s\n stdout: %(out)s\n ' + 'stderr: %(err)s') % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)} + host_lines = out.strip().split('\n') + self._driver_assert(len(host_lines) > 0, err_msg) + header = host_lines.pop(0).split('!') + self._driver_assert('name' in header, err_msg) + name_index = header.index('name') + + hosts = map(lambda x: x.split('!')[name_index], host_lines) + hostname = None + + # For each host, get its details and check for its iSCSI name + for host in hosts: + ssh_cmd = 'lshost -delim ! %s' % host + out, err = self._run_ssh(ssh_cmd) + self._driver_assert(len(out) > 0, + _('_get_host_from_iscsiname: ' + 'Unexpected response from CLI output. ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + for attrib_line in out.split('\n'): + #If '!' not found, will return the string and two empty strings + attrib_name, foo, attrib_value = attrib_line.partition('!') + if attrib_name == 'iscsi_name': + if iscsi_name == attrib_value: + hostname = host + break + if hostname is not None: + break + + LOG.debug(_('leave: _get_host_from_iscsiname: iSCSI initiator %s') + % iscsi_name) + + return hostname + + def _create_new_host(self, host_name, initiator_name): + """Create a new host on the storage system. + + We modify the given host name, replace any invalid characters and + adding a random suffix to avoid conflicts due to the translation. The + host is associated with the given iSCSI initiator name. + """ + + LOG.debug(_('enter: _create_new_host: host %(name)s with iSCSI ' + 'initiator %(init)s') % {'name': host_name, + 'init': initiator_name}) + + if isinstance(host_name, unicode): + host_name = host_name.translate(self._unicode_host_name_filter) + elif isinstance(host_name, str): + host_name = host_name.translate(self._string_host_name_filter) + else: + msg = _('_create_new_host: cannot clean host name. Host name ' + 'is not unicode or string') + LOG.error(msg) + raise exception.NoValidHost(reason=msg) + + # Add 5 digit random suffix to the host name to avoid + # conflicts in host names after removing invalid characters + # for Storwize/SVC names + host_name = '%s_%s' % (host_name, random.randint(10000, 99999)) + out, err = self._run_ssh('mkhost -name "%s" -iscsiname "%s"' + % (host_name, initiator_name)) + self._driver_assert(len(out.strip()) > 0 and + 'successfully created' in out, + _('create host %(name)s with iSCSI initiator %(init)s - ' + 'did not find success message in CLI output.\n ' + 'stdout: %(out)s\n stderr: %(err)s\n') + % {'name': host_name, + 'init': initiator_name, + 'out': str(out), + 'err': str(err)}) + + LOG.debug(_('leave: _create_new_host: host %(host)s with iSCSI ' + 'initiator %(init)s') % {'host': host_name, + 'init': initiator_name}) + + return host_name + + def _delete_host(self, host_name): + """Delete a host and associated iSCSI initiator name.""" + + LOG.debug(_('enter: _delete_host: host %s ') % host_name) + + # Check if host exists on system, expect to find the host + is_defined = self._is_host_defined(host_name) + if is_defined: + # Delete host + out, err = self._run_ssh('rmhost %s ' % host_name) + else: + LOG.info(_('warning: tried to delete host %(name)s but ' + 'it does not exist.') % {'name': host_name}) + + LOG.debug(_('leave: _delete_host: host %s ') % host_name) + + def _is_volume_defined(self, volume_name): + """Check if volume is defined.""" + LOG.debug(_('enter: _is_volume_defined: volume %s ') % volume_name) + volume_attributes = self._get_volume_attributes(volume_name) + LOG.debug(_('leave: _is_volume_defined: volume %(vol)s with %(str)s ') + % {'vol': volume_name, + 'str': volume_attributes is not None}) + if volume_attributes is None: + return False + else: + return True + + def _is_host_defined(self, host_name): + """Check if a host is defined on the storage.""" + + LOG.debug(_('enter: _is_host_defined: host %s ') % host_name) + + # Get list of hosts with the name %host_name% + # We expect zero or one line if host does not exist, + # two lines if it does exist, otherwise error + out, err = self._run_ssh('lshost -filtervalue name=%s -delim !' + % host_name) + if len(out.strip()) == 0: + return False + + lines = out.strip().split('\n') + self._driver_assert(len(lines) <= 2, + _('_is_host_defined: Unexpected response from CLI output.\n ' + 'stdout: %(out)s\n stderr: %(err)s\n') + % {'out': str(out), + 'err': str(err)}) + + if len(lines) == 2: + host_info = self._get_hdr_dic(lines[0], lines[1], '!') + host_name_from_storage = host_info['name'] + # Make sure we got the data for the right host + self._driver_assert(host_name_from_storage == host_name, + _('Data received for host %(host1)s instead of host ' + '%(host2)s.\n ' + 'stdout: %(out)s\n stderr: %(err)s\n') + % {'host1': host_name_from_storage, + 'host2': host_name, + 'out': str(out), + 'err': str(err)}) + else: # 0 or 1 lines + host_name_from_storage = None + + LOG.debug(_('leave: _is_host_defined: host %(host)s with %(str)s ') % { + 'host': host_name, + 'str': host_name_from_storage is not None}) + + if host_name_from_storage is None: + return False + else: + return True + + def _get_hostvdisk_mappings(self, host_name): + """Return the defined storage mappings for a host.""" + + return_data = {} + ssh_cmd = 'lshostvdiskmap -delim ! %s' % host_name + out, err = self._run_ssh(ssh_cmd) + if len(out.strip()) == 0: + return return_data + + mappings = out.strip().split('\n') + if len(mappings) > 0: + header = mappings.pop(0) + for mapping_line in mappings: + mapping_data = self._get_hdr_dic(header, mapping_line, '!') + return_data[mapping_data['vdisk_name']] = mapping_data + + return return_data + + def _map_vol_to_host(self, volume_name, host_name): + """Create a mapping between a volume to a host.""" + + LOG.debug(_('enter: _map_vol_to_host: volume %(vol)s to ' + 'host %(host)s') % {'vol': volume_name, + 'host': host_name}) + + # Check if this volume is already mapped to this host + mapping_data = self._get_hostvdisk_mappings(host_name) + + mapped_flag = False + result_lun = '-1' + if volume_name in mapping_data: + mapped_flag = True + result_lun = mapping_data[volume_name]['SCSI_id'] + else: + lun_used = [] + for k, v in mapping_data.iteritems(): + lun_used.append(int(v['SCSI_id'])) + lun_used.sort() + # Assume all luns are taken to this point, and then try to find + # an unused one + result_lun = str(len(lun_used)) + for index, n in enumerate(lun_used): + if n > index: + result_lun = str(index) + + # Volume is not mapped to host, create a new LUN + if not mapped_flag: + out, err = self._run_ssh('mkvdiskhostmap -host %s -scsi %s %s' + % (host_name, result_lun, volume_name)) + self._driver_assert(len(out.strip()) > 0 and + 'successfully created' in out, + _('_map_vol_to_host: mapping host %(host)s to ' + 'volume %(vol)s with LUN ' + '%(lun)s - did not find success message in CLI output. ' + 'stdout: %(out)s\n stderr: %(err)s\n') + % {'host': host_name, + 'vol': volume_name, + 'lun': result_lun, + 'out': str(out), + 'err': str(err)}) + + LOG.debug(_('leave: _map_vol_to_host: LUN %(lun)s, volume %(vol)s, ' + 'host %(host)s') % {'lun': result_lun, 'vol': volume_name, + 'host': host_name}) + + return result_lun + + def _get_flashcopy_mapping_attributes(self, fc_map_id): + """Return the attributes of a FlashCopy mapping. + + Returns the attributes for the specified FlashCopy mapping, or + None if the mapping does not exist. + An exception is raised if the information from system can not + be parsed or matched to a single FlashCopy mapping (this case + should not happen under normal conditions). + """ + + LOG.debug(_('enter: _get_flashcopy_mapping_attributes: mapping %s') + % fc_map_id) + # Get the lunid to be used + + fc_ls_map_cmd = ('lsfcmap -filtervalue id=%s -delim !' % fc_map_id) + out, err = self._run_ssh(fc_ls_map_cmd) + self._driver_assert(len(out) > 0, + _('_get_flashcopy_mapping_attributes: ' + 'Unexpected response from CLI output. ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'cmd': fc_ls_map_cmd, + 'out': str(out), + 'err': str(err)}) + + # Get list of FlashCopy mappings + # We expect zero or one line if mapping does not exist, + # two lines if it does exist, otherwise error + lines = out.strip().split('\n') + self._driver_assert(len(lines) <= 2, + _('_get_flashcopy_mapping_attributes: ' + 'Unexpected response from CLI output. ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'cmd': fc_ls_map_cmd, + 'out': str(out), + 'err': str(err)}) + + if len(lines) == 2: + attributes = self._get_hdr_dic(lines[0], lines[1], '!') + else: # 0 or 1 lines + attributes = None + + LOG.debug(_('leave: _get_flashcopy_mapping_attributes: mapping ' + '%(id)s, attributes %(attr)s') % + {'id': fc_map_id, + 'attr': attributes}) + + return attributes + + def _get_volume_attributes(self, volume_name): + """Return volume attributes, or None if volume does not exist + + Exception is raised if the information from system can not be + parsed/matched to a single volume. + """ + + LOG.debug(_('enter: _get_volume_attributes: volume %s') + % volume_name) + # Get the lunid to be used + + try: + ssh_cmd = 'lsvdisk -bytes -delim ! %s ' % volume_name + out, err = self._run_ssh(ssh_cmd) + except exception.ProcessExecutionError as e: + #Didn't get details from the storage, return None + LOG.error(_('CLI Exception output:\n command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s') % + {'cmd': ssh_cmd, + 'out': e.stdout, + 'err': e.stderr}) + return None + + self._driver_assert(len(out) > 0, + ('_get_volume_attributes: ' + 'Unexpected response from CLI output. ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) + attributes = {} + for attrib_line in out.split('\n'): + #If '!' not found, will return the string and two empty strings + attrib_name, foo, attrib_value = attrib_line.partition('!') + if attrib_name is not None and attrib_name.strip() > 0: + attributes[attrib_name] = attrib_value + + LOG.debug(_('leave: _get_volume_attributes:\n volume %(vol)s\n ' + 'attributes: %(attr)s') + % {'vol': volume_name, + 'attr': str(attributes)}) + + return attributes diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 3dba756d1..4d079bfe5 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -656,4 +656,21 @@ ###### (StrOpt) The ZFS path under which to create zvols for volumes. # san_zfs_volume_base="rpool/" -# Total option count: 467 +######### defined in cinder.volume.storwize_svc ######### + +##### (StrOpt) Storage system storage pool for volumes +# storwize_svc_volpool_name=volpool +##### (StrOpt) Storage system volume type for volumes +# storwize_svc_vol_vtype=striped +##### (StrOpt) Storage system space-efficiency parameter for volumes +# storwize_svc_vol_rsize=2% +##### (StrOpt) Storage system threshold for volume capacity warnings +# storwize_svc_vol_warning=0 +##### (BoolOpt) Storage system autoexpand parameter for volumes (True/False) +# storwize_svc_vol_autoexpand=True +##### (StrOpt) Storage system grain size parameter for volumes (32/64/128/256) +# storwize_svc_vol_grainsize=256 +##### (StrOpt) Maximum number of seconds to wait for FlashCopy to be prepared. Maximum value is 600 seconds (10 minutes). +# storwize_svc_flashcopy_timeout=120 + +# Total option count: 474 -- 2.45.2