From b3e64b8192d31cf2f28f01dd4e3e97b3e53af979 Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Thu, 7 Feb 2013 21:50:31 +0200 Subject: [PATCH] Update Storwize/SVC driver for Grizzly. Includes FC support, create_cloned_volume, volume type support, get_volume_stats, and minor bug fixes. Change-Id: I13e3d7921c5127e6b4e0cbb4e91761e0249ec295 --- cinder/tests/test_storwize_svc.py | 2311 +++++++++++++++---------- cinder/volume/driver.py | 11 +- cinder/volume/drivers/storwize_svc.py | 2019 +++++++++++---------- 3 files changed, 2451 insertions(+), 1890 deletions(-) diff --git a/cinder/tests/test_storwize_svc.py b/cinder/tests/test_storwize_svc.py index 5f7c471d6..5f9d7b782 100644 --- a/cinder/tests/test_storwize_svc.py +++ b/cinder/tests/test_storwize_svc.py @@ -1,7 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright (c) 2012 IBM, Inc. -# Copyright (c) 2012 OpenStack LLC. +# Copyright 2012, 2013 IBM Corp +# Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -21,93 +21,154 @@ # Avishay Traeger """ -Tests for the IBM Storwize V7000 and SVC volume driver. +Tests for the IBM Storwize family and SVC volume driver. """ import mox import random +import re import socket +from cinder import context from cinder import exception from cinder import flags 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 configuration as conf from cinder.volume.drivers import storwize_svc +from cinder.volume import volume_types FLAGS = flags.FLAGS LOG = logging.getLogger(__name__) +class StorwizeSVCFakeDB: + def __init__(self): + self.volume = None + + def volume_get(self, context, vol_id): + return self.volume + + def volume_set(self, vol): + self.volume = vol + + class StorwizeSVCManagementSimulator: def __init__(self, pool_name): - self._flags = {"storwize_svc_volpool_name": 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 = { - "lsportip": "", - "lsnodecanister": "", - "mkvdisk": "", - "lsvdisk": "", - "lsfcmap": "", - "prestartfcmap": "", - "startfcmap": "", - "rmfcmap": "", + 'lsportip': '', + 'lsportfc': '', + 'lsfabric': '', + 'lsiscsiauth': '', + 'lsnodecanister': '', + 'mkvdisk': '', + 'lsvdisk': '', + 'lsfcmap': '', + 'prestartfcmap': '', + 'startfcmap': '', + 'rmfcmap': '', + 'lslicense': '', } 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."), - "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."), - "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."), - "CMMVC7050E": ("", "CMMVC7050E The command failed because at " + - "least one node in the I/O group does not " + - "support compressed VDisks."), + '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.'), + '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.'), + '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.'), + 'CMMVC7050E': ('', 'CMMVC7050E The command failed because at ' + 'least one node in the I/O group does not ' + 'support compressed VDisks.'), + # Catch-all for invalid state transitions: + 'CMMVC5903E': ('', 'CMMVC5903E The FlashCopy mapping was not ' + 'changed because the mapping or consistency ' + 'group is another state.'), } + self._transitions = {'begin': {'make': 'idle_or_copied'}, + 'idle_or_copied': {'prepare': 'preparing', + 'delete': 'end', + 'delete_force': 'end'}, + 'preparing': {'flush_failed': 'stopped', + 'wait': 'prepared'}, + 'end': None, + 'stopped': {'prepare': 'preparing', + 'delete_force': 'end'}, + 'prepared': {'stop': 'stopped', + 'start': 'copying'}, + 'copying': {'wait': 'idle_or_copied', + 'stop': 'stopping'}, + # Assume the worst case where stopping->stopped + # rather than stopping idle_or_copied + 'stopping': {'wait': 'stopped'}, + } + + def _state_transition(self, function, fcmap): + if (function == 'wait' and + 'wait' not in self._transitions[fcmap['status']]): + return ('', '') + + if fcmap['status'] == 'copying' and function == 'wait': + if fcmap['copyrate'] != '0': + if fcmap['progress'] == '0': + fcmap['progress'] = '50' + else: + fcmap['progress'] = '100' + fcmap['status'] = 'idle_or_copied' + return ('', '') + else: + try: + curr_state = fcmap['status'] + fcmap['status'] = self._transitions[curr_state][function] + return ('', '') + except Exception: + return self._errors['CMMVC5903E'] # Find an unused ID def _find_unused_id(self, d): ids = [] for k, v in d.iteritems(): - ids.append(int(v["id"])) + ids.append(int(v['id'])) ids.sort() for index, n in enumerate(ids): if n > index: @@ -116,60 +177,58 @@ class StorwizeSVCManagementSimulator: # 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 + if re.match("^[a-zA-Z_][\w ._-]*$", name): + return False + return True # Convert argument string to dictionary def _cmd_to_dict(self, cmd): arg_list = cmd.split() no_param_args = [ - "autodelete", - "autoexpand", - "bytes", - "compressed", - "force", - "nohdr", + 'autodelete', + 'autoexpand', + 'bytes', + 'compressed', + 'force', + 'nohdr', ] one_param_args = [ - "chapsecret", - "cleanrate", - "delim", - "filtervalue", - "grainsize", - "host", - "iogrp", - "iscsiname", - "mdiskgrp", - "name", - "rsize", - "scsi", - "size", - "source", - "target", - "unit", - "easytier", - "warning", + 'chapsecret', + 'cleanrate', + 'copyrate', + 'delim', + 'filtervalue', + 'grainsize', + 'hbawwpn', + 'host', + 'iogrp', + 'iscsiname', + 'mdiskgrp', + 'name', + 'rsize', + 'scsi', + 'size', + 'source', + 'target', + 'unit', + 'easytier', + 'warning', ] # Handle the special case of lsnode which is a two-word command # Use the one word version of the command internally - if arg_list[0] == "svcinfo" and arg_list[1] == "lsnode": - ret = {"cmd": "lsnodecanister"} + if arg_list[0] == 'svcinfo' and arg_list[1] == 'lsnode': + ret = {'cmd': 'lsnodecanister'} arg_list.pop(0) else: - ret = {"cmd": arg_list[0]} + ret = {'cmd': arg_list[0]} skip = False for i in range(1, len(arg_list)): if skip: skip = False continue - if arg_list[i][0] == "-": + if arg_list[i][0] == '-': if arg_list[i][1:] in no_param_args: ret[arg_list[i][1:]] = True elif arg_list[i][1:] in one_param_args: @@ -179,740 +238,958 @@ class StorwizeSVCManagementSimulator: raise exception.InvalidInput( reason=_('unrecognized argument %s') % arg_list[i]) else: - ret["obj"] = arg_list[i] + ret['obj'] = arg_list[i] return ret - # Generic function for printing information - def _print_info_cmd(self, rows, delim=" ", nohdr=False, **kwargs): + def _print_info_cmd(self, rows, delim=' ', nohdr=False, **kwargs): + """Generic function for printing information.""" if nohdr: del rows[0] for index in range(len(rows)): rows[index] = delim.join(rows[index]) - return ("%s" % "\n".join(rows), "") + return ('%s' % '\n'.join(rows), '') + + def _print_info_obj_cmd(self, header, row, delim=' ', nohdr=False): + """Generic function for printing information for a specific object.""" + objrows = [] + for idx, val in enumerate(header): + objrows.append([val, row[idx]]) + + if nohdr: + for index in range(len(objrows)): + objrows[index] = ' '.join(objrows[index][1:]) + for index in range(len(objrows)): + objrows[index] = delim.join(objrows[index]) + return ('%s' % '\n'.join(objrows), '') + + def _convert_bytes_units(self, bytestr): + num = int(bytestr) + unit_array = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + unit_index = 0 + + while num > 1024: + num = num / 1024 + unit_index += 1 + + return '%d%s' % (num, unit_array[unit_index]) + + def _convert_units_bytes(self, num, unit): + unit_array = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + unit_index = 0 + + while unit.lower() != unit_array[unit_index].lower(): + num = num * 1024 + unit_index += 1 + + return str(num) + + def _cmd_lslicense(self, **kwargs): + rows = [None] * 3 + rows[0] = ['used_compression_capacity', '0.08'] + rows[1] = ['license_compression_capacity', '0'] + if self._next_cmd_error['lslicense'] == 'no_compression': + self._next_cmd_error['lslicense'] = '' + rows[2] = ['license_compression_enclosures', '0'] + else: + rows[2] = ['license_compression_enclosures', '1'] + return self._print_info_cmd(rows=rows, **kwargs) # Print mostly made-up stuff in the correct syntax + def _cmd_lssystem(self, **kwargs): + rows = [None] * 2 + rows[0] = ['id', '0123456789ABCDEF'] + rows[1] = ['name', 'storwize-svc-sim'] + return self._print_info_cmd(rows=rows, **kwargs) + + # Print mostly made-up stuff in the correct syntax, assume -bytes passed def _cmd_lsmdiskgrp(self, **kwargs): rows = [None] * 3 - 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] = ["1", 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"] - rows[2] = ["2", "volpool2", "online", - "1", "0", "3.25TB", "256", - "3.21TB", "1.54TB", "264.97MB", "35.58GB", "47", "80", - "auto", "inactive"] - return self._print_info_cmd(rows=rows, **kwargs) + 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] = ['1', self._flags['storwize_svc_volpool_name'], 'online', + '1', str(len(self._volumes_list)), '3573412790272', + '256', '3529926246400', '1693247906775', '277841182', + '38203734097', '47', '80', 'auto', 'inactive'] + rows[2] = ['2', 'volpool2', 'online', + '1', '0', '3573412790272', '256', + '3529432325160', '1693247906775', '277841182', + '38203734097', '47', '80', 'auto', 'inactive'] + if 'obj' not in kwargs: + return self._print_info_cmd(rows=rows, **kwargs) + else: + if kwargs['obj'] == self._flags['storwize_svc_volpool_name']: + row = rows[1] + elif kwargs['obj'] == 'volpool2': + row = rows[2] + else: + return self._errors['CMMVC5754E'] + + objrows = [] + for idx, val in enumerate(rows[0]): + objrows.append([val, row[idx]]) + + if 'nohdr' in kwargs: + for index in range(len(objrows)): + objrows[index] = ' '.join(objrows[index][1:]) + + if 'delim' in kwargs: + for index in range(len(objrows)): + objrows[index] = kwargs['delim'].join(objrows[index]) + + return ('%s' % '\n'.join(objrows), '') # Print mostly made-up stuff in the correct syntax def _cmd_lsnodecanister(self, **kwargs): 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"] - - if self._next_cmd_error["lsnodecanister"] == "header_mismatch": + 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] = ['1', 'node1', '', '123456789ABCDEF0', 'online', '0', + 'io_grp0', + 'yes', '123456789ABCDEF0', '100', + 'iqn.1982-01.com.ibm:1234.sim.node1', '', '01-1', '1', '1', + '0123ABC'] + rows[2] = ['2', 'node2', '', '123456789ABCDEF1', 'online', '0', + 'io_grp0', + 'no', '123456789ABCDEF1', '100', + 'iqn.1982-01.com.ibm:1234.sim.node2', '', '01-2', '1', '2', + '0123ABC'] + + if self._next_cmd_error['lsnodecanister'] == 'header_mismatch': rows[0].pop(2) - self._next_cmd_error["lsnodecanister"] = "" - if self._next_cmd_error["lsnodecanister"] == "remove_field": + self._next_cmd_error['lsnodecanister'] = '' + if self._next_cmd_error['lsnodecanister'] == 'remove_field': for row in rows: row.pop(0) - self._next_cmd_error["lsnodecanister"] = "" + self._next_cmd_error['lsnodecanister'] = '' return self._print_info_cmd(rows=rows, **kwargs) # Print mostly made-up stuff in the correct syntax def _cmd_lsportip(self, **kwargs): - if self._next_cmd_error["lsportip"] == "ip_no_config": - self._next_cmd_error["lsportip"] = "" - ip_addr1 = "" - ip_addr2 = "" - gw = "" + if self._next_cmd_error['lsportip'] == 'ip_no_config': + self._next_cmd_error['lsportip'] = '' + ip_addr1 = '' + ip_addr2 = '' + gw = '' else: - ip_addr1 = "1.234.56.78" - ip_addr2 = "1.234.56.79" - gw = "1.234.56.1" + 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"] - - if self._next_cmd_error["lsportip"] == "header_mismatch": + 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', '1', 'node1', ip_addr1, '255.255.255.0', + gw, '', '', '', '01:23:45:67:89:00', 'Full', + 'online', '1Gb/s', 'no'] + rows[2] = ['1', '1', 'node1', '', '', '', '', '', '', + '01:23:45:67:89:00', 'Full', 'online', '1Gb/s', 'yes'] + rows[3] = ['2', '1', 'node1', '', '', '', '', '', '', + '01:23:45:67:89:01', 'Full', 'unconfigured', '1Gb/s', 'no'] + rows[4] = ['2', '1', 'node1', '', '', '', '', '', '', + '01:23:45:67:89:01', 'Full', 'unconfigured', '1Gb/s', 'yes'] + rows[5] = ['3', '1', 'node1', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'no'] + rows[6] = ['3', '1', 'node1', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'yes'] + rows[7] = ['4', '1', 'node1', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'no'] + rows[8] = ['4', '1', 'node1', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'yes'] + rows[9] = ['1', '2', 'node2', ip_addr2, '255.255.255.0', + gw, '', '', '', '01:23:45:67:89:02', 'Full', + 'online', '1Gb/s', 'no'] + rows[10] = ['1', '2', 'node2', '', '', '', '', '', '', + '01:23:45:67:89:02', 'Full', 'online', '1Gb/s', 'yes'] + rows[11] = ['2', '2', 'node2', '', '', '', '', '', '', + '01:23:45:67:89:03', 'Full', 'unconfigured', '1Gb/s', 'no'] + rows[12] = ['2', '2', 'node2', '', '', '', '', '', '', + '01:23:45:67:89:03', 'Full', 'unconfigured', '1Gb/s', + 'yes'] + rows[13] = ['3', '2', 'node2', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'no'] + rows[14] = ['3', '2', 'node2', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'yes'] + rows[15] = ['4', '2', 'node2', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'no'] + rows[16] = ['4', '2', 'node2', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'yes'] + + if self._next_cmd_error['lsportip'] == 'header_mismatch': rows[0].pop(2) - self._next_cmd_error["lsportip"] = "" - if self._next_cmd_error["lsportip"] == "remove_field": + self._next_cmd_error['lsportip'] = '' + if self._next_cmd_error['lsportip'] == 'remove_field': for row in rows: row.pop(1) - self._next_cmd_error["lsportip"] = "" + self._next_cmd_error['lsportip'] = '' + + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lsportfc(self, **kwargs): + if self._next_cmd_error['lsportfc'] == 'fc_no_config': + self._next_cmd_error['lsportfc'] = '' + wwpn1 = '' + wwpn2 = '' + else: + wwpn1 = '123456789ABCDEF0' + wwpn2 = '123456789ABCDEF1' + + rows = [None] * 9 + rows[0] = ['id', 'fc_io_port_id', 'port_id', 'type', 'port_speed', + 'node_id', 'node_name', 'WWPN', 'nportid', 'status'] + rows[1] = ['0', '1', '1', 'fc', '4Gb', '1', 'node1', + wwpn1, '012ABC', 'active'] + rows[2] = ['1', '2', '2', 'fc', '4Gb', '1', 'node1', + wwpn1, '012ABC', 'active'] + rows[3] = ['2', '3', '3', 'fc', 'N/A', '1', 'node1', + wwpn1, '000000', 'inactive_unconfigured'] + rows[4] = ['3', '4', '4', 'fc', '4Gb', '1', 'node1', + wwpn1, 'ABCDEF', 'active'] + rows[5] = ['6', '1', '1', 'fc', '4Gb', '2', 'node2', + wwpn2, '012ABC', 'active'] + rows[6] = ['7', '2', '2', 'fc', '4Gb', '2', 'node2', + wwpn2, '012ABC', 'active'] + rows[7] = ['8', '3', '3', 'fc', '4Gb', '2', 'node2', + wwpn2, 'ABC123', 'active'] + rows[8] = ['9', '4', '4', 'fc', '4Gb', '2', 'node2', + wwpn2, '012ABC', 'active'] + + if self._next_cmd_error['lsportfc'] == 'header_mismatch': + rows[0].pop(2) + self._next_cmd_error['lsportfc'] = '' + if self._next_cmd_error['lsportfc'] == 'remove_field': + for row in rows: + row.pop(7) + self._next_cmd_error['lsportfc'] = '' return self._print_info_cmd(rows=rows, **kwargs) + def _cmd_lsfabric(self, **kwargs): + host_name = kwargs['host'] if 'host' in kwargs else None + host_infos = [] + + for hk, hv in self._hosts_list.iteritems(): + if not host_name or hv['host_name'] == host_name: + for mk, mv in self._mappings_list.iteritems(): + if mv['host'] == hv['host_name']: + host_infos.append(hv) + break + + if not len(host_infos): + return ('', '') + + rows = [] + rows.append(['remote_wwpn', 'remote_nportid', 'id', 'node_name', + 'local_wwpn', 'local_port', 'local_nportid', 'state', + 'name', 'cluster_name', 'type']) + for host_info in host_infos: + for wwpn in host_info['wwpns']: + rows.append([wwpn, '123456', host_info['id'], 'nodeN', + 'AABBCCDDEEFF0011', '1', '0123ABC', 'active', + host_info['host_name'], '', 'host']) + + if self._next_cmd_error['lsfabric'] == 'header_mismatch': + rows[0].pop(2) + self._next_cmd_error['lsfabric'] = '' + if self._next_cmd_error['lsfabric'] == 'remove_field': + for row in rows: + row.pop(4) + self._next_cmd_error['lsfabric'] = '' + return self._print_info_cmd(rows=rows, **kwargs) + # Create a vdisk def _cmd_mkvdisk(self, **kwargs): # 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"] + volume_info['id'] = self._find_unused_id(self._volumes_list) + volume_info['uid'] = ('ABCDEF' * 3) + ('0' * 14) + volume_info['id'] - if "name" in kwargs: - volume_info["name"] = kwargs["name"].strip('\'\"') + if 'name' in kwargs: + volume_info['name'] = kwargs['name'].strip('\'\'') else: - volume_info["name"] = "vdisk" + volume_info["id"] + volume_info['name'] = 'vdisk' + volume_info['id'] # Assume size and unit are given, store it in bytes - capacity = int(kwargs["size"]) - unit = kwargs["unit"] - - if unit == "b": - cap_bytes = capacity - elif unit == "kb": - cap_bytes = capacity * pow(1024, 1) - elif unit == "mb": - cap_bytes = capacity * pow(1024, 2) - elif unit == "gb": - cap_bytes = capacity * pow(1024, 3) - elif unit == "tb": - cap_bytes = capacity * pow(1024, 4) - elif unit == "pb": - cap_bytes = capacity * pow(1024, 5) - volume_info["cap_bytes"] = str(cap_bytes) - volume_info["capacity"] = str(capacity) + unit.upper() - - if "easytier" in kwargs: - if kwargs["easytier"] == "on": - volume_info["easy_tier"] = "on" + capacity = int(kwargs['size']) + unit = kwargs['unit'] + volume_info['capacity'] = self._convert_units_bytes(capacity, unit) + + if 'easytier' in kwargs: + if kwargs['easytier'] == 'on': + volume_info['easy_tier'] = 'on' else: - volume_info["easy_tier"] = "off" + volume_info['easy_tier'] = 'off' - if "rsize" in kwargs: + if 'rsize' in kwargs: # Fake numbers - volume_info["used_capacity"] = "0.75MB" - volume_info["real_capacity"] = "36.98MB" - volume_info["free_capacity"] = "36.23MB" - volume_info["used_capacity_bytes"] = "786432" - volume_info["real_capacity_bytes"] = "38776340" - volume_info["free_capacity_bytes"] = "37989908" - if "warning" in kwargs: - volume_info["warning"] = kwargs["warning"].rstrip('%') + volume_info['used_capacity'] = '786432' + volume_info['real_capacity'] = '21474816' + volume_info['free_capacity'] = '38219264' + if 'warning' in kwargs: + volume_info['warning'] = kwargs['warning'].rstrip('%') else: - volume_info["warning"] = "80" - if "autoexpand" in kwargs: - volume_info["autoexpand"] = "on" + volume_info['warning'] = '80' + if 'autoexpand' in kwargs: + volume_info['autoexpand'] = 'on' else: - volume_info["autoexpand"] = "off" - if "grainsize" in kwargs: - volume_info["grainsize"] = kwargs["grainsize"] + volume_info['autoexpand'] = 'off' + if 'grainsize' in kwargs: + volume_info['grainsize'] = kwargs['grainsize'] else: - volume_info["grainsize"] = "32" - if "compressed" in kwargs: - if self._next_cmd_error["mkvdisk"] == "no_compression": - self._next_cmd_error["mkvdisk"] = "" - return self._errors["CMMVC7050E"] - volume_info["compressed_copy"] = "yes" + volume_info['grainsize'] = '32' + if 'compressed' in kwargs: + volume_info['compressed_copy'] = 'yes' else: - volume_info["compressed_copy"] = "no" + volume_info['compressed_copy'] = 'no' else: - volume_info["used_capacity"] = volume_info["capacity"] - volume_info["real_capacity"] = volume_info["capacity"] - volume_info["free_capacity"] = "0.00MB" - volume_info["used_capacity_bytes"] = volume_info["cap_bytes"] - volume_info["real_capacity_bytes"] = volume_info["cap_bytes"] - volume_info["free_capacity_bytes"] = "0" - volume_info["warning"] = "" - volume_info["autoexpand"] = "" - volume_info["grainsize"] = "" - volume_info["compressed_copy"] = "no" - - if volume_info["name"] in self._volumes_list: - return self._errors["CMMVC6035E"] + volume_info['used_capacity'] = volume_info['capacity'] + volume_info['real_capacity'] = volume_info['capacity'] + volume_info['free_capacity'] = '0' + volume_info['warning'] = '' + volume_info['autoexpand'] = '' + volume_info['grainsize'] = '' + volume_info['compressed_copy'] = 'no' + + 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"]), "") + 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, **kwargs): - force = 0 - if "force" in kwargs: - force = 1 + force = True if 'force' in kwargs else False - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - vol_name = kwargs["obj"].strip('\'\"') + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol_name = kwargs['obj'].strip('\'\'') - if vol_name not in self._volumes_list: - return self._errors["CMMVC5753E"] + if not vol_name in self._volumes_list: + return self._errors['CMMVC5753E'] - if force == 0: + if not force: for k, mapping in self._mappings_list.iteritems(): - if mapping["vol"] == vol_name: - return self._errors["CMMVC5840E"] + 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"] + if ((fcmap['source'] == vol_name) or + (fcmap['target'] == vol_name)): + return self._errors['CMMVC5840E'] del self._volumes_list[vol_name] - return ("", "") + return ('', '') def _get_fcmap_info(self, vol_name): ret_vals = { - "fc_id": "", - "fc_name": "", - "fc_map_count": "0", + '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" + 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, **kwargs): - if "obj" not in kwargs: - 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 (("filtervalue" not in kwargs) or - (kwargs["filtervalue"] == "name=" + vol["name"])): - fcmap_info = self._get_fcmap_info(vol["name"]) - - if "bytes" in kwargs: - cap = vol["cap_bytes"] - else: - cap = vol["capacity"] - rows.append([str(vol["id"]), vol["name"], "0", "io_grp0", - "online", "0", - self._flags["storwize_svc_volpool_name"], - cap, "striped", - fcmap_info["fc_id"], fcmap_info["fc_name"], - "", "", vol["uid"], - fcmap_info["fc_map_count"], "1", "empty", - "1", "no"]) - + 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 (('filtervalue' not in kwargs) or + (kwargs['filtervalue'] == 'name=' + vol['name'])): + fcmap_info = self._get_fcmap_info(vol['name']) + + if 'bytes' in kwargs: + cap = self._convert_bytes_units(vol['capacity']) + else: + cap = vol['capacity'] + rows.append([str(vol['id']), vol['name'], '0', 'io_grp0', + 'online', '0', + self._flags['storwize_svc_volpool_name'], + cap, 'striped', + fcmap_info['fc_id'], fcmap_info['fc_name'], + '', '', vol['uid'], + fcmap_info['fc_map_count'], '1', 'empty', + '1', 'no']) + + if 'obj' not in kwargs: return self._print_info_cmd(rows=rows, **kwargs) - else: - if kwargs["obj"] not in self._volumes_list: - return self._errors["CMMVC5754E"] - vol = self._volumes_list[kwargs["obj"]] - fcmap_info = self._get_fcmap_info(vol["name"]) - if "bytes" in kwargs: - cap = vol["cap_bytes"] - cap_u = vol["used_capacity_bytes"] - cap_r = vol["real_capacity_bytes"] - cap_f = vol["free_capacity_bytes"] - else: - cap = vol["capacity"] - cap_u = vol["used_capacity"] - cap_r = vol["real_capacity"] - cap_f = vol["free_capacity"] + if kwargs['obj'] not in self._volumes_list: + return self._errors['CMMVC5754E'] + vol = self._volumes_list[kwargs['obj']] + fcmap_info = self._get_fcmap_info(vol['name']) + cap = vol['capacity'] + cap_u = vol['used_capacity'] + cap_r = vol['real_capacity'] + cap_f = vol['free_capacity'] + if 'bytes' not in kwargs: + for item in [cap, cap_u, cap_r, cap_f]: + item = self._convert_bytes_units(item) 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(['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", cap]) - 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"]) - - if self._next_cmd_error["lsvdisk"] == "blank_pref_node": - rows.append(["preferred_node_id", ""]) - self._next_cmd_error["lsvdisk"] = "" - elif self._next_cmd_error["lsvdisk"] == "no_pref_node": - self._next_cmd_error["lsvdisk"] = "" + 'mdisk_grp_name', + self._flags['storwize_svc_volpool_name']]) + rows.append(['capacity', cap]) + 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']) + + if self._next_cmd_error['lsvdisk'] == 'blank_pref_node': + rows.append(['preferred_node_id', '']) + self._next_cmd_error['lsvdisk'] = '' + elif self._next_cmd_error['lsvdisk'] == 'no_pref_node': + self._next_cmd_error['lsvdisk'] = '' else: - rows.append(["preferred_node_id", "6"]) - 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"]) - rows.append(["used_capacity", cap_u]) - rows.append(["real_capacity", cap_r]) - rows.append(["free_capacity", cap_f]) - rows.append(["autoexpand", vol["autoexpand"]]) - rows.append(["warning", vol["warning"]]) - rows.append(["grainsize", vol["grainsize"]]) - rows.append(["easy_tier", vol["easy_tier"]]) - rows.append(["compressed_copy", vol["compressed_copy"]]) - - if "nohdr" in kwargs: + rows.append(['preferred_node_id', '1']) + 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']) + rows.append(['used_capacity', cap_u]) + rows.append(['real_capacity', cap_r]) + rows.append(['free_capacity', cap_f]) + rows.append(['autoexpand', vol['autoexpand']]) + rows.append(['warning', vol['warning']]) + rows.append(['grainsize', vol['grainsize']]) + rows.append(['easy_tier', vol['easy_tier']]) + rows.append(['compressed_copy', vol['compressed_copy']]) + + if 'nohdr' in kwargs: for index in range(len(rows)): - rows[index] = " ".join(rows[index][1:]) + rows[index] = ' '.join(rows[index][1:]) - if "delim" in kwargs: + if 'delim' in kwargs: for index in range(len(rows)): - rows[index] = kwargs["delim"].join(rows[index]) + rows[index] = kwargs['delim'].join(rows[index]) + + return ('%s' % '\n'.join(rows), '') + + def _add_port_to_host(self, host_info, **kwargs): + if 'iscsiname' in kwargs: + added_key = 'iscsi_names' + added_val = kwargs['iscsiname'].strip('\'\"') + elif 'hbawwpn' in kwargs: + added_key = 'wwpns' + added_val = kwargs['hbawwpn'].strip('\'\"') + else: + return self._errors['CMMVC5707E'] - return ("%s" % "\n".join(rows), "") + host_info[added_key].append(added_val) + + for k, v in self._hosts_list.iteritems(): + if v['id'] == host_info['id']: + continue + for port in v[added_key]: + if port == added_val: + return self._errors['CMMVC6581E'] + return ('', '') # Make a host def _cmd_mkhost(self, **kwargs): host_info = {} - host_info["id"] = self._find_unused_id(self._hosts_list) + host_info['id'] = self._find_unused_id(self._hosts_list) - if "name" in kwargs: - host_name = kwargs["name"].strip('\'\"') + if 'name' in kwargs: + host_name = kwargs['name'].strip('\'\"') else: - host_name = "host" + str(host_info["id"]) - host_info["host_name"] = host_name - - if "iscsiname" not in kwargs: - return self._errors["CMMVC5707E"] - host_info["iscsi_name"] = kwargs["iscsiname"].strip('\'\"') + host_name = 'host' + str(host_info['id']) if self._is_invalid_name(host_name): - return self._errors["CMMVC6527E"] + return self._errors['CMMVC6527E'] if host_name in self._hosts_list: - return self._errors["CMMVC6035E"] + return self._errors['CMMVC6035E'] - for k, v in self._hosts_list.iteritems(): - if v["iscsi_name"] == host_info["iscsi_name"]: - return self._errors["CMMVC6581E"] + host_info['host_name'] = host_name + host_info['iscsi_names'] = [] + host_info['wwpns'] = [] + + out, err = self._add_port_to_host(host_info, **kwargs) + if not len(err): + self._hosts_list[host_name] = host_info + return ('Host, id [%s], successfully created' % + (host_info['id']), '') + else: + return (out, err) - self._hosts_list[host_name] = host_info - return ("Host, id [%s], successfully created" % - (host_info["id"]), "") + # Add ports to an existing host + def _cmd_addhostport(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + host_name = kwargs['obj'].strip('\'\'') + + if host_name not in self._hosts_list: + return self._errors['CMMVC5753E'] + + host_info = self._hosts_list[host_name] + return self._add_port_to_host(host_info, **kwargs) # Change host properties def _cmd_chhost(self, **kwargs): - if "chapsecret" not in kwargs: - return self._errors["CMMVC5707E"] - secret = kwargs["obj"].strip('\'\"') + if 'chapsecret' not in kwargs: + return self._errors['CMMVC5707E'] + secret = kwargs['obj'].strip('\'\'') - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - host_name = kwargs["obj"].strip('\'\"') + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + host_name = kwargs['obj'].strip('\'\'') if host_name not in self._hosts_list: - return self._errors["CMMVC5753E"] + return self._errors['CMMVC5753E'] - self._hosts_list[host_name]["chapsecret"] = secret - return ("", "") + self._hosts_list[host_name]['chapsecret'] = secret + return ('', '') # Remove a host def _cmd_rmhost(self, **kwargs): - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] - host_name = kwargs["obj"].strip('\'\"') + host_name = kwargs['obj'].strip('\'\'') if host_name not in self._hosts_list: - return self._errors["CMMVC5753E"] + return self._errors['CMMVC5753E'] for k, v in self._mappings_list.iteritems(): - if (v["host"] == host_name): - return self._errors["CMMVC5871E"] + if (v['host'] == host_name): + return self._errors['CMMVC5871E'] del self._hosts_list[host_name] - return ("", "") + return ('', '') # List information about hosts def _cmd_lshost(self, **kwargs): - if "obj" not in kwargs: + if 'obj' not in kwargs: rows = [] - rows.append(["id", "name", "port_count", "iogrp_count", "status"]) + rows.append(['id', 'name', 'port_count', 'iogrp_count', 'status']) found = False for k, host in self._hosts_list.iteritems(): - filterstr = "name=" + host["host_name"] - if (("filtervalue" not in kwargs) or - (kwargs["filtervalue"] == filterstr)): - rows.append([host["id"], host["host_name"], "1", "4", - "offline"]) + filterstr = 'name=' + host['host_name'] + if (('filtervalue' not in kwargs) or + (kwargs['filtervalue'] == filterstr)): + rows.append([host['id'], host['host_name'], '1', '4', + 'offline']) found = True if found: return self._print_info_cmd(rows=rows, **kwargs) else: - return ("", "") + return ('', '') else: - if kwargs["obj"] not in self._hosts_list: - return self._errors["CMMVC5754E"] - host = self._hosts_list[kwargs["obj"]] + if kwargs['obj'] not in self._hosts_list: + return self._errors['CMMVC5754E'] + host = self._hosts_list[kwargs['obj']] 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 "nohdr" in kwargs: + 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', 'online']) + for port in host['iscsi_names']: + rows.append(['iscsi_name', port]) + rows.append(['node_logged_in_count', '0']) + rows.append(['state', 'offline']) + for port in host['wwpns']: + rows.append(['WWPN', port]) + rows.append(['node_logged_in_count', '0']) + rows.append(['state', 'active']) + + if 'nohdr' in kwargs: for index in range(len(rows)): - rows[index] = " ".join(rows[index][1:]) + rows[index] = ' '.join(rows[index][1:]) - if "delim" in kwargs: + if 'delim' in kwargs: for index in range(len(rows)): - rows[index] = kwargs["delim"].join(rows[index]) + rows[index] = kwargs['delim'].join(rows[index]) - return ("%s" % "\n".join(rows), "") + return ('%s' % '\n'.join(rows), '') # List iSCSI authorization information about hosts def _cmd_lsiscsiauth(self, **kwargs): + if self._next_cmd_error['lsiscsiauth'] == 'no_info': + self._next_cmd_error['lsiscsiauth'] = '' + return ('', '') rows = [] - rows.append(["type", "id", "name", "iscsi_auth_method", - "iscsi_chap_secret"]) + rows.append(['type', 'id', 'name', 'iscsi_auth_method', + 'iscsi_chap_secret']) for k, host in self._hosts_list.iteritems(): - method = "none" - secret = "" - if "chapsecret" in host: - method = "chap" - secret = host["chapsecret"] - rows.append(["host", host["id"], host["host_name"], method, + method = 'none' + secret = '' + if 'chapsecret' in host: + method = 'chap' + secret = host['chapsecret'] + rows.append(['host', host['id'], host['host_name'], method, secret]) return self._print_info_cmd(rows=rows, **kwargs) # Create a vdisk-host mapping def _cmd_mkvdiskhostmap(self, **kwargs): mapping_info = {} - mapping_info["id"] = self._find_unused_id(self._mappings_list) + mapping_info['id'] = self._find_unused_id(self._mappings_list) - if "host" not in kwargs: - return self._errors["CMMVC5707E"] - mapping_info["host"] = kwargs["host"].strip('\'\"') + if 'host' not in kwargs: + return self._errors['CMMVC5707E'] + mapping_info['host'] = kwargs['host'].strip('\'\'') - if "scsi" not in kwargs: - return self._errors["CMMVC5707E"] - mapping_info["lun"] = kwargs["scsi"].strip('\'\"') + if 'scsi' not in kwargs: + return self._errors['CMMVC5707E'] + mapping_info['lun'] = kwargs['scsi'].strip('\'\'') - if "obj" not in kwargs: - return self._errors["CMMVC5707E"] - mapping_info["vol"] = kwargs["obj"].strip('\'\"') + if 'obj' not in kwargs: + return self._errors['CMMVC5707E'] + mapping_info['vol'] = kwargs['obj'].strip('\'\'') - if mapping_info["vol"] not in self._volumes_list: - return self._errors["CMMVC5753E"] + if not mapping_info['vol'] in self._volumes_list: + return self._errors['CMMVC5753E'] - if mapping_info["host"] not in self._hosts_list: - return self._errors["CMMVC5754E"] + 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"] + 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"] + 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"]), "") + 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, **kwargs): - if "host" not in kwargs: - return self._errors["CMMVC5707E"] - host = kwargs["host"].strip('\'\"') + if 'host' not in kwargs: + return self._errors['CMMVC5707E'] + host = kwargs['host'].strip('\'\'') - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - vol = kwargs["obj"].strip('\'\"') + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol = kwargs['obj'].strip('\'\'') - if vol not in self._mappings_list: - return self._errors["CMMVC5753E"] + if not vol in self._mappings_list: + return self._errors['CMMVC5753E'] - if self._mappings_list[vol]["host"] != host: - return self._errors["CMMVC5753E"] + if self._mappings_list[vol]['host'] != host: + return self._errors['CMMVC5753E'] del self._mappings_list[vol] - return ("", "") + return ('', '') # List information about vdisk-host mappings def _cmd_lshostvdiskmap(self, **kwargs): index = 1 no_hdr = 0 - delimeter = "" - host_name = kwargs["obj"] + delimeter = '' + host_name = kwargs['obj'] if host_name not in self._hosts_list: - return self._errors["CMMVC5754E"] + return self._errors['CMMVC5754E'] rows = [] - rows.append(["id", "name", "SCSI_id", "vdisk_id", "vdisk_name", - "vdisk_UID"]) + 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"]]) + 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(rows=rows, **kwargs) # Create a FlashCopy mapping def _cmd_mkfcmap(self, **kwargs): - source = "" - target = "" - - if "source" not in kwargs: - return self._errors["CMMVC5707E"] - source = kwargs["source"].strip('\'\"') - if source not in self._volumes_list: - return self._errors["CMMVC5754E"] - - if "target" not in kwargs: - return self._errors["CMMVC5707E"] - target = kwargs["target"].strip('\'\"') - if target not in self._volumes_list: - return self._errors["CMMVC5754E"] + source = '' + target = '' + copyrate = kwargs['copyrate'] if 'copyrate' in kwargs else '50' + + if 'source' not in kwargs: + return self._errors['CMMVC5707E'] + source = kwargs['source'].strip('\'\'') + if not source in self._volumes_list: + return self._errors['CMMVC5754E'] + + if 'target' not in kwargs: + return self._errors['CMMVC5707E'] + target = kwargs['target'].strip('\'\'') + if not target in self._volumes_list: + return self._errors['CMMVC5754E'] if source == target: - return self._errors["CMMVC6303E"] + return self._errors['CMMVC6303E'] - if (self._volumes_list[source]["cap_bytes"] != - self._volumes_list[target]["cap_bytes"]): - return self._errors["CMMVC5924E"] + if (self._volumes_list[source]['capacity'] != + self._volumes_list[target]['capacity']): + return self._errors['CMMVC5924E'] 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, mode, **kwargs): - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - id_num = kwargs["obj"] - - if mode == "pre": - if self._next_cmd_error["prestartfcmap"] == "bad_id": - id_num = -1 - self._next_cmd_error["prestartfcmap"] = "" - else: - if self._next_cmd_error["startfcmap"] == "bad_id": - id_num = -1 - self._next_cmd_error["startfcmap"] = "" + 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['copyrate'] = copyrate + fcmap_info['progress'] = '0' + fcmap_info['autodelete'] = True if 'autodelete' in kwargs else False + fcmap_info['status'] = 'idle_or_copied' + self._fcmappings_list[fcmap_info['id']] = fcmap_info + + return('FlashCopy Mapping, id [' + fcmap_info['id'] + + '], successfully created', '') + + def _cmd_gen_prestartfcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + + if self._next_cmd_error['prestartfcmap'] == 'bad_id': + id_num = -1 + self._next_cmd_error['prestartfcmap'] = '' - 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"] - - # Same function used for both stopfcmap and rmfcmap - # Assumes it is called with "-force " - def _cmd_stoprmfcmap(self, mode, **kwargs): - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - id_num = kwargs["obj"] - - if self._next_cmd_error["rmfcmap"] == "bad_id": + try: + fcmap = self._fcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + return self._state_transition('prepare', fcmap) + + def _cmd_gen_startfcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + + if self._next_cmd_error['startfcmap'] == 'bad_id': id_num = -1 - self._next_cmd_error["rmfcmap"] = "" + self._next_cmd_error['startfcmap'] = '' - to_delete = None - found = False - for k, fcmap in self._fcmappings_list.iteritems(): - if fcmap["id"] == id_num: - found = True - if mode == "rm": - to_delete = k + try: + fcmap = self._fcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] - if to_delete: - del self._fcmappings_list[to_delete] + return self._state_transition('start', fcmap) - if found: - return ("", "") - else: - return self._errors["CMMVC5753E"] + def _cmd_stopfcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + + try: + fcmap = self._fcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + return self._state_transition('stop', fcmap) + + def _cmd_rmfcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + force = True if 'force' in kwargs else False + + if self._next_cmd_error['rmfcmap'] == 'bad_id': + id_num = -1 + self._next_cmd_error['rmfcmap'] = '' + + try: + fcmap = self._fcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + function = 'delete_force' if force else 'delete' + ret = self._state_transition(function, fcmap) + if fcmap['status'] == 'end': + del self._fcmappings_list[id_num] + return ret + + def _cmd_lsvdiskfcmappings(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5707E'] + vdisk = kwargs['obj'] + rows = [] + rows.append(['id', 'name']) + for k, v in self._fcmappings_list.iteritems(): + if v['source'] == vdisk or v['target'] == vdisk: + rows.append([v['id'], v['name']]) + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_chfcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5707E'] + id_num = kwargs['obj'] + + try: + fcmap = self._fcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + for key in ['name', 'copyrate', 'autodelete']: + if key in kwargs: + fcmap[key] = kwargs[key] + return ('', '') def _cmd_lsfcmap(self, **kwargs): 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"]) + 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 - filter_key = kwargs["filtervalue"].split("=")[0] - filter_value = kwargs["filtervalue"].split("=")[1] + filter_key = kwargs['filtervalue'].split('=')[0] + filter_value = kwargs['filtervalue'].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"]] - old_status = v["status"] - if old_status == "preparing": - new_status = "prepared" - if self._next_cmd_error["lsfcmap"] == "bogus_prepare": - new_status = "bogus" - elif (old_status == "copying") and (v["progress"] == "0"): - new_status = "copying" - v["progress"] = "50" - elif (old_status == "copying") and (v["progress"] == "50"): - new_status = "idle_or_copied" + source = self._volumes_list[v['source']] + target = self._volumes_list[v['target']] + self._state_transition('wait', v) + + if self._next_cmd_error['lsfcmap'] == 'speed_up': + self._next_cmd_error['lsfcmap'] = '' + curr_state = v['status'] + while self._state_transition('wait', v) == ("", ""): + if curr_state == v['status']: + break + curr_state = v['status'] + + if ((v['status'] == 'idle_or_copied' and v['autodelete'] and + v['progress'] == '100') or (v['status'] == 'end')): to_delete.append(k) else: - new_status = old_status - v["status"] = new_status - - if ((self._next_cmd_error["lsfcmap"] == "speed_up") or - (self._next_cmd_error["lsfcmap"] == "bogus_prepare")): - print_status = new_status - self._next_cmd_error["lsfcmap"] = "" - else: - print_status = old_status - - rows.append([v["id"], v["name"], source["id"], - source["name"], target["id"], target["name"], "", - "", print_status, v["progress"], "50", "100", - "off", "", "", "no", "", "no"]) + rows.append([v['id'], v['name'], source['id'], + source['name'], target['id'], target['name'], + '', '', v['status'], v['progress'], + v['copyrate'], '100', 'off', '', '', 'no', '', + 'no']) for d in to_delete: del self._fcmappings_list[k] return self._print_info_cmd(rows=rows, **kwargs) + # Add host to list + def _add_host_to_list(self, connector): + host_info = {} + host_info['id'] = self._find_unused_id(self._hosts_list) + host_info['host_name'] = connector['host'] + host_info['iscsi_names'] = [] + host_info['wwpns'] = [] + if 'initiator' in connector: + host_info['iscsi_names'].append(connector['initiator']) + if 'wwpns' in connector: + host_info['wwpns'] = host_info['wwpns'] + connector['wwpns'] + self._hosts_list[connector['host']] = host_info + # The main function to run commands on the management simulator def execute_command(self, cmd, check_exit_code=True): try: kwargs = self._cmd_to_dict(cmd) except IndexError: - return self._errors["CMMVC5707E"] + return self._errors['CMMVC5707E'] - command = kwargs["cmd"] - del kwargs["cmd"] + command = kwargs['cmd'] + del kwargs['cmd'] arg_list = cmd.split() - if command == "lsmdiskgrp": + if command == 'lsmdiskgrp': out, err = self._cmd_lsmdiskgrp(**kwargs) - elif command == "lsnodecanister": + elif command == 'lslicense': + out, err = self._cmd_lslicense(**kwargs) + elif command == 'lssystem': + out, err = self._cmd_lssystem(**kwargs) + elif command == 'lsnodecanister': out, err = self._cmd_lsnodecanister(**kwargs) - elif command == "lsportip": + elif command == 'lsportip': out, err = self._cmd_lsportip(**kwargs) - elif command == "mkvdisk": + elif command == 'lsportfc': + out, err = self._cmd_lsportfc(**kwargs) + elif command == 'lsfabric': + out, err = self._cmd_lsfabric(**kwargs) + elif command == 'mkvdisk': out, err = self._cmd_mkvdisk(**kwargs) - elif command == "rmvdisk": + elif command == 'rmvdisk': out, err = self._cmd_rmvdisk(**kwargs) - elif command == "lsvdisk": + elif command == 'lsvdisk': out, err = self._cmd_lsvdisk(**kwargs) - elif command == "mkhost": + elif command == 'mkhost': out, err = self._cmd_mkhost(**kwargs) - elif command == "chhost": + elif command == 'addhostport': + out, err = self._cmd_addhostport(**kwargs) + elif command == 'chhost': out, err = self._cmd_chhost(**kwargs) - elif command == "rmhost": + elif command == 'rmhost': out, err = self._cmd_rmhost(**kwargs) - elif command == "lshost": + elif command == 'lshost': out, err = self._cmd_lshost(**kwargs) - elif command == "lsiscsiauth": + elif command == 'lsiscsiauth': out, err = self._cmd_lsiscsiauth(**kwargs) - elif command == "mkvdiskhostmap": + elif command == 'mkvdiskhostmap': out, err = self._cmd_mkvdiskhostmap(**kwargs) - elif command == "rmvdiskhostmap": + elif command == 'rmvdiskhostmap': out, err = self._cmd_rmvdiskhostmap(**kwargs) - elif command == "lshostvdiskmap": + elif command == 'lshostvdiskmap': out, err = self._cmd_lshostvdiskmap(**kwargs) - elif command == "mkfcmap": + elif command == 'mkfcmap': out, err = self._cmd_mkfcmap(**kwargs) - elif command == "prestartfcmap": - out, err = self._cmd_gen_startfcmap(mode="pre", **kwargs) - elif command == "startfcmap": - out, err = self._cmd_gen_startfcmap(mode="start", **kwargs) - elif command == "stopfcmap": - out, err = self._cmd_stoprmfcmap(mode="stop", **kwargs) - elif command == "rmfcmap": - out, err = self._cmd_stoprmfcmap(mode="rm", **kwargs) - elif command == "lsfcmap": + elif command == 'prestartfcmap': + out, err = self._cmd_gen_prestartfcmap(**kwargs) + elif command == 'startfcmap': + out, err = self._cmd_gen_startfcmap(**kwargs) + elif command == 'stopfcmap': + out, err = self._cmd_stopfcmap(**kwargs) + elif command == 'rmfcmap': + out, err = self._cmd_rmfcmap(**kwargs) + elif command == 'chfcmap': + out, err = self._cmd_chfcmap(**kwargs) + elif command == 'lsfcmap': out, err = self._cmd_lsfcmap(**kwargs) + elif command == 'lsvdiskfcmappings': + out, err = self._cmd_lsvdiskfcmappings(**kwargs) else: - out, err = ("", "ERROR: Unsupported command") + out, err = ('', 'ERROR: Unsupported command') if (check_exit_code) and (len(err) != 0): raise exception.ProcessExecutionError(exit_code=1, @@ -940,8 +1217,8 @@ class StorwizeSVCFakeDriver(storwize_svc.StorwizeSVCDriver): 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}) + LOG.debug(_('CLI output:\n stdout: %(stdout)s\n stderr: ' + '%(stderr)s') % {'stdout': stdout, 'stderr': stderr}) except exception.ProcessExecutionError as e: with excutils.save_and_reraise_exception(): @@ -952,18 +1229,32 @@ class StorwizeSVCFakeDriver(storwize_svc.StorwizeSVCDriver): return ret +class StorwizeSVCFakeSock: + def settimeout(self, time): + return + + class StorwizeSVCDriverTestCase(test.TestCase): def setUp(self): super(StorwizeSVCDriverTestCase, self).setUp() - self.USESIM = 1 - if self.USESIM == 1: - self.flags( - san_ip="hostname", - san_login="user", - san_password="pass", - storwize_svc_flashcopy_timeout="20", - ) - self.sim = StorwizeSVCManagementSimulator("volpool") + self.USESIM = True + if self.USESIM: + self._def_flags = {'san_ip': 'hostname', + 'san_login': 'user', + 'san_password': 'pass', + 'storwize_svc_flashcopy_timeout': 20, + 'storwize_svc_connection_protocol': 'iscsi', + 'storwize_svc_multipath_enabled': False} + self._host_name = 'storwize-svc-test' + self._host_ip = '1.234.56.78' + self._host_wwpns = [ + str(random.randint(0, 9999999999999999)).zfill(16), + str(random.randint(0, 9999999999999999)).zfill(16)] + self._iscsi_name = ('test.initiator.%s' % + str(random.randint(10000, 99999))) + self._reset_flags() + self.sim = StorwizeSVCManagementSimulator('volpool') + configuration = mox.MockObject(conf.Configuration) configuration.san_is_local = False configuration.append_config_values(mox.IgnoreArg()) @@ -971,242 +1262,287 @@ class StorwizeSVCDriverTestCase(test.TestCase): self.driver = StorwizeSVCFakeDriver(configuration=configuration) self.driver.set_fake_storage(self.sim) else: - self.flags( - san_ip="-1.-1.-1.-1", - san_login="user", - san_password="password", - storwize_svc_volpool_name="pool", - ) + self._def_flags = {'san_ip': '1.111.11.11', + 'san_login': 'user', + 'san_password': 'password', + 'storwize_svc_volpool_name': 'openstack', + 'storwize_svc_connection_protocol': 'iscsi', + 'storwize_svc_multipath_enabled': False, + 'ssh_conn_timeout': 0} + self._host_name = socket.gethostname() + self._host_ip = socket.gethostbyname(self._host_name) + + self._host_wwpns = [] + out, err = utils.execute('systool', '-c', 'fc_host', '-v', + run_as_root=True) + lines = out.split('\n') + for line in lines: + val = line.split('=') + if (len(val) == 2 and + val[0].strip().replace(" ", "") == 'port_name'): + self._host_wwpns.append(val[1].strip()[3:-1]) + self.assertNotEqual(len(self._host_wwpns), 0) + + lines = utils.read_file_as_root('/etc/iscsi/initiatorname.iscsi') + for l in lines.split('\n'): + if l.startswith('InitiatorName='): + self._iscsi_name = l[l.index('=') + 1:].strip() + + self._reset_flags() self.driver = storwize_svc.StorwizeSVCDriver() + self.driver.db = StorwizeSVCFakeDB() self.driver.do_setup(None) self.driver.check_for_setup_error() self.stubs.Set(storwize_svc.time, 'sleep', lambda s: None) - def test_storwize_svc_volume_tests(self): - self.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) - - if self.USESIM == 1: - self.flags(storwize_svc_vol_rsize="2%") - self.flags(storwize_svc_vol_compression=True) - self.driver.create_volume(volume) - is_volume_defined = self.driver._is_volume_defined(volume["name"]) - self.assertEqual(is_volume_defined, True) - self.driver.delete_volume(volume) - FLAGS.reset() - - def test_storwize_svc_ip_connectivity(self): - # Check for missing san_ip - self.flags(san_ip=None) - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - - if self.USESIM != 1: - # Check for invalid ip - self.flags(san_ip="-1.-1.-1.-1") - self.assertRaises(socket.gaierror, - self.driver.check_for_setup_error) + def _reset_flags(self): + FLAGS.reset() + self.flags(**self._def_flags) - # Check for unreachable IP - self.flags(san_ip="1.1.1.1") - self.assertRaises(socket.error, - self.driver.check_for_setup_error) + def _assert_vol_exists(self, name, exists): + is_vol_defined = self.driver._is_vdisk_defined(name) + self.assertEqual(is_vol_defined, exists) def test_storwize_svc_connectivity(self): # Make sure we detect if the pool doesn't exist - no_exist_pool = "i-dont-exist-%s" % random.randint(10000, 99999) + no_exist_pool = 'i-dont-exist-%s' % random.randint(10000, 99999) self.flags(storwize_svc_volpool_name=no_exist_pool) self.assertRaises(exception.InvalidInput, - self.driver.check_for_setup_error) - FLAGS.reset() + self.driver.do_setup, None) + self._reset_flags() # Check the case where the user didn't configure IP addresses # as well as receiving unexpected results from the storage - if self.USESIM == 1: - self.sim.error_injection("lsnodecanister", "header_mismatch") + if self.USESIM: + self.sim.error_injection('lsnodecanister', 'header_mismatch') self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.sim.error_injection("lsnodecanister", "remove_field") + self.driver.do_setup, None) + self.sim.error_injection('lsnodecanister', 'remove_field') self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.sim.error_injection("lsportip", "ip_no_config") + self.driver.do_setup, None) + + self.sim.error_injection('lsportip', 'ip_no_config') + self.sim.error_injection('lsportfc', 'fc_no_config') self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.sim.error_injection("lsportip", "header_mismatch") + self.driver.do_setup, None) + self.sim.error_injection('lsportip', 'header_mismatch') self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.sim.error_injection("lsportip", "remove_field") + self.driver.do_setup, None) + self.sim.error_injection('lsportip', 'remove_field') self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) + self.driver.do_setup, None) + self.sim.error_injection('lsportfc', 'header_mismatch') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.do_setup, None) + self.sim.error_injection('lsportfc', 'remove_field') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.do_setup, None) # Check with bad parameters + self.flags(san_ip='') + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() + self.flags(san_password=None) self.flags(san_private_key=None) self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() + self.driver.check_for_setup_error) + self._reset_flags() - self.flags(storwize_svc_vol_rsize="invalid") + self.flags(storwize_svc_vol_rsize=101) self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() + self.driver.check_for_setup_error) + self._reset_flags() - self.flags(storwize_svc_vol_warning="invalid") + self.flags(storwize_svc_vol_warning=101) self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() + self.driver.check_for_setup_error) + self._reset_flags() - self.flags(storwize_svc_vol_autoexpand="invalid") + self.flags(storwize_svc_vol_grainsize=42) self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() + self.driver.check_for_setup_error) + self._reset_flags() - self.flags(storwize_svc_vol_grainsize=str(42)) + self.flags(storwize_svc_flashcopy_timeout=601) self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() + self.driver.check_for_setup_error) + self._reset_flags() + + self.flags(storwize_svc_vol_compression=True) + self.flags(storwize_svc_vol_rsize=-1) + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() - self.flags(storwize_svc_flashcopy_timeout=str(601)) + self.flags(storwize_svc_connection_protocol='foo') self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() + self.driver.check_for_setup_error) + self._reset_flags() - self.flags(storwize_svc_vol_compression=True) - self.flags(storwize_svc_vol_rsize="-1") + self.flags(storwize_svc_connection_protocol='iscsi') + self.flags(storwize_svc_multipath_enabled=True) self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() + self.driver.check_for_setup_error) + self._reset_flags() + + if self.USESIM: + self.sim.error_injection('lslicense', 'no_compression') + self.flags(storwize_svc_vol_compression=True) + self.driver.do_setup(None) + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() # Finally, check with good parameters - self.driver.check_for_setup_error() + self.driver.do_setup(None) - def test_storwize_svc_flashcopy(self): - volume1 = {} - volume1["name"] = "test1_volume%s" % random.randint(10000, 99999) - volume1["size"] = 10 - volume1["id"] = 10 - self.driver.create_volume(volume1) + def _generate_vol_info(self, vol_name, vol_id): + rand_id = str(random.randint(10000, 99999)) + if vol_name: + return {'name': 'snap_volume%s' % rand_id, + 'volume_name': vol_name, + 'id': rand_id, + 'volume_id': vol_id, + 'volume_size': 10} + else: + return {'name': 'test_volume%s' % rand_id, + 'size': 10, + 'id': '%s' % rand_id, + 'volume_type_id': None} + + def _create_test_vol(self, opts): + ctxt = context.get_admin_context() + type_ref = volume_types.create(ctxt, 'testtype', opts) + volume = self._generate_vol_info(None, None) + volume['volume_type_id'] = type_ref['id'] + self.driver.create_volume(volume) - snapshot = {} - snapshot["name"] = "snap_volume%s" % random.randint(10000, 99999) - snapshot["volume_name"] = volume1["name"] + attrs = self.driver._get_vdisk_attributes(volume['name']) + self.driver.delete_volume(volume) + volume_types.destroy(ctxt, type_ref['id']) + return attrs + + def _fail_prepare_fc_map(self, fc_map_id, source, target): + raise exception.ProcessExecutionError(exit_code=1, + stdout='', + stderr='unit-test-fail', + cmd='prestartfcmap id') + + def test_storwize_svc_snapshots(self): + vol1 = self._generate_vol_info(None, None) + self.driver.create_volume(vol1) + self.driver.db.volume_set(vol1) + snap1 = self._generate_vol_info(vol1['name'], vol1['id']) # Test timeout and volume cleanup - self.flags(storwize_svc_flashcopy_timeout=str(1)) + self.flags(storwize_svc_flashcopy_timeout=1) self.assertRaises(exception.InvalidSnapshot, - self.driver.create_snapshot, snapshot) - is_volume_defined = self.driver._is_volume_defined(snapshot["name"]) - self.assertEqual(is_volume_defined, False) - FLAGS.reset() - - # Test bogus statuses - if self.USESIM == 1: - self.sim.error_injection("lsfcmap", "bogus_prepare") - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_snapshot, snapshot) + self.driver.create_snapshot, snap1) + self._assert_vol_exists(snap1['name'], False) + self._reset_flags() # Test prestartfcmap, startfcmap, and rmfcmap failing - if self.USESIM == 1: - self.sim.error_injection("prestartfcmap", "bad_id") - self.assertRaises(exception.ProcessExecutionError, - self.driver.create_snapshot, snapshot) - self.sim.error_injection("lsfcmap", "speed_up") - self.sim.error_injection("startfcmap", "bad_id") + orig = self.driver._call_prepare_fc_map + self.driver._call_prepare_fc_map = self._fail_prepare_fc_map + self.assertRaises(exception.ProcessExecutionError, + self.driver.create_snapshot, snap1) + self.driver._call_prepare_fc_map = orig + + if self.USESIM: + self.sim.error_injection('lsfcmap', 'speed_up') + self.sim.error_injection('startfcmap', 'bad_id') self.assertRaises(exception.ProcessExecutionError, - self.driver.create_snapshot, snapshot) - self.sim.error_injection("prestartfcmap", "bad_id") - self.sim.error_injection("rmfcmap", "bad_id") + self.driver.create_snapshot, snap1) + self._assert_vol_exists(snap1['name'], False) + self.sim.error_injection('prestartfcmap', 'bad_id') self.assertRaises(exception.ProcessExecutionError, - self.driver.create_snapshot, snapshot) + self.driver.create_snapshot, snap1) + self._assert_vol_exists(snap1['name'], False) # Test successful snapshot - self.driver.create_snapshot(snapshot) - - # Ensure snapshot is defined - is_volume_defined = self.driver._is_volume_defined(snapshot["name"]) - self.assertEqual(is_volume_defined, True) + self.driver.create_snapshot(snap1) + self._assert_vol_exists(snap1['name'], True) # Try to create a snapshot from an non-existing volume - should fail - snapshot2 = {} - snapshot2["name"] = "snap_volume%s" % random.randint(10000, 99999) - snapshot2["volume_name"] = "undefined-vol" + snap_novol = self._generate_vol_info('undefined-vol', '12345') self.assertRaises(exception.VolumeNotFound, self.driver.create_snapshot, - snapshot2) - - # Create volume from snapshot - volume2 = {} - volume2["name"] = "snap2vol_volume%s" % random.randint(10000, 99999) - - # Create volume from snapshot into an existsing volume - self.assertRaises(exception.InvalidSnapshot, - self.driver.create_volume_from_snapshot, - volume1, - snapshot) + snap_novol) + + # We support deleting a volume that has snapshots, so delete the volume + # first + self.driver.delete_volume(vol1) + self.driver.delete_snapshot(snap1) + + def test_storwize_svc_create_volfromsnap_clone(self): + vol1 = self._generate_vol_info(None, None) + self.driver.create_volume(vol1) + self.driver.db.volume_set(vol1) + snap1 = self._generate_vol_info(vol1['name'], vol1['id']) + self.driver.create_snapshot(snap1) + vol2 = self._generate_vol_info(None, None) + vol3 = self._generate_vol_info(None, None) # Try to create a volume from a non-existing snapshot + snap_novol = self._generate_vol_info('undefined-vol', '12345') + vol_novol = self._generate_vol_info(None, None) self.assertRaises(exception.SnapshotNotFound, self.driver.create_volume_from_snapshot, - volume2, - snapshot2) + vol_novol, + snap_novol) # Fail the snapshot - if self.USESIM == 1: - self.sim.error_injection("prestartfcmap", "bad_id") - self.assertRaises(exception.ProcessExecutionError, - self.driver.create_volume_from_snapshot, - volume2, - snapshot) + orig = self.driver._call_prepare_fc_map + self.driver._call_prepare_fc_map = self._fail_prepare_fc_map + self.assertRaises(exception.ProcessExecutionError, + self.driver.create_volume_from_snapshot, + vol2, snap1) + self.driver._call_prepare_fc_map = orig + self._assert_vol_exists(vol2['name'], False) + + # Try to create where source size != target size + vol2['size'] += 1 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + vol2, snap1) + self._assert_vol_exists(vol2['name'], False) + vol2['size'] -= 1 # Succeed - if self.USESIM == 1: - self.sim.error_injection("lsfcmap", "speed_up") - self.driver.create_volume_from_snapshot(volume2, snapshot) - - # Ensure volume is defined - is_volume_defined = self.driver._is_volume_defined(volume2["name"]) - self.assertEqual(is_volume_defined, True) - - self.driver._delete_volume(volume2, True) - self.driver._delete_snapshot(snapshot, True) - - # Check with target with different size - volume3 = {} - volume3["name"] = "test3_volume%s" % random.randint(10000, 99999) - volume3["size"] = 11 - volume3["id"] = 11 - self.driver.create_volume(volume3) - snapshot["name"] = volume3["name"] - self.assertRaises(exception.InvalidSnapshot, - self.driver.create_snapshot, - snapshot) - self.driver._delete_volume(volume1, True) - self.driver._delete_volume(volume3, True) - - # Snapshot volume that doesn't exist - snapshot = {} - snapshot["name"] = "snap_volume%s" % random.randint(10000, 99999) - snapshot["volume_name"] = "no_exist" - self.assertRaises(exception.VolumeNotFound, - self.driver.create_snapshot, - snapshot) + if self.USESIM: + self.sim.error_injection('lsfcmap', 'speed_up') + self.driver.create_volume_from_snapshot(vol2, snap1) + self._assert_vol_exists(vol2['name'], True) + + # Try to clone where source size != target size + vol3['size'] += 1 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + vol3, vol2) + self._assert_vol_exists(vol3['name'], False) + vol3['size'] -= 1 + + if self.USESIM: + self.sim.error_injection('lsfcmap', 'speed_up') + self.driver.create_cloned_volume(vol3, vol2) + self._assert_vol_exists(vol3['name'], True) + + # Delete in the 'opposite' order to make sure it works + self.driver.delete_volume(vol3) + self._assert_vol_exists(vol3['name'], False) + self.driver.delete_volume(vol2) + self._assert_vol_exists(vol2['name'], False) + self.driver.delete_snapshot(snap1) + self._assert_vol_exists(snap1['name'], False) + self.driver.delete_volume(vol1) + self._assert_vol_exists(vol1['name'], False) 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 - + volume = self._generate_vol_info(None, None) self.driver.create_volume(volume) self.driver.ensure_export(None, volume) @@ -1216,11 +1552,11 @@ class StorwizeSVCDriverTestCase(test.TestCase): self.driver.remove_export(None, volume) # 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"])) + attributes = self.driver._get_vdisk_attributes(volume['name']) + attr_size = float(attributes['capacity']) / (1024 ** 3) # bytes to GB + self.assertEqual(attr_size, float(volume['size'])) pool = storwize_svc.FLAGS.storwize_svc_volpool_name - self.assertEqual(attributes["mdisk_grp_name"], pool) + self.assertEqual(attributes['mdisk_grp_name'], pool) # Try to create the volume again (should fail) self.assertRaises(exception.ProcessExecutionError, @@ -1228,7 +1564,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): volume) # Try to delete a volume that doesn't exist (should not fail) - vol_no_exist = {"name": "i_dont_exist"} + vol_no_exist = {'name': 'i_dont_exist'} self.driver.delete_volume(vol_no_exist) # Ensure export for volume that doesn't exist (should not fail) self.driver.ensure_export(None, vol_no_exist) @@ -1236,24 +1572,13 @@ class StorwizeSVCDriverTestCase(test.TestCase): # Delete the volume self.driver.delete_volume(volume) - def _create_test_vol(self): - volume = {} - volume["name"] = "testparam_volume%s" % random.randint(10000, 99999) - volume["size"] = 1 - volume["id"] = 1 - self.driver.create_volume(volume) - - attrs = self.driver._get_volume_attributes(volume["name"]) - self.driver.delete_volume(volume) - return attrs - def test_storwize_svc_volume_params(self): # Option test matrix # Option Value Covered by test # # rsize -1 1 - # rsize 2% 2,3 + # rsize 2 2,3 # warning 0 2 - # warning 80% 3 + # warning 80 3 # autoexpand True 2 # autoexpand False 3 # grainsize 32 2 @@ -1263,138 +1588,177 @@ class StorwizeSVCDriverTestCase(test.TestCase): # easytier True 1,3 # easytier False 2 - # Test 1 - self.flags(storwize_svc_vol_rsize="-1") - self.flags(storwize_svc_vol_easytier=True) - attrs = self._create_test_vol() - self.assertEquals(attrs["free_capacity"], "0") - self.assertEquals(attrs["easy_tier"], "on") - FLAGS.reset() - - # Test 2 - self.flags(storwize_svc_vol_rsize="2%") - self.flags(storwize_svc_vol_compression=False) - self.flags(storwize_svc_vol_warning="0") - self.flags(storwize_svc_vol_autoexpand=True) - self.flags(storwize_svc_vol_grainsize="32") - self.flags(storwize_svc_vol_easytier=False) - attrs = self._create_test_vol() - self.assertNotEqual(attrs["capacity"], attrs["real_capacity"]) - self.assertEquals(attrs["compressed_copy"], "no") - self.assertEquals(attrs["warning"], "0") - self.assertEquals(attrs["autoexpand"], "on") - self.assertEquals(attrs["grainsize"], "32") - self.assertEquals(attrs["easy_tier"], "off") - FLAGS.reset() - - # Test 3 - self.flags(storwize_svc_vol_rsize="2%") - self.flags(storwize_svc_vol_compression=False) - self.flags(storwize_svc_vol_warning="80%") - self.flags(storwize_svc_vol_autoexpand=False) - self.flags(storwize_svc_vol_grainsize="256") - self.flags(storwize_svc_vol_easytier=True) - attrs = self._create_test_vol() - self.assertNotEqual(attrs["capacity"], attrs["real_capacity"]) - self.assertEquals(attrs["compressed_copy"], "no") - self.assertEquals(attrs["warning"], "80") - self.assertEquals(attrs["autoexpand"], "off") - self.assertEquals(attrs["grainsize"], "256") - self.assertEquals(attrs["easy_tier"], "on") - FLAGS.reset() - - # Test 4 - self.flags(storwize_svc_vol_rsize="2%") - self.flags(storwize_svc_vol_compression=True) - try: - attrs = self._create_test_vol() - self.assertNotEqual(attrs["capacity"], attrs["real_capacity"]) - self.assertEquals(attrs["compressed_copy"], "yes") - except exception.ProcessExecutionError as e: - if "CMMVC7050E" not in e.stderr: - raise exception.ProcessExecutionError(exit_code=e.exit_code, - stdout=e.stdout, - stderr=e.stderr, - cmd=e.cmd) - if self.USESIM == 1: - self.sim.error_injection("mkvdisk", "no_compression") - self.assertRaises(exception.ProcessExecutionError, - self._create_test_vol) - FLAGS.reset() + opts_list = [] + chck_list = [] + opts_list.append({'rsize': -1, 'easytier': True}) + chck_list.append({'free_capacity': '0', 'easy_tier': 'on'}) + opts_list.append({'rsize': 2, 'compression': False, 'warning': 0, + 'autoexpand': True, 'grainsize': 32, + 'easytier': False}) + chck_list.append({'-free_capacity': '0', 'compressed_copy': 'no', + 'warning': '0', 'autoexpand': 'on', + 'grainsize': '32', 'easy_tier': 'off'}) + opts_list.append({'rsize': 2, 'compression': False, 'warning': 80, + 'autoexpand': False, 'grainsize': 256, + 'easytier': True}) + chck_list.append({'-free_capacity': '0', 'compressed_copy': 'no', + 'warning': '80', 'autoexpand': 'off', + 'grainsize': '256', 'easy_tier': 'on'}) + opts_list.append({'rsize': 2, 'compression': True}) + chck_list.append({'-free_capacity': '0', + 'compressed_copy': 'yes'}) + + for idx in range(len(opts_list)): + attrs = self._create_test_vol(opts_list[idx]) + for k, v in chck_list[idx].iteritems(): + print k + ' ' + v + try: + if k[0] == '-': + k = k[1:] + self.assertNotEqual(attrs[k], v) + else: + self.assertEqual(attrs[k], v) + except exception.ProcessExecutionError as e: + if 'CMMVC7050E' not in e.stderr: + raise e def test_storwize_svc_unicode_host_and_volume_names(self): - volume1 = {} - volume1["name"] = u"unicode1_volume%s" % random.randint(10000, 99999) - volume1["size"] = 2 - volume1["id"] = 1 + # We'll check with iSCSI only - nothing protocol-dependednt here + self.flags(storwize_svc_connection_protocol='iscsi') + self.driver.do_setup(None) + + rand_id = random.randint(10000, 99999) + volume1 = {'name': u'unicode1_volume%s' % rand_id, + 'size': 2, + 'id': 1, + 'volume_type_id': None} self.driver.create_volume(volume1) - # Make sure that the volumes have been created - is_volume_defined = self.driver._is_volume_defined(volume1["name"]) - self.assertEqual(is_volume_defined, True) - conn = {} - conn["initiator"] = u"unicode:init:%s" % random.randint(10000, 99999) - conn["ip"] = "10.10.10.10" # Bogus ip for testing + self._assert_vol_exists(volume1['name'], True) + + self.assertRaises(exception.NoValidHost, + self.driver._connector_to_hostname_prefix, + {'host': 12345}) + + # Add a a host first to make life interesting (this host and + # conn['host'] should be translated to the same prefix, and the + # initiator should differentiate + tmpconn1 = {'initiator': u'unicode:initiator1.%s' % rand_id, + 'ip': '10.10.10.10', + 'host': u'unicode.foo}.bar{.baz-%s' % rand_id} + self.driver._create_host(tmpconn1) + + # Add a host with a different prefix + tmpconn2 = {'initiator': u'unicode:initiator2.%s' % rand_id, + 'ip': '10.10.10.11', + 'host': u'unicode.hello.world-%s' % rand_id} + self.driver._create_host(tmpconn2) + + conn = {'initiator': u'unicode:initiator3.%s' % rand_id, + 'ip': '10.10.10.12', + 'host': u'unicode.foo}.bar}.baz-%s' % rand_id} self.driver.initialize_connection(volume1, conn) + host_name = self.driver._get_host_from_connector(conn) + self.assertNotEqual(host_name, None) self.driver.terminate_connection(volume1, conn) + host_name = self.driver._get_host_from_connector(conn) + self.assertEqual(host_name, None) self.driver.delete_volume(volume1) + # Clean up temporary hosts + for tmpconn in [tmpconn1, tmpconn2]: + host_name = self.driver._get_host_from_connector(tmpconn) + self.assertNotEqual(host_name, None) + self.driver._delete_host(host_name) + 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 + + ctxt = context.get_admin_context() + volume1 = self._generate_vol_info(None, None) self.driver.create_volume(volume1) - volume2 = {} - volume2["name"] = "test2_volume%s" % random.randint(10000, 99999) - volume2["size"] = 2 - volume2["id"] = 1 + volume2 = self._generate_vol_info(None, None) self.driver.create_volume(volume2) - # Check case where no hosts exist - if self.USESIM == 1: - ret = self.driver._get_host_from_iscsiname("foo") - self.assertEquals(ret, None) - ret = self.driver._is_host_defined("foo") - self.assertEquals(ret, False) - - # 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) + # Create volume types that we created + types = {} + for protocol in ['fc', 'iscsi']: + opts = {'protocol': protocol} + types[protocol] = volume_types.create(ctxt, protocol, opts) + + conn = {'initiator': self._iscsi_name, + 'ip': self._host_ip, + 'host': self._host_name, + 'wwpns': self._host_wwpns} + + for protocol in ['fc', 'iscsi']: + volume1['volume_type_id'] = types[protocol]['id'] + volume2['volume_type_id'] = types[protocol]['id'] + + # Check case where no hosts exist + ret = self.driver._get_host_from_connector(conn) + self.assertEqual(ret, None) + + # Make sure that the volumes have been created + self._assert_vol_exists(volume1['name'], True) + self._assert_vol_exists(volume2['name'], True) + + # Check bad output from lsfabric + if protocol == 'fc' and self.USESIM: + for error in ['remove_field', 'header_mismatch']: + self.sim.error_injection('lsfabric', error) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + volume1, conn) + host_name = self.driver._get_host_from_connector(conn) + self.assertEqual(host_name, None) + + # Initialize connection from the first volume to a host + self.driver.initialize_connection(volume1, conn) - # Initialize again, should notice it and do nothing - self.driver.initialize_connection(volume1, conn) + # Initialize again, should notice it and do nothing + self.driver.initialize_connection(volume1, conn) - # Try to delete the 1st volume (should fail because it is mapped) - self.assertRaises(exception.ProcessExecutionError, - self.driver.delete_volume, - volume1) + # Try to delete the 1st volume (should fail because it is mapped) + self.assertRaises(exception.ProcessExecutionError, + self.driver.delete_volume, + volume1) + + self.driver.terminate_connection(volume1, conn) + + # Check cases with no auth set for host + if self.USESIM: + for case in ['no_info', 'no_auth_set']: + conn_na = {'initiator': 'test:init:%s' % + random.randint(10000, 99999), + 'ip': '11.11.11.11', + 'host': 'host-%s' % case} + self.sim._add_host_to_list(conn_na) + volume1['volume_type_id'] = types['iscsi']['id'] + if case == 'no_info': + self.sim.error_injection('lsiscsiauth', 'no_info') + self.driver.initialize_connection(volume1, conn_na) + ret = self.driver._get_chap_secret_for_host(conn_na['host']) + self.assertNotEqual(ret, None) + self.driver.terminate_connection(volume1, conn_na) # Test no preferred node - self.driver.terminate_connection(volume1, conn) - if self.USESIM == 1: - self.sim.error_injection("lsvdisk", "no_pref_node") - self.driver.initialize_connection(volume1, conn) + if self.USESIM: + self.sim.error_injection('lsvdisk', 'no_pref_node') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + volume1, conn) # Initialize connection from the second volume to the host with no # preferred node set if in simulation mode, otherwise, just # another initialize connection. - if self.USESIM == 1: - self.sim.error_injection("lsvdisk", "blank_pref_node") + if self.USESIM: + self.sim.error_injection('lsvdisk', 'blank_pref_node') self.driver.initialize_connection(volume2, conn) # Try to remove connection from host that doesn't exist (should fail) - conn_no_exist = {"initiator": "i_dont_exist"} + conn_no_exist = conn.copy() + conn_no_exist['initiator'] = 'i_dont_exist' + conn_no_exist['wwpns'] = ['i_dont_exist'] self.assertRaises(exception.VolumeBackendAPIException, self.driver.terminate_connection, volume1, @@ -1402,29 +1766,124 @@ class StorwizeSVCDriverTestCase(test.TestCase): # Try to remove connection from volume that isn't mapped (should print # message but NOT fail) - vol_no_exist = {"name": "i_dont_exist"} + 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) + self._assert_vol_exists(volume1['name'], 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) + host_name = self.driver._get_host_from_connector(conn) + self.assertNotEqual(host_name, None) # 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) + self._assert_vol_exists(volume2['name'], False) + + # Delete volume types that we created + for protocol in ['fc', 'iscsi']: + volume_types.destroy(ctxt, types[protocol]['id']) # 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) + ret = self.driver._get_host_from_connector(conn) + self.assertEqual(ret, None) + + def test_storwize_svc_delete_volume_snapshots(self): + # Create a volume with two snapshots + master = self._generate_vol_info(None, None) + self.driver.create_volume(master) + self.driver.db.volume_set(master) + + # Fail creating a snapshot - will force delete the snapshot + if self.USESIM and False: + snap = self._generate_vol_info(master['name'], master['id']) + self.sim.error_injection('startfcmap', 'bad_id') + self.assertRaises(exception.ProcessExecutionError, + self.driver.create_snapshot, snap) + self._assert_vol_exists(snap['name'], False) + + # Delete a snapshot + snap = self._generate_vol_info(master['name'], master['id']) + self.driver.create_snapshot(snap) + self._assert_vol_exists(snap['name'], True) + self.driver.delete_snapshot(snap) + self._assert_vol_exists(snap['name'], False) + + # Delete a volume with snapshots (regular) + snap = self._generate_vol_info(master['name'], master['id']) + self.driver.create_snapshot(snap) + self._assert_vol_exists(snap['name'], True) + self.driver.delete_volume(master) + self._assert_vol_exists(master['name'], False) + + # Fail create volume from snapshot - will force delete the volume + if self.USESIM: + volfs = self._generate_vol_info(None, None) + self.sim.error_injection('startfcmap', 'bad_id') + self.sim.error_injection('lsfcmap', 'speed_up') + self.assertRaises(exception.ProcessExecutionError, + self.driver.create_volume_from_snapshot, + volfs, snap) + self._assert_vol_exists(volfs['name'], False) + + # Create volume from snapshot and delete it + volfs = self._generate_vol_info(None, None) + if self.USESIM: + self.sim.error_injection('lsfcmap', 'speed_up') + self.driver.create_volume_from_snapshot(volfs, snap) + self._assert_vol_exists(volfs['name'], True) + self.driver.delete_volume(volfs) + self._assert_vol_exists(volfs['name'], False) + + # Create volume from snapshot and delete the snapshot + volfs = self._generate_vol_info(None, None) + if self.USESIM: + self.sim.error_injection('lsfcmap', 'speed_up') + self.driver.create_volume_from_snapshot(volfs, snap) + self.driver.delete_snapshot(snap) + self._assert_vol_exists(snap['name'], False) + + # Fail create clone - will force delete the target volume + if self.USESIM: + clone = self._generate_vol_info(None, None) + self.sim.error_injection('startfcmap', 'bad_id') + self.sim.error_injection('lsfcmap', 'speed_up') + self.assertRaises(exception.ProcessExecutionError, + self.driver.create_cloned_volume, + clone, volfs) + self._assert_vol_exists(clone['name'], False) + + # Create the clone, delete the source and target + clone = self._generate_vol_info(None, None) + if self.USESIM: + self.sim.error_injection('lsfcmap', 'speed_up') + self.driver.create_cloned_volume(clone, volfs) + self._assert_vol_exists(clone['name'], True) + self.driver.delete_volume(volfs) + self._assert_vol_exists(volfs['name'], False) + self.driver.delete_volume(clone) + self._assert_vol_exists(clone['name'], False) + + # Note defined in python 2.6, so define here... + def assertLessEqual(self, a, b, msg=None): + if not a <= b: + self.fail('%s not less than or equal to %s' % (repr(a), repr(b))) + + def test_storwize_svc_get_volume_stats(self): + #first call, no refresh, expect None + stats = self.driver.get_volume_stats() + self.assertEqual(stats, {}) + + #call with refresh + stats = self.driver.get_volume_stats(refresh=True) + self.assertLessEqual(stats['free_capacity_gb'], + stats['total_capacity_gb']) + if self.USESIM: + self.assertEqual(stats['volume_backend_name'], + 'storwize-svc-sim_volpool') + self.assertAlmostEqual(stats['total_capacity_gb'], 3328.0) + self.assertAlmostEqual(stats['free_capacity_gb'], 3287.5) diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index 96391f028..e4888b504 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -21,6 +21,7 @@ Drivers for volumes. """ import os +import socket import time from cinder import exception @@ -342,9 +343,8 @@ class ISCSIDriver(VolumeDriver): def copy_image_to_volume(self, context, volume, image_service, image_id): """Fetch the image from image_service and write it to the volume.""" LOG.debug(_('copy_image_to_volume %s.') % volume['name']) - initiator = self._get_iscsi_initiator() - connector = {} - connector['initiator'] = initiator + connector = {'initiator': self._get_iscsi_initiator(), + 'host': socket.gethostname()} iscsi_properties, volume_path = self._attach_volume( context, volume, connector) @@ -360,9 +360,8 @@ class ISCSIDriver(VolumeDriver): def copy_volume_to_image(self, context, volume, image_service, image_meta): """Copy the volume to the specified image.""" LOG.debug(_('copy_volume_to_image %s.') % volume['name']) - initiator = self._get_iscsi_initiator() - connector = {} - connector['initiator'] = initiator + connector = {'initiator': self._get_iscsi_initiator(), + 'host': socket.gethostname()} iscsi_properties, volume_path = self._attach_volume( context, volume, connector) diff --git a/cinder/volume/drivers/storwize_svc.py b/cinder/volume/drivers/storwize_svc.py index 7afcb5d2e..82f763e58 100644 --- a/cinder/volume/drivers/storwize_svc.py +++ b/cinder/volume/drivers/storwize_svc.py @@ -1,7 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright (c) 2012 IBM, Inc. -# Copyright (c) 2012 OpenStack LLC. +# Copyright 2012, 2013 IBM Corp +# Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -21,7 +21,7 @@ # Avishay Traeger """ -Volume driver for IBM Storwize V7000 and SVC storage systems. +Volume driver for IBM Storwize family and SVC storage systems. Notes: 1. If you specify both a password and a key file, this driver will use the @@ -33,10 +33,11 @@ Notes: file or by using volume types(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 +1. The driver expects CLI output in English, error messages may be in a localized format. +2. Clones and creating volumes from snapshots, where the source and target + are of different sizes, is not supported. + """ import random @@ -44,6 +45,7 @@ import re import string import time +from cinder import context from cinder import exception from cinder import flags from cinder.openstack.common import cfg @@ -51,25 +53,29 @@ from cinder.openstack.common import excutils from cinder.openstack.common import log as logging from cinder import utils from cinder.volume.drivers.san import san +from cinder.volume import volume_types +VERSION = 1.1 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_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.IntOpt('storwize_svc_vol_rsize', + default=2, + help='Storage system space-efficiency parameter for volumes ' + '(percentage)'), + cfg.IntOpt('storwize_svc_vol_warning', + default=0, + help='Storage system threshold for volume capacity warnings ' + '(percentage)'), 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', + cfg.IntOpt('storwize_svc_vol_grainsize', + default=256, help='Storage system grain size parameter for volumes ' '(32/64/128/256)'), cfg.BoolOpt('storwize_svc_vol_compression', @@ -78,30 +84,50 @@ storwize_svc_opts = [ cfg.BoolOpt('storwize_svc_vol_easytier', default=True, help='Enable Easy Tier for volumes'), - cfg.StrOpt('storwize_svc_flashcopy_timeout', - default='120', + cfg.IntOpt('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).'), ] + 'prepared. Maximum value is 600 seconds (10 minutes).'), + cfg.StrOpt('storwize_svc_connection_protocol', + default='iscsi', + help='Connection protocol (iscsi/fc)'), + cfg.BoolOpt('storwize_svc_multipath_enabled', + default=False, + help='Connect with multipath (currently FC-only)'), +] FLAGS = flags.FLAGS FLAGS.register_opts(storwize_svc_opts) class StorwizeSVCDriver(san.SanISCSIDriver): - """IBM Storwize V7000 and SVC iSCSI volume driver.""" + """IBM Storwize V7000 and SVC iSCSI/FC volume driver. + + Version history: + 1.0 - Initial driver + 1.1 - FC support, create_cloned_volume, volume type support, + get_volume_stats, minor bug fixes + + """ + + """=====================================================================""" + """ SETUP """ + """=====================================================================""" def __init__(self, *args, **kwargs): super(StorwizeSVCDriver, self).__init__(*args, **kwargs) - self.iscsi_ipv4_conf = None - self.iscsi_ipv6_conf = None + self._storage_nodes = {} + self._enabled_protocols = set() + self._supported_protocols = ['iscsi', 'fc'] + self._compression_enabled = False + self._context = None - # Build cleanup transaltion tables for hosts names to follow valid - # host names for Storwizew V7000 and SVC storage systems. + # Build cleanup translation tables for host names 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 != '_')): + ch = str(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)) @@ -109,222 +135,152 @@ class StorwizeSVCDriver(san.SanISCSIDriver): 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. + def _get_iscsi_ip_addrs(self): + generator = self._port_conf_generator('lsportip') + header = next(generator, None) + if not header: + return - Create a dictionary object from the data row string using the header - string. The strings are converted to columns using the delimiter in - delim. - """ + for port_data in generator: + try: + port_node_id = port_data['node_id'] + port_ipv4 = port_data['IP_address'] + port_ipv6 = port_data['IP_address_6'] + state = port_data['state'] + except KeyError: + self._handle_keyerror('lsportip', header) + + if port_node_id in self._storage_nodes and ( + state == 'configured' or state == 'online'): + node = self._storage_nodes[port_node_id] + if len(port_ipv4): + node['ipv4'].append(port_ipv4) + if len(port_ipv6): + node['ipv6'].append(port_ipv6) - 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 _get_fc_wwpns(self): + generator = self._port_conf_generator('lsportfc') + header = next(generator, None) + if not header: + return - 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) + for port_data in generator: + try: + port_node_id = port_data['node_id'] + wwpn = port_data['WWPN'] + status = port_data['status'] + except KeyError as e: + self._handle_keyerror('lsportfc', header) - def check_for_setup_error(self): + if (port_node_id in self._storage_nodes and + 'unconfigured' not in status): + node = self._storage_nodes[port_node_id] + if len(wwpn) and wwpn not in node['WWPN']: + node['WWPN'].append(wwpn) + + def do_setup(self, ctxt): """Check that we have all configuration details from the storage.""" - LOG.debug(_('enter: check_for_setup_error')) + LOG.debug(_('enter: do_setup')) + self._context = ctxt # 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)}) + self._assert_ssh_return(len(out.strip()), 'do_setup', + ssh_cmd, out, err) search_text = '!%s!' % FLAGS.storwize_svc_volpool_name if search_text not in out: raise exception.InvalidInput( reason=(_('pool %s doesn\'t exist') % FLAGS.storwize_svc_volpool_name)) - storage_nodes = {} - # Get the iSCSI names of the Storwize/SVC nodes + # Check if compression is supported + ssh_cmd = 'lslicense -delim !' + out, err = self._run_ssh(ssh_cmd) + license_lines = out.strip().split('\n') + self._compression_enabled = False + for license_line in license_lines: + name, foo, value = license_line.partition('!') + if ((name == "license_compression_enclosures" and value != '0') or + (name == "license_compression_capacity" and value != '0')): + self._compression_enabled = True + + # Get the iSCSI and FC names of the Storwize/SVC nodes ssh_cmd = 'svcinfo lsnode -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)}) + self._assert_ssh_return(len(out.strip()), 'do_setup', + ssh_cmd, out, 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)}) + self._assert_ssh_return(len(nodes), + 'do_setup', ssh_cmd, out, 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: + except exception.VolumeBackendAPIException: 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)}) + self._log_cli_output_error('do_setup', + ssh_cmd, out, err) node = {} try: node['id'] = node_data['id'] node['name'] = node_data['name'] + node['IO_group'] = node_data['IO_group_id'] node['iscsi_name'] = node_data['iscsi_name'] + node['WWNN'] = node_data['WWNN'] node['status'] = node_data['status'] + node['WWPN'] = [] 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 ' - 'svcinfo lsnode: %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 + node['enabled_protocols'] = [] + if node['status'] == 'online': + self._storage_nodes[node['id']] = node + except KeyError: + self._handle_keyerror('lsnode', header) + + # Get the iSCSI IP addresses and WWPNs of the Storwize/SVC nodes + self._get_iscsi_ip_addrs() + self._get_fc_wwpns() + + # For each node, check what connection modes it supports. Delete any + # nodes that do not support any types (may be partially configured). + to_delete = [] + for k, node in self._storage_nodes.iteritems(): + if ((len(node['ipv4']) or len(node['ipv6'])) + and len(node['iscsi_name'])): + node['enabled_protocols'].append('iscsi') + self._enabled_protocols.add('iscsi') + if len(node['WWPN']): + node['enabled_protocols'].append('fc') + self._enabled_protocols.add('fc') + if not len(node['enabled_protocols']): + to_delete.append(k) + + for delkey in to_delete: + del self._storage_nodes[delkey] + + # Make sure we have at least one node configured + self._driver_assert(len(self._storage_nodes), + _('do_setup: No configured nodes')) - LOG.debug(_('leave: check_for_setup_error')) + LOG.debug(_('leave: do_setup')) - 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 _build_default_opts(self): + opts = {'rsize': FLAGS.storwize_svc_vol_rsize, + 'warning': FLAGS.storwize_svc_vol_warning, + 'autoexpand': FLAGS.storwize_svc_vol_autoexpand, + 'grainsize': FLAGS.storwize_svc_vol_grainsize, + 'compression': FLAGS.storwize_svc_vol_compression, + 'easytier': FLAGS.storwize_svc_vol_easytier, + 'protocol': FLAGS.storwize_svc_connection_protocol, + 'multipath': FLAGS.storwize_svc_multipath_enabled} + return opts - def _check_flags(self): + def check_for_setup_error(self): """Ensure that the flags are set properly.""" + LOG.debug(_('enter: check_for_setup_error')) required_flags = ['san_ip', 'san_ssh_port', 'san_login', 'storwize_svc_volpool_name'] @@ -339,302 +295,475 @@ class StorwizeSVCDriver(san.SanISCSIDriver): 'authentication: set either san_password or ' 'san_private_key option')) - # Check that rsize is a number or percentage - rsize = FLAGS.storwize_svc_vol_rsize - if not self._check_num_perc(rsize) and (rsize != '-1'): - raise exception.InvalidInput( - reason=_('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 = FLAGS.storwize_svc_vol_warning - if not self._check_num_perc(warning): - raise exception.InvalidInput( - reason=_('Illegal value specified for ' - 'storwize_svc_vol_warning: ' - 'set to either a number or a percentage')) - - # Check that grainsize is 32/64/128/256 - grainsize = FLAGS.storwize_svc_vol_grainsize - if grainsize not in ['32', '64', '128', '256']: - raise exception.InvalidInput( - reason=_('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 + # Check that flashcopy_timeout is not more than 10 minutes flashcopy_timeout = FLAGS.storwize_svc_flashcopy_timeout - if not (flashcopy_timeout.isdigit() and int(flashcopy_timeout) > 0 and - int(flashcopy_timeout) <= 600): + if not (flashcopy_timeout > 0 and flashcopy_timeout <= 600): raise exception.InvalidInput( - reason=_('Illegal value %s specified for ' + reason=_('Illegal value %d specified for ' 'storwize_svc_flashcopy_timeout: ' 'valid values are between 0 and 600') % flashcopy_timeout) - # Check that rsize is set - volume_compression = FLAGS.storwize_svc_vol_compression - if ((volume_compression is True) and - (FLAGS.storwize_svc_vol_rsize == '-1')): - raise exception.InvalidInput( - reason=_('If compression is set to True, rsize must ' - 'also be set (not equal to -1)')) + opts = self._build_default_opts() + self._check_vdisk_opts(opts) - def do_setup(self, context): - """Validate the flags.""" - LOG.debug(_('enter: do_setup')) - self._check_flags() - LOG.debug(_('leave: do_setup')) + LOG.debug(_('leave: check_for_setup_error')) - def create_volume(self, volume): - """Create a new volume - uses the internal method.""" - return self._create_volume(volume, units='gb') + """=====================================================================""" + """ INITIALIZE/TERMINATE CONNECTIONS """ + """=====================================================================""" + + def ensure_export(self, ctxt, volume): + """Check that the volume exists on the storage. - def _create_volume(self, volume, units='gb'): - """Create a new volume.""" + 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_vdisk_defined(volume['name']) + if not volume_defined: + LOG.error(_('ensure_export: Volume %s not found on storage') + % volume['name']) - name = volume['name'] + def create_export(self, ctxt, volume): model_update = None + return model_update - LOG.debug(_('enter: create_volume: volume %s ') % name) + def remove_export(self, ctxt, volume): + pass - size = int(volume['size']) + def _add_chapsecret_to_host(self, host_name): + """Generate and store a randomly-generated CHAP secret for the host.""" - if FLAGS.storwize_svc_vol_autoexpand is True: - autoex = '-autoexpand' - else: - autoex = '' + chap_secret = utils.generate_password() + ssh_cmd = ('chhost -chapsecret "%(chap_secret)s" %(host_name)s' + % {'chap_secret': chap_secret, 'host_name': host_name}) + out, err = self._run_ssh(ssh_cmd) + # No output should be returned from chhost + self._assert_ssh_return(len(out.strip()) == 0, + '_add_chapsecret_to_host', ssh_cmd, out, err) + return chap_secret - if FLAGS.storwize_svc_vol_easytier is True: - easytier = '-easytier on' - else: - easytier = '-easytier off' + def _get_chap_secret_for_host(self, host_name): + """Return the CHAP secret for the given host.""" - # Set space-efficient options - if FLAGS.storwize_svc_vol_rsize.strip() == '-1': - ssh_cmd_se_opt = '' - else: - ssh_cmd_se_opt = ( - '-rsize %(rsize)s %(autoex)s -warning %(warn)s' % - {'rsize': FLAGS.storwize_svc_vol_rsize, - 'autoex': autoex, - 'warn': FLAGS.storwize_svc_vol_warning}) - if FLAGS.storwize_svc_vol_compression: - ssh_cmd_se_opt = ssh_cmd_se_opt + ' -compressed' - else: - ssh_cmd_se_opt = ssh_cmd_se_opt + ( - ' -grainsize %(grain)s' % - {'grain': FLAGS.storwize_svc_vol_grainsize}) + LOG.debug(_('enter: _get_chap_secret_for_host: host name %s') + % host_name) - ssh_cmd = ('mkvdisk -name %(name)s -mdiskgrp %(mdiskgrp)s ' - '-iogrp 0 -size %(size)s -unit ' - '%(unit)s %(easytier)s %(ssh_cmd_se_opt)s' - % {'name': name, - 'mdiskgrp': FLAGS.storwize_svc_volpool_name, - 'size': size, 'unit': units, 'easytier': easytier, - 'ssh_cmd_se_opt': ssh_cmd_se_opt}) + ssh_cmd = 'lsiscsiauth -delim !' 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)}) + if not len(out.strip()): + return None - LOG.debug(_('leave: create_volume: volume %(name)s ') % {'name': name}) + host_lines = out.strip().split('\n') + self._assert_ssh_return(len(host_lines), '_get_chap_secret_for_host', + ssh_cmd, out, err) - def delete_volume(self, volume): - self._delete_volume(volume, False) + header = host_lines.pop(0).split('!') + self._assert_ssh_return('name' in header, '_get_chap_secret_for_host', + ssh_cmd, out, err) + self._assert_ssh_return('iscsi_auth_method' in header, + '_get_chap_secret_for_host', ssh_cmd, out, err) + self._assert_ssh_return('iscsi_chap_secret' in header, + '_get_chap_secret_for_host', ssh_cmd, out, err) + name_index = header.index('name') + method_index = header.index('iscsi_auth_method') + secret_index = header.index('iscsi_chap_secret') - def _delete_volume(self, volume, force_opt): - """Driver entry point for destroying existing volumes.""" + chap_secret = None + host_found = False + for line in host_lines: + info = line.split('!') + if info[name_index] == host_name: + host_found = True + if info[method_index] == 'chap': + chap_secret = info[secret_index] - name = volume['name'] - LOG.debug(_('enter: delete_volume: volume %(name)s ') % {'name': name}) + self._assert_ssh_return(host_found, '_get_chap_secret_for_host', + ssh_cmd, out, err) - if force_opt: - force_flag = '-force' - else: - force_flag = '' + LOG.debug(_('leave: _get_chap_secret_for_host: host name ' + '%(host_name)s with secret %(chap_secret)s') + % {'host_name': host_name, 'chap_secret': chap_secret}) - 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}) + return chap_secret - LOG.debug(_('leave: delete_volume: volume %(name)s ') % {'name': name}) + def _connector_to_hostname_prefix(self, connector): + """Translate connector info to storage system host name. - def ensure_export(self, context, volume): - """Check that the volume exists on the storage. + Translate a host's name and IP to the prefix of its hostname on the + storage subsystem. We create a host name host name from the host and + IP address, replacing any invalid characters (at most 55 characters), + and adding a random 8-character suffix to avoid collisions. The total + length should be at most 63 characters. - 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 + host_name = connector['host'] + 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_host: Cannot clean host name. Host name ' + 'is not unicode or string') + LOG.error(msg) + raise exception.NoValidHost(reason=msg) - def remove_export(self, context, volume): - pass + host_name = str(host_name) + return host_name[:55] - def initialize_connection(self, volume, connector): - """Perform the necessary work so that an iSCSI connection can be made. + def _get_host_from_connector(self, connector): + """List the hosts defined in the storage. + + Return the host name with the given connection info, or None if there + is no host fitting that information. - 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'] + prefix = self._connector_to_hostname_prefix(connector) + LOG.debug(_('enter: _get_host_from_connector: prefix %s') % prefix) - 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.')) + # Get list of host in the storage + ssh_cmd = 'lshost -delim !' + out, err = self._run_ssh(ssh_cmd) - chap_secret = self._get_chap_secret_for_host(host_name) - if chap_secret is None: - chap_secret = self._add_chapsecret_to_host(host_name) + if not len(out.strip()): + return None - lun_id = self._map_vol_to_host(volume_name, host_name) + host_lines = out.strip().split('\n') + self._assert_ssh_return(len(host_lines), '_get_host_from_connector', + ssh_cmd, out, err) + header = host_lines.pop(0).split('!') + self._assert_ssh_return('name' in header, '_get_host_from_connector', + ssh_cmd, out, err) + name_index = header.index('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 + hosts = map(lambda x: x.split('!')[name_index], host_lines) + hostname = None + + # For each host with the prefix, check connection details to verify + for host in hosts: + if not host.startswith(prefix): + continue + ssh_cmd = 'lshost -delim ! %s' % host + out, err = self._run_ssh(ssh_cmd) + self._assert_ssh_return(len(out.strip()), + '_get_host_from_connector', + ssh_cmd, out, err) + for attr_line in out.split('\n'): + # If '!' not found, return the string and two empty strings + attr_name, foo, attr_val = attr_line.partition('!') + found = False + if ('initiator' in connector and + attr_name == 'iscsi_name' and + attr_val == connector['initiator']): + found = True + elif ('wwpns' in connector and + attr_name == 'WWPN' and + attr_val.lower() in + map(str.lower, map(str, connector['wwpns']))): + found = True + + if found: + hostname = host 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'] - properties['auth_method'] = 'CHAP' - properties['auth_username'] = initiator_name - properties['auth_password'] = chap_secret - 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)}) + if hostname is not None: + break - return {'driver_volume_type': 'iscsi', 'data': properties, } + LOG.debug(_('leave: _get_host_from_connector: host %s') % hostname) - def terminate_connection(self, volume, connector, **kwargs): - """Cleanup after an iSCSI connection has been terminated. + return hostname + + def _create_host(self, connector): + """Create a new host on the storage system. + + We create a host name and associate it with the given connection + information. - 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) + LOG.debug(_('enter: _create_host: host %s') % connector['host']) + + rand_id = str(random.randint(0, 99999999)).zfill(8) + host_name = '%s-%s' % (self._connector_to_hostname_prefix(connector), + rand_id) + + # Get all port information from the connector + ports = [] + if 'initiator' in connector: + ports.append('-iscsiname %s' % connector['initiator']) + if 'wwpns' in connector: + for wwpn in connector['wwpns']: + ports.append('-hbawwpn %s' % wwpn) + + # When creating a host, we need one port + self._driver_assert(len(ports), _('_create_host: No connector ports')) + port1 = ports.pop(0) + ssh_cmd = ('mkhost -force %(port1)s -name "%(host_name)s"' % + {'port1': port1, 'host_name': host_name}) + out, err = self._run_ssh(ssh_cmd) + self._assert_ssh_return('successfully created' in out, + '_create_host', ssh_cmd, out, err) - # 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)}) + # Add any additional ports to the host + for port in ports: + ssh_cmd = ('addhostport -force %s %s' % (port, host_name)) + out, err = self._run_ssh(ssh_cmd) + + LOG.debug(_('leave: _create_host: host %(host)s - %(host_name)s') % + {'host': connector['host'], 'host_name': host_name}) + return host_name + + 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) + + mappings = out.strip().split('\n') + if len(mappings): + 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 %(volume_name)s to ' + 'host %(host_name)s') + % {'volume_name': volume_name, 'host_name': 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: + ssh_cmd = ('mkvdiskhostmap -host %(host_name)s -scsi ' + '%(result_lun)s %(volume_name)s' % + {'host_name': host_name, + 'result_lun': result_lun, + 'volume_name': volume_name}) + out, err = self._run_ssh(ssh_cmd) + self._assert_ssh_return('successfully created' in out, + '_map_vol_to_host', ssh_cmd, out, err) + + LOG.debug(_('leave: _map_vol_to_host: LUN %(result_lun)s, volume ' + '%(volume_name)s, host %(host_name)s') % + {'result_lun': result_lun, + 'volume_name': volume_name, + 'host_name': host_name}) + return result_lun + + def _delete_host(self, host_name): + """Delete a host on the storage system.""" + + LOG.debug(_('enter: _delete_host: host %s ') % host_name) + + ssh_cmd = 'rmhost %s ' % host_name + out, err = self._run_ssh(ssh_cmd) + # No output should be returned from rmhost + self._assert_ssh_return(len(out.strip()) == 0, + '_delete_host', ssh_cmd, out, err) + + LOG.debug(_('leave: _delete_host: host %s ') % host_name) + + def _get_conn_fc_wwpns(self, host_name): + wwpns = [] + cmd = 'lsfabric -host %s' % host_name + generator = self._port_conf_generator(cmd) + header = next(generator, None) + if not header: + return wwpns + + for port_data in generator: + try: + wwpns.append(port_data['local_wwpn']) + except KeyError as e: + self._handle_keyerror('lsfabric', header) + + return wwpns + + def initialize_connection(self, volume, connector): + """Perform the necessary work so that an iSCSI/FC connection can + be made. + + To be able to create an iSCSI/FC connection from a given host to a + volume, we must: + 1. Translate the given iSCSI name or WWNN to a host name + 2. Create new host on the storage system if it does not yet exist + 3. Map the volume to the host if it is not already done + 4. Return the connection information for relevant nodes (in the + proper I/O group) + + """ + + LOG.debug(_('enter: initialize_connection: volume %(vol)s with ' + 'connector %(conn)s') % {'vol': str(volume), + 'conn': str(connector)}) + + vol_opts = self._get_vdisk_params(volume['volume_type_id']) + host_name = connector['host'] + volume_name = volume['name'] + + # Check if a host object is defined for this host name + host_name = self._get_host_from_connector(connector) + if host_name is None: + # Host does not exist - add a new host to Storwize/SVC + host_name = self._create_host(connector) + # Verify that create_new_host succeeded + self._driver_assert( + host_name is not None, + _('_create_host failed to return the host name.')) + + if vol_opts['protocol'] == 'iscsi': + chap_secret = self._get_chap_secret_for_host(host_name) + if chap_secret is None: + chap_secret = self._add_chapsecret_to_host(host_name) + + volume_attributes = self._get_vdisk_attributes(volume_name) + lun_id = self._map_vol_to_host(volume_name, host_name) + + self._driver_assert(volume_attributes is not None, + _('initialize_connection: Failed to get attributes' + ' for volume %s') % volume_name) + + try: + preferred_node = volume_attributes['preferred_node_id'] + IO_group = volume_attributes['IO_group_id'] + except KeyError as e: + LOG.error(_('Did not find expected column name in ' + 'lsvdisk: %s') % str(e)) + exception_msg = (_('initialize_connection: Missing volume ' + 'attribute for volume %s') % volume_name) + raise exception.VolumeBackendAPIException(data=exception_msg) + + try: + # Get preferred node and other nodes in I/O group + preferred_node_entry = None + io_group_nodes = [] + for k, node in self._storage_nodes.iteritems(): + if vol_opts['protocol'] not in node['enabled_protocols']: + continue + if node['id'] == preferred_node: + preferred_node_entry = node + if node['IO_group'] == IO_group: + io_group_nodes.append(node) + + if not len(io_group_nodes): + exception_msg = (_('initialize_connection: No node found in ' + 'I/O group %(gid)s for volume %(vol)s') % + {'gid': IO_group, 'vol': volume_name}) + raise exception.VolumeBackendAPIException(data=exception_msg) + + if not preferred_node_entry and not vol_opts['multipath']: + # Get 1st node in I/O group + preferred_node_entry = io_group_nodes[0] + LOG.warn(_('initialize_connection: Did not find a preferred ' + 'node for volume %s') % volume_name) + + properties = {} + properties['target_discovered'] = False + properties['target_lun'] = lun_id + properties['volume_id'] = volume['id'] + if vol_opts['protocol'] == 'iscsi': + type_str = 'iscsi' + # We take the first IP address for now. Ideally, OpenStack will + # support iSCSI multipath for improved performance. + if len(preferred_node_entry['ipv4']): + ipaddr = preferred_node_entry['ipv4'][0] + else: + ipaddr = preferred_node_entry['ipv6'][0] + properties['target_portal'] = '%s:%s' % (ipaddr, '3260') + properties['target_iqn'] = preferred_node_entry['iscsi_name'] + properties['auth_method'] = 'CHAP' + properties['auth_username'] = connector['initiator'] + properties['auth_password'] = chap_secret + else: + type_str = 'fibre_channel' + conn_wwpns = self._get_conn_fc_wwpns(host_name) + if not vol_opts['multipath']: + if preferred_node_entry['WWPN'] in conn_wwpns: + properties['target_wwn'] = preferred_node_entry['WWPN'] + else: + properties['target_wwn'] = conn_wwpns[0] + else: + properties['target_wwn'] = conn_wwpns + except Exception: + with excutils.save_and_reraise_exception(): + self.terminate_connection(volume, connector) + LOG.error(_('initialize_connection: Failed to collect return ' + 'properties for volume %(vol)s and connector ' + '%(conn)s.\n') % {'vol': str(volume), + 'conn': str(connector)}) + + 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': type_str, 'data': properties, } + + def terminate_connection(self, volume, connector, **kwargs): + """Cleanup after an iSCSI connection has been terminated. + + When we clean up a terminated connection between a given connector + and volume, we: + 1. Translate the given connector 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'] + host_name = self._get_host_from_connector(connector) + # Verify that _get_host_from_connector returned the host. + # This should always succeed as we terminate an existing connection. + self._driver_assert( + host_name is not None, + _('_get_host_from_connector failed to return the host name ' + 'for connector')) + + # Check if vdisk-host mapping exists, remove if it does + mapping_data = self._get_hostvdisk_mappings(host_name) + if vol_name in mapping_data: + ssh_cmd = 'rmvdiskhostmap -host %s %s' % (host_name, vol_name) + out, err = self._run_ssh(ssh_cmd) + # Verify CLI behaviour - no output is returned from + # rmvdiskhostmap + self._assert_ssh_return(len(out.strip()) == 0, + 'terminate_connection', ssh_cmd, out, 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}) + LOG.error(_('terminate_connection: No mapping of volume ' + '%(vol_name)s to host %(host_name)s found') % + {'vol_name': vol_name, 'host_name': host_name}) # If this host has no more mappings, delete it if not mapping_data: @@ -644,36 +773,108 @@ class StorwizeSVCDriver(san.SanISCSIDriver): 'connector %(conn)s') % {'vol': str(volume), 'conn': str(connector)}) - def _flashcopy_cleanup(self, fc_map_id, source, target): - """Clean up a failed FlashCopy operation.""" + """=====================================================================""" + """ VOLUMES/SNAPSHOTS """ + """=====================================================================""" - try: - out, err = self._run_ssh('stopfcmap -force %s' % fc_map_id) - out, err = self._run_ssh('rmfcmap -force %s' % fc_map_id) - except exception.ProcessExecutionError as e: - LOG.error(_('_run_flashcopy: fail to cleanup failed FlashCopy ' - 'mapping %(fc_map_id)% ' - 'from %(source)s to %(target)s.\n' - 'stdout: %(out)s\n stderr: %(err)s') - % {'fc_map_id': fc_map_id, - 'source': source, - 'target': target, - 'out': e.stdout, - 'err': e.stderr}) + def _get_vdisk_attributes(self, vdisk_name): + """Return vdisk attributes, or None if vdisk does not exist - def _run_flashcopy(self, source, target): - """Create a FlashCopy mapping from the source to the target.""" + Exception is raised if the information from system can not be + parsed/matched to a single vdisk. + """ + + ssh_cmd = 'lsvdisk -bytes -delim ! %s ' % vdisk_name + return self._execute_command_and_parse_attributes(ssh_cmd) + + def _get_vdisk_fc_mappings(self, vdisk_name): + """Return FlashCopy mappings that this vdisk is associated with.""" + + ssh_cmd = 'lsvdiskfcmappings -nohdr %s' % vdisk_name + out, err = self._run_ssh(ssh_cmd) + + mapping_ids = [] + if (len(out.strip())): + lines = out.strip().split('\n') + for line in lines: + mapping_ids.append(line.split()[0]) + return mapping_ids + + def _get_vdisk_params(self, type_id): + opts = self._build_default_opts() + if type_id: + ctxt = context.get_admin_context() + volume_type = volume_types.get_volume_type(ctxt, type_id) + specs = volume_type.get('extra_specs') + for key, value in specs.iteritems(): + if key in opts: + this_type = type(opts[key]).__name__ + if this_type == 'int': + value = int(value) + elif this_type == 'bool': + value = False if value == "0" else True + opts[key] = value + self._check_vdisk_opts(opts) + return opts + + def _create_vdisk(self, name, size, units, opts): + """Create a new vdisk.""" + + LOG.debug(_('enter: _create_vdisk: vdisk %s ') % name) + + model_update = None + autoex = '-autoexpand' if opts['autoexpand'] else '' + easytier = '-easytier on' if opts['easytier'] else '-easytier off' + + # Set space-efficient options + if opts['rsize'] == -1: + ssh_cmd_se_opt = '' + else: + ssh_cmd_se_opt = ( + '-rsize %(rsize)d%% %(autoex)s -warning %(warn)d%%' % + {'rsize': opts['rsize'], + 'autoex': autoex, + 'warn': opts['warning']}) + if opts['compression']: + ssh_cmd_se_opt = ssh_cmd_se_opt + ' -compressed' + else: + ssh_cmd_se_opt = ssh_cmd_se_opt + ( + ' -grainsize %d' % opts['grainsize']) + + ssh_cmd = ('mkvdisk -name %(name)s -mdiskgrp %(mdiskgrp)s ' + '-iogrp 0 -size %(size)s -unit ' + '%(unit)s %(easytier)s %(ssh_cmd_se_opt)s' + % {'name': name, + 'mdiskgrp': FLAGS.storwize_svc_volpool_name, + 'size': size, 'unit': units, 'easytier': easytier, + 'ssh_cmd_se_opt': ssh_cmd_se_opt}) + out, err = self._run_ssh(ssh_cmd) + self._assert_ssh_return(len(out.strip()), '_create_vdisk', + ssh_cmd, out, 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_vdisk %(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( - _('enter: _run_flashcopy: execute FlashCopy from source ' - '%(source)s to target %(target)s') % {'source': source, - 'target': target}) + LOG.debug(_('leave: _create_vdisk: volume %s ') % name) - fc_map_cli_cmd = ('mkfcmap -source %s -target %s -autodelete ' - '-cleanrate 0' % (source, target)) + def _make_fc_map(self, source, target, full_copy): + copyflag = '' if full_copy else '-copyrate 0' + fc_map_cli_cmd = ('mkfcmap -source %(src)s -target %(tgt)s ' + '-autodelete %(copyflag)s' % + {'src': source, + 'tgt': target, + 'copyflag': copyflag}) out, err = self._run_ssh(fc_map_cli_cmd) self._driver_assert( - len(out.strip()) > 0, + len(out.strip()), _('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') @@ -717,24 +918,27 @@ class StorwizeSVCDriver(san.SanISCSIDriver): 'target': target, 'out': str(out), 'err': str(err)}) + return fc_map_id + + def _call_prepare_fc_map(self, fc_map_id, source, target): 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 ' + LOG.error(_('_prepare_fc_map: Failed 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}) - self._flashcopy_cleanup(fc_map_id, source, target) + def _prepare_fc_map(self, fc_map_id, source, target): + self._call_prepare_fc_map(fc_map_id, source, target) mapping_ready = False wait_time = 5 # Allow waiting of up to timeout (set as parameter) - max_retries = (int(FLAGS.storwize_svc_flashcopy_timeout) - / wait_time) + 1 + max_retries = (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) @@ -744,9 +948,11 @@ class StorwizeSVCDriver(san.SanISCSIDriver): if mapping_attributes['status'] == 'prepared': mapping_ready = True break + elif mapping_attributes['status'] == 'stopped': + self._call_prepare_fc_map(fc_map_id, source, target) elif mapping_attributes['status'] != 'preparing': # Unexpected mapping status - exception_msg = (_('unexecpted mapping status %(status)s ' + exception_msg = (_('Unexecpted mapping status %(status)s ' 'for mapping %(id)s. Attributes: ' '%(attr)s') % {'status': mapping_attributes['status'], @@ -757,519 +963,418 @@ class StorwizeSVCDriver(san.SanISCSIDriver): time.sleep(wait_time) if not mapping_ready: - exception_msg = (_('mapping %(id)s prepare failed to complete ' - 'within the alloted %(to)s seconds timeout. ' - 'Terminating') + exception_msg = (_('Mapping %(id)s prepare failed to complete ' + 'within the alloted %(to)d seconds timeout. ' + 'Terminating.') % {'id': fc_map_id, 'to': FLAGS.storwize_svc_flashcopy_timeout}) - LOG.error(_('_run_flashcopy: fail to start FlashCopy ' + LOG.error(_('_prepare_fc_map: Failed to start FlashCopy ' 'from %(source)s to %(target)s with ' 'exception %(ex)s') % {'source': source, 'target': target, 'ex': exception_msg}) - self._flashcopy_cleanup(fc_map_id, source, target) raise exception.InvalidSnapshot( - reason=_('_run_flashcopy: %s') % exception_msg) + reason=_('_prepare_fc_map: %s') % exception_msg) + def _start_fc_map(self, fc_map_id, source, target): 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 ' + LOG.error(_('_start_fc_map: Failed 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}) - self._flashcopy_cleanup(fc_map_id, source, target) - - 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 = snapshot['name'] - tgt_volume = volume['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.SnapshotNotFound(exception_msg, - volume_id=source_volume) - self._driver_assert( - 'capacity' in src_volume_attributes, - _('create_volume_from_snapshot: cannot get source ' - 'volume %(src)s capacity from volume attributes ' - '%(attr)s') - % {'src': source_volume, - 'attr': src_volume_attributes}) - 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 + def _run_flashcopy(self, source, target, full_copy=True): + """Create a FlashCopy mapping from the source to the target.""" - self._create_volume(snapshot_volume, units='b') + LOG.debug(_('enter: _run_flashcopy: execute FlashCopy from source ' + '%(source)s to target %(target)s') % + {'source': source, 'target': target}) + fc_map_id = self._make_fc_map(source, target, full_copy) try: - self._run_flashcopy(source_volume, tgt_volume) + self._prepare_fc_map(fc_map_id, source, target) + self._start_fc_map(fc_map_id, source, target) except Exception: with excutils.save_and_reraise_exception(): - # Clean up newly-created snapshot if the FlashCopy failed - self._delete_volume(snapshot_volume, True) + self._delete_vdisk(target, True) - LOG.debug( - _('leave: create_volume_from_snapshot: %s created successfully') - % tgt_volume) + LOG.debug(_('leave: _run_flashcopy: FlashCopy started from ' + '%(source)s to %(target)s') % + {'source': source, 'target': target}) - def create_snapshot(self, snapshot): + def _create_copy(self, src_vdisk, tgt_vdisk, full_copy, opts, src_id, + from_vol): """Create a new snapshot using FlashCopy.""" - src_volume = snapshot['volume_name'] - tgt_volume = snapshot['name'] + LOG.debug(_('enter: _create_copy: snapshot %(tgt_vdisk)s from ' + 'vdisk %(src_vdisk)s') % + {'tgt_vdisk': tgt_vdisk, 'src_vdisk': src_vdisk}) - # Flag to keep track of created volumes in case FlashCopy - tgt_volume_created = False - - 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: + src_vdisk_attributes = self._get_vdisk_attributes(src_vdisk) + if src_vdisk_attributes is None: exception_msg = ( - _('create_snapshot: source volume %s does not exist') - % src_volume) + _('_create_copy: Source vdisk %s does not exist') + % src_vdisk) LOG.error(exception_msg) - raise exception.VolumeNotFound(exception_msg, - volume_id=src_volume) + if from_vol: + raise exception.VolumeNotFound(exception_msg, + volume_id=src_id) + else: + raise exception.SnapshotNotFound(exception_msg, + snapshot_id=src_id) self._driver_assert( - 'capacity' in src_volume_attributes, - _('create_volume_from_snapshot: cannot get source ' - 'volume %(src)s capacity from volume attributes ' + 'capacity' in src_vdisk_attributes, + _('_create_copy: cannot get source vdisk ' + '%(src)s capacity from vdisk attributes ' '%(attr)s') - % {'src': src_volume, - 'attr': src_volume_attributes}) - - source_volume_size = src_volume_attributes['capacity'] - - tgt_volume_attributes = self._get_volume_attributes(tgt_volume) - # Does the snapshot target exist? - snapshot_volume = {} - if tgt_volume_attributes is None: - # No, create a new snapshot volume - snapshot_volume['name'] = tgt_volume - snapshot_volume['size'] = source_volume_size - self._create_volume(snapshot_volume, units='b') - tgt_volume_created = True - else: - # Yes, target exists, verify exact same size as source - self._driver_assert( - 'capacity' in tgt_volume_attributes, - _('create_volume_from_snapshot: cannot get source ' - 'volume %(src)s capacity from volume attributes ' - '%(attr)s') - % {'src': tgt_volume, - 'attr': tgt_volume_attributes}) - 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) + % {'src': src_vdisk, + 'attr': src_vdisk_attributes}) - try: - self._run_flashcopy(src_volume, tgt_volume) - except exception.InvalidSnapshot: - with excutils.save_and_reraise_exception(): - # Clean up newly-created snapshot if the FlashCopy failed - if tgt_volume_created: - self._delete_volume(snapshot_volume, True) + src_vdisk_size = src_vdisk_attributes['capacity'] + self._create_vdisk(tgt_vdisk, src_vdisk_size, 'b', opts) + self._run_flashcopy(src_vdisk, tgt_vdisk, full_copy) - LOG.debug(_('leave: create_snapshot: %s created successfully') - % tgt_volume) + LOG.debug(_('leave: _create_copy: snapshot %(tgt_vdisk)s from ' + 'vdisk %(src_vdisk)s') % + {'tgt_vdisk': tgt_vdisk, 'src_vdisk': src_vdisk}) - 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) + def _get_flashcopy_mapping_attributes(self, fc_map_id): + LOG.debug(_('enter: _get_flashcopy_mapping_attributes: mapping %s') + % fc_map_id) - if (len(out.strip()) == 0): + fc_ls_map_cmd = ('lsfcmap -filtervalue id=%s -delim !' % fc_map_id) + out, err = self._run_ssh(fc_ls_map_cmd) + if not len(out.strip()): 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, 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) + # 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._assert_ssh_return(len(lines) <= 2, + '_get_flashcopy_mapping_attributes', + fc_ls_map_cmd, out, err) - return hostname + if len(lines) == 2: + attributes = self._get_hdr_dic(lines[0], lines[1], '!') + else: # 0 or 1 lines + attributes = None - def _add_chapsecret_to_host(self, host_name): - """Generate and store a randomly-generated CHAP secret for the host.""" + LOG.debug(_('leave: _get_flashcopy_mapping_attributes: mapping ' + '%(fc_map_id)s, attributes %(attributes)s') % + {'fc_map_id': fc_map_id, 'attributes': attributes}) - chap_secret = utils.generate_password() - out, err = self._run_ssh('chhost -chapsecret "%s" %s' - % (chap_secret, host_name)) - # No output should be returned from chhost - self._driver_assert( - len(out.strip()) == 0, - _('change host %(name)s - non empty output from CLI.\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'name': host_name, - 'out': str(out), - 'err': str(err)}) + return attributes - return chap_secret + def _is_vdisk_defined(self, vdisk_name): + """Check if vdisk is defined.""" + LOG.debug(_('enter: _is_vdisk_defined: vdisk %s ') % vdisk_name) + vdisk_attributes = self._get_vdisk_attributes(vdisk_name) + LOG.debug(_('leave: _is_vdisk_defined: vdisk %(vol)s with %(str)s ') + % {'vol': vdisk_name, + 'str': vdisk_attributes is not None}) + if vdisk_attributes is None: + return False + else: + return True - def _create_new_host(self, host_name, initiator_name): - """Create a new host on the storage system. + def _delete_vdisk(self, name, force): + """Deletes existing vdisks. + + It is very important to properly take care of mappings before deleting + the disk: + 1. If no mappings, then it was a vdisk, and can be deleted + 2. If it is the source of a flashcopy mapping and copy_rate is 0, then + it is a vdisk that has a snapshot. If the force flag is set, + delete the mapping and the vdisk, otherwise set the mapping to + copy and wait (this will allow users to delete vdisks that have + snapshots if/when the upper layers allow it). + 3. If it is the target of a mapping and copy_rate is 0, it is a + snapshot, and we should properly stop the mapping and delete. + 4. If it is the source/target of a mapping and copy_rate is not 0, it + is a clone or vdisk created from a snapshot. We wait for the copy + to complete (the mapping will be autodeleted) and then delete the + vdisk. - 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) + LOG.debug(_('enter: _delete_vdisk: vdisk %s') % name) - # 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)}) + # Try to delete volume only if found on the storage + vdisk_defined = self._is_vdisk_defined(name) + if not vdisk_defined: + LOG.info(_('warning: Tried to delete vdisk %s but it does not ' + 'exist.') % name) + return + + # Ensure vdisk has no FlashCopy mappings + mapping_ids = self._get_vdisk_fc_mappings(name) + while len(mapping_ids): + wait_for_copy = False + for map_id in mapping_ids: + attrs = self._get_flashcopy_mapping_attributes(map_id) + if not attrs: + continue + source = attrs['source_vdisk_name'] + target = attrs['target_vdisk_name'] + copy_rate = attrs['copy_rate'] + status = attrs['status'] + + if copy_rate == '0': + # Case #2: A vdisk that has snapshots + if source == name: + ssh_cmd = ('chfcmap -copyrate 50 -autodelete ' + 'on %s' % map_id) + out, err = self._run_ssh(ssh_cmd) + wait_for_copy = True + # Case #3: A snapshot + else: + msg = (_('Vdisk %(name)s not involved in ' + 'mapping %(src)s -> %(tgt)s') % + {'name': name, 'src': source, 'tgt': target}) + self._driver_assert(target == name, msg) + if status in ['copying', 'prepared']: + self._run_ssh('stopfcmap %s' % map_id) + elif status == 'stopping': + wait_for_copy = True + else: + self._run_ssh('rmfcmap -force %s' % map_id) + # Case 4: Copy in progress - wait and will autodelete + else: + if status == 'prepared': + self._run_ssh('stopfcmap %s' % map_id) + self._run_ssh('rmfcmap -force %s' % map_id) + elif status == 'idle_or_copied': + # Prepare failed + self._run_ssh('rmfcmap -force %s' % map_id) + else: + wait_for_copy = True + if wait_for_copy: + time.sleep(5) + mapping_ids = self._get_vdisk_fc_mappings(name) + + forceflag = '-force' if force else '' + ssh_cmd = 'rmvdisk %(frc)s %(name)s' % {'frc': forceflag, 'name': name} + out, err = self._run_ssh(ssh_cmd) + # No output should be returned from rmvdisk + self._assert_ssh_return(len(out.strip()) == 0, + ('_delete_vdisk %(name)s') + % {'name': name}, + ssh_cmd, out, err) + LOG.debug(_('leave: _delete_vdisk: vdisk %s') % name) - LOG.debug(_('leave: _create_new_host: host %(host)s with iSCSI ' - 'initiator %(init)s') % {'host': host_name, - 'init': initiator_name}) + def create_volume(self, volume): + opts = self._get_vdisk_params(volume['volume_type_id']) + return self._create_vdisk(volume['name'], str(volume['size']), 'gb', + opts) - return host_name + def delete_volume(self, volume): + self._delete_vdisk(volume['name'], False) - def _delete_host(self, host_name): - """Delete a host and associated iSCSI initiator name.""" + def create_snapshot(self, snapshot): + source_vol = self.db.volume_get(self._context, snapshot['volume_id']) + opts = self._get_vdisk_params(source_vol['volume_type_id']) + self._create_copy(src_vdisk=snapshot['volume_name'], + tgt_vdisk=snapshot['name'], + full_copy=False, + opts=opts, + src_id=snapshot['volume_id'], + from_vol=True) - LOG.debug(_('enter: _delete_host: host %s ') % host_name) + def delete_snapshot(self, snapshot): + self._delete_vdisk(snapshot['name'], False) - # 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) - # No output should be returned from rmhost - self._driver_assert( - len(out.strip()) == 0, - _('delete host %(name)s - non empty output from CLI.\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'name': host_name, - 'out': str(out), - 'err': str(err)}) - else: - LOG.info(_('warning: tried to delete host %(name)s but ' - 'it does not exist.') % {'name': host_name}) + def create_volume_from_snapshot(self, volume, snapshot): + if volume['size'] != snapshot['volume_size']: + exception_message = (_('create_volume_from_snapshot: ' + 'Source and destination size differ.')) + raise exception.VolumeBackendAPIException(data=exception_message) - LOG.debug(_('leave: _delete_host: host %s ') % host_name) + opts = self._get_vdisk_params(volume['volume_type_id']) + self._create_copy(src_vdisk=snapshot['name'], + tgt_vdisk=volume['name'], + full_copy=True, + opts=opts, + src_id=snapshot['id'], + from_vol=False) + + def create_cloned_volume(self, tgt_volume, src_volume): + if src_volume['size'] != tgt_volume['size']: + exception_message = (_('create_cloned_volume: ' + 'Source and destination size differ.')) + raise exception.VolumeBackendAPIException(data=exception_message) - 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 + opts = self._get_vdisk_params(tgt_volume['volume_type_id']) + self._create_copy(src_vdisk=src_volume['name'], + tgt_vdisk=tgt_volume['name'], + full_copy=True, + opts=opts, + src_id=src_volume['id'], + from_vol=True) + + def copy_image_to_volume(self, context, volume, image_service, image_id): + opts = self._get_vdisk_params(volume['volume_type_id']) + if opts['protocol'] == 'iscsi': + # Implemented in base iSCSI class + return super(StorwizeSVCDriver, self).copy_image_to_volume( + context, volume, image_service, image_id) else: - return True + raise NotImplementedError() + + def copy_volume_to_image(self, context, volume, image_service, image_meta): + opts = self._get_vdisk_params(volume['volume_type_id']) + if opts['protocol'] == 'iscsi': + # Implemented in base iSCSI class + return super(StorwizeSVCDriver, self).copy_volume_to_image( + context, volume, image_service, image_meta) + else: + raise NotImplementedError() - def _is_host_defined(self, host_name): - """Check if a host is defined on the storage.""" + """=====================================================================""" + """ MISC/HELPERS """ + """=====================================================================""" - LOG.debug(_('enter: _is_host_defined: host %s ') % host_name) + def get_volume_stats(self, refresh=False): + """Get volume status. - # 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 + If 'refresh' is True, run update the stats first.""" + if refresh: + self._update_volume_status() - 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)}) + return self._stats - 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 + def _update_volume_status(self): + """Retrieve status info from volume group.""" - LOG.debug(_('leave: _is_host_defined: host %(host)s with %(str)s ') % { - 'host': host_name, - 'str': host_name_from_storage is not None}) + LOG.debug(_("Updating volume status")) + data = {} - if host_name_from_storage is None: - return False - else: - return True + data['volume_backend_name'] = 'IBM_STORWIZE_SVC' # To be overwritten + data['vendor_name'] = 'IBM' + data['driver_version'] = '1.1' + data['storage_protocol'] = 'iSCSI' + data['storage_protocols'] = self._enabled_protocols - def _get_hostvdisk_mappings(self, host_name): - """Return the defined storage mappings for a host.""" + data['total_capacity_gb'] = 0 + data['free_capacity_gb'] = 0 + data['reserved_percentage'] = 0 + data['QoS_support'] = False - return_data = {} - ssh_cmd = 'lshostvdiskmap -delim ! %s' % host_name - out, err = self._run_ssh(ssh_cmd) - - 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.""" + pool = FLAGS.storwize_svc_volpool_name + #Get storage system name + ssh_cmd = 'lssystem -delim !' + attributes = self._execute_command_and_parse_attributes(ssh_cmd) + if not attributes or not attributes['name']: + exception_message = (_('_update_volume_status: ' + 'Could not get system name')) + raise exception.VolumeBackendAPIException(data=exception_message) - LOG.debug(_('enter: _map_vol_to_host: volume %(vol)s to ' - 'host %(host)s') - % {'vol': volume_name, - 'host': host_name}) + data['volume_backend_name'] = '%s_%s' % (attributes['name'], pool) - # Check if this volume is already mapped to this host - mapping_data = self._get_hostvdisk_mappings(host_name) + ssh_cmd = 'lsmdiskgrp -bytes -delim ! %s' % pool + attributes = self._execute_command_and_parse_attributes(ssh_cmd) + if not attributes: + LOG.error(_('Could not get pool data from the storage')) + exception_message = (_('_update_volume_status: ' + 'Could not get storage pool data')) + raise exception.VolumeBackendAPIException(data=exception_message) - 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) + data['total_capacity_gb'] = (float(attributes['capacity']) / + (1024 ** 3)) + data['free_capacity_gb'] = (float(attributes['free_capacity']) / + (1024 ** 3)) - # 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)}) + self._stats = data - 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}) + def _port_conf_generator(self, cmd): + ssh_cmd = '%s -delim !' % cmd + out, err = self._run_ssh(ssh_cmd) - return result_lun + if not len(out.strip()): + return + port_lines = out.strip().split('\n') + if not len(port_lines): + return - def _get_flashcopy_mapping_attributes(self, fc_map_id): - """Return the attributes of a FlashCopy mapping. + header = port_lines.pop(0) + yield header + for portip_line in port_lines: + try: + port_data = self._get_hdr_dic(header, portip_line, '!') + except exception.VolumeBackendAPIException: + with excutils.save_and_reraise_exception(): + self._log_cli_output_error('_port_conf_generator', + ssh_cmd, out, err) + yield port_data - 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). - """ + def _check_vdisk_opts(self, opts): + # Check that rsize is either -1 or between 0 and 100 + if not (opts['rsize'] >= -1 and opts['rsize'] <= 100): + raise exception.InvalidInput( + reason=_('Illegal value specified for storwize_svc_vol_rsize: ' + 'set to either a percentage (0-100) or -1')) - LOG.debug(_('enter: _get_flashcopy_mapping_attributes: mapping %s') - % fc_map_id) - # Get the lunid to be used + # Check that warning is either -1 or between 0 and 100 + if not (opts['warning'] >= -1 and opts['warning'] <= 100): + raise exception.InvalidInput( + reason=_('Illegal value specified for ' + 'storwize_svc_vol_warning: ' + 'set to a percentage (0-100)')) - 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)}) + # Check that grainsize is 32/64/128/256 + if opts['grainsize'] not in [32, 64, 128, 256]: + raise exception.InvalidInput( + reason=_('Illegal value specified for ' + 'storwize_svc_vol_grainsize: set to either ' + '32, 64, 128, or 256')) - # 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)}) + # Check that compression is supported + if opts['compression'] and not self._compression_enabled: + raise exception.InvalidInput( + reason=_('System does not support compression')) - if len(lines) == 2: - attributes = self._get_hdr_dic(lines[0], lines[1], '!') - else: # 0 or 1 lines - attributes = None + # Check that rsize is set if compression is set + if opts['compression'] and opts['rsize'] == -1: + raise exception.InvalidInput( + reason=_('If compression is set to True, rsize must ' + 'also be set (not equal to -1)')) - LOG.debug(_('leave: _get_flashcopy_mapping_attributes: mapping ' - '%(id)s, attributes %(attr)s') - % {'id': fc_map_id, - 'attr': attributes}) + # Check that the requested protocol is enabled + if not opts['protocol'] in self._enabled_protocols: + raise exception.InvalidInput( + reason=_('Illegal value %(prot)s specified for ' + 'storwize_svc_connection_protocol: ' + 'valid values are %(enabled)s') + % {'prot': opts['protocol'], + 'enabled': ','.join(self._enabled_protocols)}) + + # Check that multipath is only enabled for fc + if opts['protocol'] != 'fc' and opts['multipath']: + raise exception.InvalidInput( + reason=_('Multipath is currently only supported for FC ' + 'connections and not iSCSI. (This is a Nova ' + 'limitation.)')) - return attributes + def _execute_command_and_parse_attributes(self, ssh_cmd): + """Execute command on the Storwize/SVC and parse 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 the system + can not be obtained. - 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 + LOG.debug(_('enter: _execute_command_and_parse_attributes: ' + ' command %s') % ssh_cmd) 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 @@ -1280,71 +1385,69 @@ class StorwizeSVCDriver(san.SanISCSIDriver): '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)}) + self._assert_ssh_return(len(out), + '_execute_command_and_parse_attributes', + ssh_cmd, out, err) attributes = {} for attrib_line in out.split('\n'): # If '!' not found, 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: + if attrib_name is not None and len(attrib_name.strip()): attributes[attrib_name] = attrib_value - LOG.debug(_('leave: _get_volume_attributes:\n volume %(vol)s\n ' + LOG.debug(_('leave: _execute_command_and_parse_attributes:\n' + 'command: %(cmd)s\n' 'attributes: %(attr)s') - % {'vol': volume_name, + % {'cmd': ssh_cmd, 'attr': str(attributes)}) return attributes - def _get_chap_secret_for_host(self, host_name): - """Return the CHAP secret for the given host.""" - - LOG.debug(_('enter: _get_chap_secret_for_host: host name %s') - % host_name) - - ssh_cmd = 'lsiscsiauth -delim !' - out, err = self._run_ssh(ssh_cmd) - - if (len(out.strip()) == 0): - return None - - err_msg = _('_get_chap_secret_for_host: ' - '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) + def _get_hdr_dic(self, header, row, delim): + """Return CLI row data as a dictionary indexed by names from header. + string. The strings are converted to columns using the delimiter in + delim. + """ - header = host_lines.pop(0).split('!') - self._driver_assert('name' in header, err_msg) - self._driver_assert('iscsi_auth_method' in header, err_msg) - self._driver_assert('iscsi_chap_secret' in header, err_msg) - name_index = header.index('name') - method_index = header.index('iscsi_auth_method') - secret_index = header.index('iscsi_chap_secret') + 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 - chap_secret = None - host_found = False - for line in host_lines: - info = line.split('!') - if info[name_index] == host_name: - host_found = True - if info[method_index] == 'chap': - chap_secret = info[secret_index] + def _log_cli_output_error(self, function, cmd, out, err): + LOG.error(_('%(fun)s: Failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\nstdout: %(out)s\nstderr: %(err)s\n') + % {'fun': function, 'cmd': cmd, + 'out': str(out), 'err': str(err)}) - self._driver_assert(host_found is not False, err_msg) + 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) - LOG.debug(_('leave: _get_chap_secret_for_host: host name %(host)s ' - 'with secret %(secret)s') - % {'host': host_name, 'secret': chap_secret}) + def _assert_ssh_return(self, test, fun, ssh_cmd, out, err): + self._driver_assert( + test, + _('%(fun)s: Failed with unexpected CLI output.\n ' + 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') + % {'fun': fun, + 'cmd': ssh_cmd, + 'out': str(out), + 'err': str(err)}) - return chap_secret + def _handle_keyerror(self, function, header): + msg = (_('Did not find expected column in %(fun)s: %(hdr)s') % + {'fun': function, 'hdr': header}) + LOG.error(msg) + raise exception.VolumeBackendAPIException( + data=msg) -- 2.45.2