1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright (c) 2012 IBM, Inc.
4
# Copyright (c) 2012 OpenStack LLC.
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
20
# Ronen Kat <ronenkat@il.ibm.com>
21
# Avishay Traeger <avishay@il.ibm.com>
24
Volume driver for IBM Storwize V7000 and SVC storage systems.
27
1. If you specify both a password and a key file, this driver will use the
29
2. When using a key file for authentication, it is up to the user or
30
system administrator to store the private key in a safe manner.
31
3. The defaults for creating volumes are "-vtype striped -rsize 2% -autoexpand
32
-grainsize 256 -warning 0". These can be changed in the configuration file
33
(recommended only for advanced users).
36
1. The driver was not tested with SVC or clustered configurations of Storwize
38
2. The driver expects CLI output in English, error messages may be in a
47
from nova import exception
48
from nova import flags
49
from nova.openstack.common import cfg
50
from nova.openstack.common import excutils
51
from nova.openstack.common import log as logging
52
from nova import utils
53
from nova.volume import san
55
LOG = logging.getLogger(__name__)
58
cfg.StrOpt('storwize_svc_volpool_name',
60
help='Storage system storage pool for volumes'),
61
cfg.StrOpt('storwize_svc_vol_vtype',
63
help='Storage system volume type for volumes'),
64
cfg.StrOpt('storwize_svc_vol_rsize',
66
help='Storage system space-efficiency parameter for volumes'),
67
cfg.StrOpt('storwize_svc_vol_warning',
69
help='Storage system threshold for volume capacity warnings'),
70
cfg.BoolOpt('storwize_svc_vol_autoexpand',
72
help='Storage system autoexpand parameter for volumes '
74
cfg.StrOpt('storwize_svc_vol_grainsize',
76
help='Storage system grain size parameter for volumes '
78
cfg.BoolOpt('storwize_svc_vol_compression',
80
help='Storage system compression option for volumes'),
81
cfg.StrOpt('storwize_svc_flashcopy_timeout',
83
help='Maximum number of seconds to wait for FlashCopy to be'
84
'prepared. Maximum value is 600 seconds (10 minutes).'),
88
FLAGS.register_opts(storwize_svc_opts)
91
class StorwizeSVCDriver(san.SanISCSIDriver):
92
"""IBM Storwize V7000 and SVC iSCSI volume driver."""
94
def __init__(self, *args, **kwargs):
95
super(StorwizeSVCDriver, self).__init__(*args, **kwargs)
96
self.iscsi_ipv4_conf = None
97
self.iscsi_ipv6_conf = None
99
# Build cleanup transaltion tables for hosts names to follow valid
100
# host names for Storwizew V7000 and SVC storage systems.
101
invalid_ch_in_host = ''
102
for num in range(0, 128):
104
if ((not ch.isalnum()) and (ch != ' ') and (ch != '.')
105
and (ch != '-') and (ch != '_')):
106
invalid_ch_in_host = invalid_ch_in_host + ch
107
self._string_host_name_filter = string.maketrans(invalid_ch_in_host,
108
'-' * len(invalid_ch_in_host))
110
self._unicode_host_name_filter = dict((ord(unicode(char)), u'-')
111
for char in invalid_ch_in_host)
113
def _get_hdr_dic(self, header, row, delim):
114
"""Return CLI row data as a dictionary indexed by names from header.
116
Create a dictionary object from the data row string using the header
117
string. The strings are converted to columns using the delimiter in
121
attributes = header.split(delim)
122
values = row.split(delim)
123
self._driver_assert(len(values) == len(attributes),
124
_('_get_hdr_dic: attribute headers and values do not match.\n '
125
'Headers: %(header)s\n Values: %(row)s')
126
% {'header': str(header),
129
for attribute, value in map(None, attributes, values):
130
dic[attribute] = value
133
def _driver_assert(self, assert_condition, exception_message):
134
"""Internal assertion mechanism for CLI output."""
135
if not assert_condition:
136
LOG.error(exception_message)
137
raise exception.VolumeBackendAPIException(data=exception_message)
139
def check_for_setup_error(self):
140
"""Check that we have all configuration details from the storage."""
142
LOG.debug(_('enter: check_for_setup_error'))
144
# Validate that the pool exists
145
ssh_cmd = 'lsmdiskgrp -delim ! -nohdr'
146
out, err = self._run_ssh(ssh_cmd)
147
self._driver_assert(len(out) > 0,
148
_('check_for_setup_error: failed with unexpected CLI output.\n '
149
'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s')
153
search_text = '!%s!' % getattr(FLAGS, 'storwize_svc_volpool_name')
154
if search_text not in out:
155
raise exception.InvalidParameterValue(
156
err=_('pool %s doesn\'t exist')
157
% getattr(FLAGS, 'storwize_svc_volpool_name'))
160
# Get the iSCSI names of the Storwize/SVC nodes
161
ssh_cmd = 'svcinfo lsnode -delim !'
162
out, err = self._run_ssh(ssh_cmd)
163
self._driver_assert(len(out) > 0,
164
_('check_for_setup_error: failed with unexpected CLI output.\n '
165
'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s')
170
nodes = out.strip().split('\n')
171
self._driver_assert(len(nodes) > 0,
172
_('check_for_setup_error: failed with unexpected CLI output.\n '
173
'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s')
177
header = nodes.pop(0)
178
for node_line in nodes:
180
node_data = self._get_hdr_dic(header, node_line, '!')
181
except exception.VolumeBackendAPIException as e:
182
with excutils.save_and_reraise_exception():
183
LOG.error(_('check_for_setup_error: '
184
'failed with unexpected CLI output.\n '
185
'Command: %(cmd)s\n '
186
'stdout: %(out)s\n stderr: %(err)s\n')
192
node['id'] = node_data['id']
193
node['name'] = node_data['name']
194
node['iscsi_name'] = node_data['iscsi_name']
195
node['status'] = node_data['status']
198
if node['iscsi_name'] != '':
199
storage_nodes[node['id']] = node
200
except KeyError as e:
201
LOG.error(_('Did not find expected column name in '
202
'svcinfo lsnode: %s') % str(e))
203
exception_message = (
204
_('check_for_setup_error: Unexpected CLI output.\n '
206
'Command: %(cmd)s\n '
207
'stdout: %(out)s\n stderr: %(err)s')
212
raise exception.VolumeBackendAPIException(
213
data=exception_message)
215
# Get the iSCSI IP addresses of the Storwize/SVC nodes
216
ssh_cmd = 'lsportip -delim !'
217
out, err = self._run_ssh(ssh_cmd)
218
self._driver_assert(len(out) > 0,
219
_('check_for_setup_error: failed with unexpected CLI output.\n '
220
'Command: %(cmd)s\n '
221
'stdout: %(out)s\n stderr: %(err)s')
226
portips = out.strip().split('\n')
227
self._driver_assert(len(portips) > 0,
228
_('check_for_setup_error: failed with unexpected CLI output.\n '
229
'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s')
233
header = portips.pop(0)
234
for portip_line in portips:
236
port_data = self._get_hdr_dic(header, portip_line, '!')
237
except exception.VolumeBackendAPIException as e:
238
with excutils.save_and_reraise_exception():
239
LOG.error(_('check_for_setup_error: '
240
'failed with unexpected CLI output.\n '
241
'Command: %(cmd)s\n '
242
'stdout: %(out)s\n stderr: %(err)s\n')
247
port_node_id = port_data['node_id']
248
port_ipv4 = port_data['IP_address']
249
port_ipv6 = port_data['IP_address_6']
250
except KeyError as e:
251
LOG.error(_('Did not find expected column name in '
252
'lsportip: %s') % str(e))
253
exception_message = (
254
_('check_for_setup_error: Unexpected CLI output.\n '
256
'Command: %(cmd)s\n '
257
'stdout: %(out)s\n stderr: %(err)s')
262
raise exception.VolumeBackendAPIException(
263
data=exception_message)
265
if port_node_id in storage_nodes:
266
node = storage_nodes[port_node_id]
267
if len(port_ipv4) > 0:
268
node['ipv4'].append(port_ipv4)
269
if len(port_ipv6) > 0:
270
node['ipv6'].append(port_ipv6)
272
raise exception.VolumeBackendAPIException(
273
data=_('check_for_setup_error: '
274
'fail to storage configuration: unknown '
275
'storage node %(node_id)s from CLI output.\n '
276
'stdout: %(out)s\n stderr: %(err)s\n')
277
% {'node_id': port_node_id,
283
for node_key in storage_nodes:
284
node = storage_nodes[node_key]
285
if 'ipv4' in node and len(node['iscsi_name']) > 0:
286
iscsi_ipv4_conf.append({'iscsi_name': node['iscsi_name'],
288
'node_id': node['id']})
289
if 'ipv6' in node and len(node['iscsi_name']) > 0:
290
iscsi_ipv6_conf.append({'iscsi_name': node['iscsi_name'],
292
'node_id': node['id']})
293
if (len(node['ipv4']) == 0) and (len(node['ipv6']) == 0):
294
raise exception.VolumeBackendAPIException(
295
data=_('check_for_setup_error: '
296
'fail to storage configuration: storage '
297
'node %s has no IP addresses configured')
300
# Make sure we have at least one IPv4 address with a iSCSI name
301
# TODO(ronenkat) need to expand this to support IPv6
302
self._driver_assert(len(iscsi_ipv4_conf) > 0,
303
_('could not obtain IP address and iSCSI name from the storage. '
304
'Please verify that the storage is configured for iSCSI.\n '
305
'Storage nodes: %(nodes)s\n portips: %(portips)s')
306
% {'nodes': nodes, 'portips': portips})
308
self.iscsi_ipv4_conf = iscsi_ipv4_conf
309
self.iscsi_ipv6_conf = iscsi_ipv6_conf
311
LOG.debug(_('leave: check_for_setup_error'))
313
def _check_num_perc(self, value):
314
"""Return True if value is either a number or a percentage."""
315
if value.endswith('%'):
317
return value.isdigit()
319
def _check_flags(self):
320
"""Ensure that the flags are set properly."""
322
required_flags = ['san_ip', 'san_ssh_port', 'san_login',
323
'storwize_svc_volpool_name']
324
for flag in required_flags:
325
if not getattr(FLAGS, flag, None):
326
raise exception.InvalidParameterValue(
327
err=_('%s is not set') % flag)
329
# Ensure that either password or keyfile were set
330
if not (getattr(FLAGS, 'san_password', None)
331
or getattr(FLAGS, 'san_private_key', None)):
332
raise exception.InvalidParameterValue(
333
err=_('Password or SSH private key is required for '
334
'authentication: set either san_password or '
335
'san_private_key option'))
337
# vtype should either be 'striped' or 'seq'
338
vtype = getattr(FLAGS, 'storwize_svc_vol_vtype')
339
if vtype not in ['striped', 'seq']:
340
raise exception.InvalidParameterValue(
341
err=_('Illegal value specified for storwize_svc_vol_vtype: '
342
'set to either \'striped\' or \'seq\''))
344
# Check that rsize is a number or percentage
345
rsize = getattr(FLAGS, 'storwize_svc_vol_rsize')
346
if not self._check_num_perc(rsize) and (rsize not in ['auto', '-1']):
347
raise exception.InvalidParameterValue(
348
err=_('Illegal value specified for storwize_svc_vol_rsize: '
349
'set to either a number or a percentage'))
351
# Check that warning is a number or percentage
352
warning = getattr(FLAGS, 'storwize_svc_vol_warning')
353
if not self._check_num_perc(warning):
354
raise exception.InvalidParameterValue(
355
err=_('Illegal value specified for storwize_svc_vol_warning: '
356
'set to either a number or a percentage'))
358
# Check that autoexpand is a boolean
359
autoexpand = getattr(FLAGS, 'storwize_svc_vol_autoexpand')
360
if type(autoexpand) != type(True):
361
raise exception.InvalidParameterValue(
362
err=_('Illegal value specified for '
363
'storwize_svc_vol_autoexpand: set to either '
366
# Check that grainsize is 32/64/128/256
367
grainsize = getattr(FLAGS, 'storwize_svc_vol_grainsize')
368
if grainsize not in ['32', '64', '128', '256']:
369
raise exception.InvalidParameterValue(
370
err=_('Illegal value specified for '
371
'storwize_svc_vol_grainsize: set to either '
372
'\'32\', \'64\', \'128\', or \'256\''))
374
# Check that flashcopy_timeout is numeric and 32/64/128/256
375
flashcopy_timeout = getattr(FLAGS, 'storwize_svc_flashcopy_timeout')
376
if not (flashcopy_timeout.isdigit() and int(flashcopy_timeout) > 0 and
377
int(flashcopy_timeout) <= 600):
378
raise exception.InvalidParameterValue(
379
err=_('Illegal value %s specified for '
380
'storwize_svc_flashcopy_timeout: '
381
'valid values are between 0 and 600')
384
# Check that compression is a boolean
385
volume_compression = getattr(FLAGS, 'storwize_svc_vol_compression')
386
if type(volume_compression) != type(True):
387
raise exception.InvalidInput(
388
reason=_('Illegal value specified for '
389
'storwize_svc_vol_compression: set to either '
392
def do_setup(self, context):
393
"""Validate the flags."""
394
LOG.debug(_('enter: do_setup'))
396
LOG.debug(_('leave: do_setup'))
398
def create_volume(self, volume):
399
"""Create a new volume - uses the internal method."""
400
return self._create_volume(volume, units='gb')
402
def _create_volume(self, volume, units='gb'):
403
"""Create a new volume."""
405
default_size = '1' # 1GB
406
name = volume['name']
409
LOG.debug(_('enter: create_volume: volume %s ') % name)
411
if int(volume['size']) > 0:
412
size = int(volume['size'])
416
if getattr(FLAGS, 'storwize_svc_vol_autoexpand'):
417
autoexpand = '-autoexpand'
421
# Set space-efficient options
422
if getattr(FLAGS, 'storwize_svc_vol_rsize').strip() == '-1':
425
ssh_cmd_se_opt = ('-rsize %(rsize)s %(autoexpand)s ' %
426
{'rsize': getattr(FLAGS, 'storwize_svc_vol_rsize'),
427
'autoexpand': autoexpand})
428
if getattr(FLAGS, 'storwize_svc_vol_compression'):
429
ssh_cmd_se_opt = ssh_cmd_se_opt + '-compressed'
431
ssh_cmd_se_opt = ssh_cmd_se_opt + ('-grainsize %(grain)s' %
432
{'grain': getattr(FLAGS, 'storwize_svc_vol_grainsize')})
434
ssh_cmd = ('mkvdisk -name %(name)s -mdiskgrp %(mdiskgrp)s '
435
'-iogrp 0 -vtype %(vtype)s -size %(size)s -unit '
436
'%(unit)s %(ssh_cmd_se_opt)s'
438
'mdiskgrp': getattr(FLAGS, 'storwize_svc_volpool_name'),
439
'vtype': getattr(FLAGS, 'storwize_svc_vol_vtype'),
440
'size': size, 'unit': units,
441
'ssh_cmd_se_opt': ssh_cmd_se_opt})
442
out, err = self._run_ssh(ssh_cmd)
443
self._driver_assert(len(out.strip()) > 0,
444
_('create volume %(name)s - did not find '
445
'success message in CLI output.\n '
446
'stdout: %(out)s\n stderr: %(err)s')
447
% {'name': name, 'out': str(out), 'err': str(err)})
449
# Ensure that the output is as expected
450
match_obj = re.search('Virtual Disk, id \[([0-9]+)\], '
451
'successfully created', out)
452
# Make sure we got a "successfully created" message with vdisk id
453
self._driver_assert(match_obj is not None,
454
_('create volume %(name)s - did not find '
455
'success message in CLI output.\n '
456
'stdout: %(out)s\n stderr: %(err)s')
457
% {'name': name, 'out': str(out), 'err': str(err)})
459
LOG.debug(_('leave: create_volume: volume %(name)s ') % {'name': name})
461
def delete_volume(self, volume):
462
self._delete_volume(volume, False)
464
def _delete_volume(self, volume, force_opt):
465
"""Driver entry point for destroying existing volumes."""
467
name = volume['name']
468
LOG.debug(_('enter: delete_volume: volume %(name)s ') % {'name': name})
471
force_flag = '-force'
475
volume_defined = self._is_volume_defined(name)
476
# Try to delete volume only if found on the storage
478
out, err = self._run_ssh('rmvdisk %(force)s %(name)s'
479
% {'force': force_flag,
481
# No output should be returned from rmvdisk
482
self._driver_assert(len(out.strip()) == 0,
483
_('delete volume %(name)s - non empty output from CLI.\n '
484
'stdout: %(out)s\n stderr: %(err)s')
489
# Log that volume does not exist
490
LOG.info(_('warning: tried to delete volume %(name)s but '
491
'it does not exist.') % {'name': name})
493
LOG.debug(_('leave: delete_volume: volume %(name)s ') % {'name': name})
495
def ensure_export(self, context, volume):
496
"""Check that the volume exists on the storage.
498
The system does not "export" volumes as a Linux iSCSI target does,
499
and therefore we just check that the volume exists on the storage.
501
volume_defined = self._is_volume_defined(volume['name'])
502
if not volume_defined:
503
LOG.error(_('ensure_export: volume %s not found on storage')
506
def create_export(self, context, volume):
510
def remove_export(self, context, volume):
513
def check_for_export(self, context, volume_id):
514
raise NotImplementedError()
516
def initialize_connection(self, volume, connector):
517
"""Perform the necessary work so that an iSCSI connection can be made.
519
To be able to create an iSCSI connection from a given iSCSI name to a
521
1. Translate the given iSCSI name to a host name
522
2. Create new host on the storage system if it does not yet exist
523
2. Map the volume to the host if it is not already done
524
3. Return iSCSI properties, including the IP address of the preferred
525
node for this volume and the LUN number.
527
LOG.debug(_('enter: initialize_connection: volume %(vol)s with '
528
'connector %(conn)s') % {'vol': str(volume),
529
'conn': str(connector)})
531
initiator_name = connector['initiator']
532
volume_name = volume['name']
534
host_name = self._get_host_from_iscsiname(initiator_name)
535
# Check if a host is defined for the iSCSI initiator name
536
if host_name is None:
537
# Host does not exist - add a new host to Storwize/SVC
538
host_name = self._create_new_host('host%s' % initiator_name,
540
# Verify that create_new_host succeeded
541
self._driver_assert(host_name is not None,
542
_('_create_new_host failed to return the host name.'))
544
lun_id = self._map_vol_to_host(volume_name, host_name)
547
# Only IPv4 for now because lack of OpenStack support
548
# TODO(ronenkat): Add support for IPv6
549
volume_attributes = self._get_volume_attributes(volume_name)
550
if (volume_attributes is not None and
551
'preferred_node_id' in volume_attributes):
552
preferred_node = volume_attributes['preferred_node_id']
553
preferred_node_entry = None
554
for node in self.iscsi_ipv4_conf:
555
if node['node_id'] == preferred_node:
556
preferred_node_entry = node
558
if preferred_node_entry is None:
559
preferred_node_entry = self.iscsi_ipv4_conf[0]
560
LOG.error(_('initialize_connection: did not find preferred '
561
'node %(node)s for volume %(vol)s in iSCSI '
562
'configuration') % {'node': preferred_node,
566
preferred_node_entry = self.iscsi_ipv4_conf[0]
568
_('initialize_connection: did not find a preferred node '
569
'for volume %s in iSCSI configuration') % volume_name)
572
# We didn't use iSCSI discover, as in server-based iSCSI
573
properties['target_discovered'] = False
574
# We take the first IP address for now. Ideally, OpenStack will
575
# support multipath for improved performance.
576
properties['target_portal'] = ('%s:%s' %
577
(preferred_node_entry['ip'][0], '3260'))
578
properties['target_iqn'] = preferred_node_entry['iscsi_name']
579
properties['target_lun'] = lun_id
580
properties['volume_id'] = volume['id']
582
LOG.debug(_('leave: initialize_connection:\n volume: %(vol)s\n '
583
'connector %(conn)s\n properties: %(prop)s')
584
% {'vol': str(volume),
585
'conn': str(connector),
586
'prop': str(properties)})
588
return {'driver_volume_type': 'iscsi', 'data': properties, }
590
def terminate_connection(self, volume, connector):
591
"""Cleanup after an iSCSI connection has been terminated.
593
When we clean up a terminated connection between a given iSCSI name
595
1. Translate the given iSCSI name to a host name
596
2. Remove the volume-to-host mapping if it exists
597
3. Delete the host if it has no more mappings (hosts are created
598
automatically by this driver when mappings are created)
600
LOG.debug(_('enter: terminate_connection: volume %(vol)s with '
601
'connector %(conn)s') % {'vol': str(volume),
602
'conn': str(connector)})
604
vol_name = volume['name']
605
initiator_name = connector['initiator']
606
host_name = self._get_host_from_iscsiname(initiator_name)
607
# Verify that _get_host_from_iscsiname returned the host.
608
# This should always succeed as we terminate an existing connection.
609
self._driver_assert(host_name is not None,
610
_('_get_host_from_iscsiname failed to return the host name '
611
'for iscsi name %s') % initiator_name)
613
# Check if vdisk-host mapping exists, remove if it does
614
mapping_data = self._get_hostvdisk_mappings(host_name)
615
if vol_name in mapping_data:
616
out, err = self._run_ssh('rmvdiskhostmap -host %s %s'
617
% (host_name, vol_name))
618
# Verify CLI behaviour - no output is returned from
620
self._driver_assert(len(out.strip()) == 0,
621
_('delete mapping of volume %(vol)s to host %(host)s '
622
'- non empty output from CLI.\n '
623
'stdout: %(out)s\n stderr: %(err)s')
628
del mapping_data[vol_name]
630
LOG.error(_('terminate_connection: no mapping of volume '
631
'%(vol)s to host %(host)s found') %
632
{'vol': vol_name, 'host': host_name})
634
# If this host has no more mappings, delete it
636
self._delete_host(host_name)
638
LOG.debug(_('leave: terminate_connection: volume %(vol)s with '
639
'connector %(conn)s') % {'vol': str(volume),
640
'conn': str(connector)})
642
def _flashcopy_cleanup(self, fc_map_id, source, target):
643
"""Clean up a failed FlashCopy operation."""
646
out, err = self._run_ssh('stopfcmap -force %s' % fc_map_id)
647
out, err = self._run_ssh('rmfcmap -force %s' % fc_map_id)
648
except exception.ProcessExecutionError as e:
649
LOG.error(_('_run_flashcopy: fail to cleanup failed FlashCopy '
650
'mapping %(fc_map_id)% '
651
'from %(source)s to %(target)s.\n'
652
'stdout: %(out)s\n stderr: %(err)s')
653
% {'fc_map_id': fc_map_id,
659
def _run_flashcopy(self, source, target):
660
"""Create a FlashCopy mapping from the source to the target."""
663
_('enter: _run_flashcopy: execute FlashCopy from source '
664
'%(source)s to target %(target)s') % {'source': source,
667
fc_map_cli_cmd = ('mkfcmap -source %s -target %s -autodelete '
668
'-cleanrate 0' % (source, target))
669
out, err = self._run_ssh(fc_map_cli_cmd)
670
self._driver_assert(len(out.strip()) > 0,
671
_('create FC mapping from %(source)s to %(target)s - '
672
'did not find success message in CLI output.\n'
673
' stdout: %(out)s\n stderr: %(err)s\n')
679
# Ensure that the output is as expected
680
match_obj = re.search('FlashCopy Mapping, id \[([0-9]+)\], '
681
'successfully created', out)
682
# Make sure we got a "successfully created" message with vdisk id
683
self._driver_assert(match_obj is not None,
684
_('create FC mapping from %(source)s to %(target)s - '
685
'did not find success message in CLI output.\n'
686
' stdout: %(out)s\n stderr: %(err)s\n')
693
fc_map_id = match_obj.group(1)
694
self._driver_assert(fc_map_id is not None,
695
_('create FC mapping from %(source)s to %(target)s - '
696
'did not find mapping id in CLI output.\n'
697
' stdout: %(out)s\n stderr: %(err)s\n')
703
self._driver_assert(False,
704
_('create FC mapping from %(source)s to %(target)s - '
705
'did not find mapping id in CLI output.\n'
706
' stdout: %(out)s\n stderr: %(err)s\n')
712
out, err = self._run_ssh('prestartfcmap %s' % fc_map_id)
713
except exception.ProcessExecutionError as e:
714
with excutils.save_and_reraise_exception():
715
LOG.error(_('_run_flashcopy: fail to prepare FlashCopy '
716
'from %(source)s to %(target)s.\n'
717
'stdout: %(out)s\n stderr: %(err)s')
722
self._flashcopy_cleanup(fc_map_id, source, target)
724
mapping_ready = False
726
# Allow waiting of up to timeout (set as parameter)
727
max_retries = (int(getattr(FLAGS,
728
'storwize_svc_flashcopy_timeout')) / wait_time) + 1
729
for try_number in range(1, max_retries):
730
mapping_attributes = self._get_flashcopy_mapping_attributes(
732
if (mapping_attributes is None or
733
'status' not in mapping_attributes):
735
if mapping_attributes['status'] == 'prepared':
738
elif mapping_attributes['status'] != 'preparing':
739
# Unexpected mapping status
740
exception_msg = (_('unexecpted mapping status %(status)s '
741
'for mapping %(id)s. Attributes: '
743
% {'status': mapping_attributes['status'],
745
'attr': mapping_attributes})
746
raise exception.VolumeBackendAPIException(
748
# Need to wait for mapping to be prepared, wait a few seconds
749
time.sleep(wait_time)
751
if not mapping_ready:
752
exception_msg = (_('mapping %(id)s prepare failed to complete '
753
'within the alloted %(to)s seconds timeout. '
754
'Terminating') % {'id': fc_map_id,
756
FLAGS, 'storwize_svc_flashcopy_timeout')})
757
LOG.error(_('_run_flashcopy: fail to start FlashCopy '
758
'from %(source)s to %(target)s with '
762
'ex': exception_msg})
763
self._flashcopy_cleanup(fc_map_id, source, target)
764
raise exception.InvalidSnapshot(
765
reason=_('_run_flashcopy: %s') % exception_msg)
768
out, err = self._run_ssh('startfcmap %s' % fc_map_id)
769
except exception.ProcessExecutionError as e:
770
with excutils.save_and_reraise_exception():
771
LOG.error(_('_run_flashcopy: fail to start FlashCopy '
772
'from %(source)s to %(target)s.\n'
773
'stdout: %(out)s\n stderr: %(err)s')
778
self._flashcopy_cleanup(fc_map_id, source, target)
780
LOG.debug(_('leave: _run_flashcopy: FlashCopy started from '
781
'%(source)s to %(target)s') % {'source': source,
784
def create_volume_from_snapshot(self, volume, snapshot):
785
"""Create a new snapshot from volume."""
787
source_volume = snapshot['name']
788
tgt_volume = volume['name']
790
LOG.debug(_('enter: create_volume_from_snapshot: snapshot %(tgt)s '
791
'from volume %(src)s') % {'tgt': tgt_volume,
792
'src': source_volume})
794
src_volume_attributes = self._get_volume_attributes(source_volume)
795
if src_volume_attributes is None:
796
exception_msg = (_('create_volume_from_snapshot: source volume %s '
797
'does not exist') % source_volume)
798
LOG.error(exception_msg)
799
raise exception.SnapshotNotFound(exception_msg,
800
volume_id=source_volume)
802
self._driver_assert('capacity' in src_volume_attributes,
803
_('create_volume_from_snapshot: cannot get source '
804
'volume %(src)s capacity from volume attributes '
805
'%(attr)s') % {'src': source_volume,
806
'attr': src_volume_attributes})
807
src_volume_size = src_volume_attributes['capacity']
809
tgt_volume_attributes = self._get_volume_attributes(tgt_volume)
810
# Does the snapshot target exist?
811
if tgt_volume_attributes is not None:
812
exception_msg = (_('create_volume_from_snapshot: target volume %s '
813
'already exists, cannot create') % tgt_volume)
814
LOG.error(exception_msg)
815
raise exception.InvalidSnapshot(reason=exception_msg)
818
snapshot_volume['name'] = tgt_volume
819
snapshot_volume['size'] = src_volume_size
821
self._create_volume(snapshot_volume, units='b')
824
self._run_flashcopy(source_volume, tgt_volume)
826
with excutils.save_and_reraise_exception():
827
# Clean up newly-created snapshot if the FlashCopy failed
828
self._delete_volume(snapshot_volume, True)
831
_('leave: create_volume_from_snapshot: %s created successfully')
834
def create_snapshot(self, snapshot):
835
"""Create a new snapshot using FlashCopy."""
837
src_volume = snapshot['volume_name']
838
tgt_volume = snapshot['name']
840
# Flag to keep track of created volumes in case FlashCopy
841
tgt_volume_created = False
843
LOG.debug(_('enter: create_snapshot: snapshot %(tgt)s from '
844
'volume %(src)s') % {'tgt': tgt_volume,
847
src_volume_attributes = self._get_volume_attributes(src_volume)
848
if src_volume_attributes is None:
850
_('create_snapshot: source volume %s does not exist')
852
LOG.error(exception_msg)
853
raise exception.VolumeNotFound(exception_msg,
854
volume_id=src_volume)
856
self._driver_assert('capacity' in src_volume_attributes,
857
_('create_volume_from_snapshot: cannot get source '
858
'volume %(src)s capacity from volume attributes '
859
'%(attr)s') % {'src': src_volume,
860
'attr': src_volume_attributes})
862
source_volume_size = src_volume_attributes['capacity']
864
tgt_volume_attributes = self._get_volume_attributes(tgt_volume)
865
# Does the snapshot target exist?
867
if tgt_volume_attributes is None:
868
# No, create a new snapshot volume
869
snapshot_volume['name'] = tgt_volume
870
snapshot_volume['size'] = source_volume_size
871
self._create_volume(snapshot_volume, units='b')
872
tgt_volume_created = True
874
# Yes, target exists, verify exact same size as source
875
self._driver_assert('capacity' in tgt_volume_attributes,
876
_('create_volume_from_snapshot: cannot get source '
877
'volume %(src)s capacity from volume attributes '
878
'%(attr)s') % {'src': tgt_volume,
879
'attr': tgt_volume_attributes})
880
target_volume_size = tgt_volume_attributes['capacity']
881
if target_volume_size != source_volume_size:
883
_('create_snapshot: source %(src)s and target '
884
'volume %(tgt)s have different capacities '
885
'(source:%(ssize)s target:%(tsize)s)') %
888
'ssize': source_volume_size,
889
'tsize': target_volume_size})
890
LOG.error(exception_msg)
891
raise exception.InvalidSnapshot(reason=exception_msg)
894
self._run_flashcopy(src_volume, tgt_volume)
895
except exception.InvalidSnapshot:
896
with excutils.save_and_reraise_exception():
897
# Clean up newly-created snapshot if the FlashCopy failed
898
if tgt_volume_created:
899
self._delete_volume(snapshot_volume, True)
901
LOG.debug(_('leave: create_snapshot: %s created successfully')
904
def delete_snapshot(self, snapshot):
905
self._delete_snapshot(snapshot, False)
907
def _delete_snapshot(self, snapshot, force_opt):
908
"""Delete a snapshot from the storage."""
909
LOG.debug(_('enter: delete_snapshot: snapshot %s') % snapshot)
911
snapshot_defined = self._is_volume_defined(snapshot['name'])
914
self._delete_volume(snapshot, force_opt)
916
self.delete_volume(snapshot)
918
LOG.debug(_('leave: delete_snapshot: snapshot %s') % snapshot)
920
def _get_host_from_iscsiname(self, iscsi_name):
921
"""List the hosts defined in the storage.
923
Return the host name with the given iSCSI name, or None if there is
924
no host name with that iSCSI name.
927
LOG.debug(_('enter: _get_host_from_iscsiname: iSCSI initiator %s')
930
# Get list of host in the storage
931
ssh_cmd = 'lshost -delim !'
932
out, err = self._run_ssh(ssh_cmd)
934
if (len(out.strip()) == 0):
937
err_msg = _('_get_host_from_iscsiname: '
938
'failed with unexpected CLI output.\n'
939
' command: %(cmd)s\n stdout: %(out)s\n '
940
'stderr: %(err)s') % {'cmd': ssh_cmd,
943
host_lines = out.strip().split('\n')
944
self._driver_assert(len(host_lines) > 0, err_msg)
945
header = host_lines.pop(0).split('!')
946
self._driver_assert('name' in header, err_msg)
947
name_index = header.index('name')
949
hosts = map(lambda x: x.split('!')[name_index], host_lines)
952
# For each host, get its details and check for its iSCSI name
954
ssh_cmd = 'lshost -delim ! %s' % host
955
out, err = self._run_ssh(ssh_cmd)
956
self._driver_assert(len(out) > 0,
957
_('_get_host_from_iscsiname: '
958
'Unexpected response from CLI output. '
959
'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s')
963
for attrib_line in out.split('\n'):
964
# If '!' not found, return the string and two empty strings
965
attrib_name, foo, attrib_value = attrib_line.partition('!')
966
if attrib_name == 'iscsi_name':
967
if iscsi_name == attrib_value:
970
if hostname is not None:
973
LOG.debug(_('leave: _get_host_from_iscsiname: iSCSI initiator %s')
978
def _create_new_host(self, host_name, initiator_name):
979
"""Create a new host on the storage system.
981
We modify the given host name, replace any invalid characters and
982
adding a random suffix to avoid conflicts due to the translation. The
983
host is associated with the given iSCSI initiator name.
986
LOG.debug(_('enter: _create_new_host: host %(name)s with iSCSI '
987
'initiator %(init)s') % {'name': host_name,
988
'init': initiator_name})
990
if isinstance(host_name, unicode):
991
host_name = host_name.translate(self._unicode_host_name_filter)
992
elif isinstance(host_name, str):
993
host_name = host_name.translate(self._string_host_name_filter)
995
msg = _('_create_new_host: cannot clean host name. Host name '
996
'is not unicode or string')
998
raise exception.NoValidHost(reason=msg)
1000
# Add 5 digit random suffix to the host name to avoid
1001
# conflicts in host names after removing invalid characters
1002
# for Storwize/SVC names
1003
host_name = '%s_%s' % (host_name, random.randint(10000, 99999))
1004
out, err = self._run_ssh('mkhost -name "%s" -iscsiname "%s"'
1005
% (host_name, initiator_name))
1006
self._driver_assert(len(out.strip()) > 0 and
1007
'successfully created' in out,
1008
_('create host %(name)s with iSCSI initiator %(init)s - '
1009
'did not find success message in CLI output.\n '
1010
'stdout: %(out)s\n stderr: %(err)s\n')
1011
% {'name': host_name,
1012
'init': initiator_name,
1016
LOG.debug(_('leave: _create_new_host: host %(host)s with iSCSI '
1017
'initiator %(init)s') % {'host': host_name,
1018
'init': initiator_name})
1022
def _delete_host(self, host_name):
1023
"""Delete a host and associated iSCSI initiator name."""
1025
LOG.debug(_('enter: _delete_host: host %s ') % host_name)
1027
# Check if host exists on system, expect to find the host
1028
is_defined = self._is_host_defined(host_name)
1031
out, err = self._run_ssh('rmhost %s ' % host_name)
1033
LOG.info(_('warning: tried to delete host %(name)s but '
1034
'it does not exist.') % {'name': host_name})
1036
LOG.debug(_('leave: _delete_host: host %s ') % host_name)
1038
def _is_volume_defined(self, volume_name):
1039
"""Check if volume is defined."""
1040
LOG.debug(_('enter: _is_volume_defined: volume %s ') % volume_name)
1041
volume_attributes = self._get_volume_attributes(volume_name)
1042
LOG.debug(_('leave: _is_volume_defined: volume %(vol)s with %(str)s ')
1043
% {'vol': volume_name,
1044
'str': volume_attributes is not None})
1045
if volume_attributes is None:
1050
def _is_host_defined(self, host_name):
1051
"""Check if a host is defined on the storage."""
1053
LOG.debug(_('enter: _is_host_defined: host %s ') % host_name)
1055
# Get list of hosts with the name %host_name%
1056
# We expect zero or one line if host does not exist,
1057
# two lines if it does exist, otherwise error
1058
out, err = self._run_ssh('lshost -filtervalue name=%s -delim !'
1060
if len(out.strip()) == 0:
1063
lines = out.strip().split('\n')
1064
self._driver_assert(len(lines) <= 2,
1065
_('_is_host_defined: Unexpected response from CLI output.\n '
1066
'stdout: %(out)s\n stderr: %(err)s\n')
1071
host_info = self._get_hdr_dic(lines[0], lines[1], '!')
1072
host_name_from_storage = host_info['name']
1073
# Make sure we got the data for the right host
1074
self._driver_assert(host_name_from_storage == host_name,
1075
_('Data received for host %(host1)s instead of host '
1077
'stdout: %(out)s\n stderr: %(err)s\n')
1078
% {'host1': host_name_from_storage,
1082
else: # 0 or 1 lines
1083
host_name_from_storage = None
1085
LOG.debug(_('leave: _is_host_defined: host %(host)s with %(str)s ') % {
1087
'str': host_name_from_storage is not None})
1089
if host_name_from_storage is None:
1094
def _get_hostvdisk_mappings(self, host_name):
1095
"""Return the defined storage mappings for a host."""
1098
ssh_cmd = 'lshostvdiskmap -delim ! %s' % host_name
1099
out, err = self._run_ssh(ssh_cmd)
1101
mappings = out.strip().split('\n')
1102
if len(mappings) > 0:
1103
header = mappings.pop(0)
1104
for mapping_line in mappings:
1105
mapping_data = self._get_hdr_dic(header, mapping_line, '!')
1106
return_data[mapping_data['vdisk_name']] = mapping_data
1110
def _map_vol_to_host(self, volume_name, host_name):
1111
"""Create a mapping between a volume to a host."""
1113
LOG.debug(_('enter: _map_vol_to_host: volume %(vol)s to '
1114
'host %(host)s') % {'vol': volume_name,
1117
# Check if this volume is already mapped to this host
1118
mapping_data = self._get_hostvdisk_mappings(host_name)
1122
if volume_name in mapping_data:
1124
result_lun = mapping_data[volume_name]['SCSI_id']
1127
for k, v in mapping_data.iteritems():
1128
lun_used.append(int(v['SCSI_id']))
1130
# Assume all luns are taken to this point, and then try to find
1132
result_lun = str(len(lun_used))
1133
for index, n in enumerate(lun_used):
1135
result_lun = str(index)
1137
# Volume is not mapped to host, create a new LUN
1139
out, err = self._run_ssh('mkvdiskhostmap -host %s -scsi %s %s'
1140
% (host_name, result_lun, volume_name))
1141
self._driver_assert(len(out.strip()) > 0 and
1142
'successfully created' in out,
1143
_('_map_vol_to_host: mapping host %(host)s to '
1144
'volume %(vol)s with LUN '
1145
'%(lun)s - did not find success message in CLI output. '
1146
'stdout: %(out)s\n stderr: %(err)s\n')
1147
% {'host': host_name,
1153
LOG.debug(_('leave: _map_vol_to_host: LUN %(lun)s, volume %(vol)s, '
1154
'host %(host)s') % {'lun': result_lun, 'vol': volume_name,
1159
def _get_flashcopy_mapping_attributes(self, fc_map_id):
1160
"""Return the attributes of a FlashCopy mapping.
1162
Returns the attributes for the specified FlashCopy mapping, or
1163
None if the mapping does not exist.
1164
An exception is raised if the information from system can not
1165
be parsed or matched to a single FlashCopy mapping (this case
1166
should not happen under normal conditions).
1169
LOG.debug(_('enter: _get_flashcopy_mapping_attributes: mapping %s')
1171
# Get the lunid to be used
1173
fc_ls_map_cmd = ('lsfcmap -filtervalue id=%s -delim !' % fc_map_id)
1174
out, err = self._run_ssh(fc_ls_map_cmd)
1175
self._driver_assert(len(out) > 0,
1176
_('_get_flashcopy_mapping_attributes: '
1177
'Unexpected response from CLI output. '
1178
'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s')
1179
% {'cmd': fc_ls_map_cmd,
1183
# Get list of FlashCopy mappings
1184
# We expect zero or one line if mapping does not exist,
1185
# two lines if it does exist, otherwise error
1186
lines = out.strip().split('\n')
1187
self._driver_assert(len(lines) <= 2,
1188
_('_get_flashcopy_mapping_attributes: '
1189
'Unexpected response from CLI output. '
1190
'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s')
1191
% {'cmd': fc_ls_map_cmd,
1196
attributes = self._get_hdr_dic(lines[0], lines[1], '!')
1197
else: # 0 or 1 lines
1200
LOG.debug(_('leave: _get_flashcopy_mapping_attributes: mapping '
1201
'%(id)s, attributes %(attr)s') %
1203
'attr': attributes})
1207
def _get_volume_attributes(self, volume_name):
1208
"""Return volume attributes, or None if volume does not exist
1210
Exception is raised if the information from system can not be
1211
parsed/matched to a single volume.
1214
LOG.debug(_('enter: _get_volume_attributes: volume %s')
1216
# Get the lunid to be used
1219
ssh_cmd = 'lsvdisk -bytes -delim ! %s ' % volume_name
1220
out, err = self._run_ssh(ssh_cmd)
1221
except exception.ProcessExecutionError as e:
1222
# Didn't get details from the storage, return None
1223
LOG.error(_('CLI Exception output:\n command: %(cmd)s\n '
1224
'stdout: %(out)s\n stderr: %(err)s') %
1230
self._driver_assert(len(out) > 0,
1231
('_get_volume_attributes: '
1232
'Unexpected response from CLI output. '
1233
'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s')
1238
for attrib_line in out.split('\n'):
1239
# If '!' not found, return the string and two empty strings
1240
attrib_name, foo, attrib_value = attrib_line.partition('!')
1241
if attrib_name is not None and attrib_name.strip() > 0:
1242
attributes[attrib_name] = attrib_value
1244
LOG.debug(_('leave: _get_volume_attributes:\n volume %(vol)s\n '
1245
'attributes: %(attr)s')
1246
% {'vol': volume_name,
1247
'attr': str(attributes)})