~ubuntu-branches/ubuntu/quantal/nova/quantal-proposed

« back to all changes in this revision

Viewing changes to nova/volume/storwize_svc.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2012-08-16 14:04:11 UTC
  • mto: This revision was merged to the branch mainline in revision 84.
  • Revision ID: package-import@ubuntu.com-20120816140411-0mr4n241wmk30t9l
Tags: upstream-2012.2~f3
ImportĀ upstreamĀ versionĀ 2012.2~f3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright (c) 2012 IBM, Inc.
 
4
# Copyright (c) 2012 OpenStack LLC.
 
5
# All Rights Reserved.
 
6
#
 
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
 
10
#
 
11
#         http://www.apache.org/licenses/LICENSE-2.0
 
12
#
 
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
 
17
#    under the License.
 
18
#
 
19
# Authors:
 
20
#   Ronen Kat <ronenkat@il.ibm.com>
 
21
#   Avishay Traeger <avishay@il.ibm.com>
 
22
 
 
23
"""
 
24
Volume driver for IBM Storwize V7000 and SVC storage systems.
 
25
 
 
26
Notes:
 
27
1. If you specify both a password and a key file, this driver will use the
 
28
   key file only.
 
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).
 
34
 
 
35
Limitations:
 
36
1. The driver was not tested with SVC or clustered configurations of Storwize
 
37
   V7000.
 
38
2. The driver expects CLI output in English, error messages may be in a
 
39
   localized format.
 
40
"""
 
41
 
 
42
import random
 
43
import re
 
44
import string
 
45
import time
 
46
 
 
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
 
54
 
 
55
LOG = logging.getLogger(__name__)
 
56
 
 
57
storwize_svc_opts = [
 
58
    cfg.StrOpt('storwize_svc_volpool_name',
 
59
               default='volpool',
 
60
               help='Storage system storage pool for volumes'),
 
61
    cfg.StrOpt('storwize_svc_vol_vtype',
 
62
               default='striped',
 
63
               help='Storage system volume type for volumes'),
 
64
    cfg.StrOpt('storwize_svc_vol_rsize',
 
65
               default='2%',
 
66
               help='Storage system space-efficiency parameter for volumes'),
 
67
    cfg.StrOpt('storwize_svc_vol_warning',
 
68
               default='0',
 
69
               help='Storage system threshold for volume capacity warnings'),
 
70
    cfg.BoolOpt('storwize_svc_vol_autoexpand',
 
71
               default=True,
 
72
               help='Storage system autoexpand parameter for volumes '
 
73
                    '(True/False)'),
 
74
    cfg.StrOpt('storwize_svc_vol_grainsize',
 
75
               default='256',
 
76
               help='Storage system grain size parameter for volumes '
 
77
                    '(32/64/128/256)'),
 
78
    cfg.BoolOpt('storwize_svc_vol_compression',
 
79
               default=False,
 
80
               help='Storage system compression option for volumes'),
 
81
    cfg.StrOpt('storwize_svc_flashcopy_timeout',
 
82
               default='120',
 
83
               help='Maximum number of seconds to wait for FlashCopy to be'
 
84
                    'prepared. Maximum value is 600 seconds (10 minutes).'),
 
85
]
 
86
 
 
87
FLAGS = flags.FLAGS
 
88
FLAGS.register_opts(storwize_svc_opts)
 
89
 
 
90
 
 
91
class StorwizeSVCDriver(san.SanISCSIDriver):
 
92
    """IBM Storwize V7000 and SVC iSCSI volume driver."""
 
93
 
 
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
 
98
 
 
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):
 
103
            ch = chr(num)
 
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))
 
109
 
 
110
        self._unicode_host_name_filter = dict((ord(unicode(char)), u'-')
 
111
                                         for char in invalid_ch_in_host)
 
112
 
 
113
    def _get_hdr_dic(self, header, row, delim):
 
114
        """Return CLI row data as a dictionary indexed by names from header.
 
115
 
 
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
 
118
        delim.
 
119
        """
 
120
 
 
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),
 
127
                   'row': str(row)})
 
128
        dic = {}
 
129
        for attribute, value in map(None, attributes, values):
 
130
            dic[attribute] = value
 
131
        return dic
 
132
 
 
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)
 
138
 
 
139
    def check_for_setup_error(self):
 
140
        """Check that we have all configuration details from the storage."""
 
141
 
 
142
        LOG.debug(_('enter: check_for_setup_error'))
 
143
 
 
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')
 
150
                % {'cmd': ssh_cmd,
 
151
                   'out': str(out),
 
152
                   'err': str(err)})
 
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'))
 
158
 
 
159
        storage_nodes = {}
 
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')
 
166
                % {'cmd': ssh_cmd,
 
167
                   'out': str(out),
 
168
                   'err': str(err)})
 
169
 
 
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')
 
174
                % {'cmd': ssh_cmd,
 
175
                   'out': str(out),
 
176
                   'err': str(err)})
 
177
        header = nodes.pop(0)
 
178
        for node_line in nodes:
 
179
            try:
 
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')
 
187
                              % {'cmd': ssh_cmd,
 
188
                                 'out': str(out),
 
189
                                 'err': str(err)})
 
190
            node = {}
 
191
            try:
 
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']
 
196
                node['ipv4'] = []
 
197
                node['ipv6'] = []
 
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 '
 
205
                      'Details: %(msg)s\n'
 
206
                      'Command: %(cmd)s\n '
 
207
                      'stdout: %(out)s\n stderr: %(err)s')
 
208
                            % {'msg': str(e),
 
209
                               'cmd': ssh_cmd,
 
210
                               'out': str(out),
 
211
                               'err': str(err)})
 
212
                raise exception.VolumeBackendAPIException(
 
213
                        data=exception_message)
 
214
 
 
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')
 
222
                            % {'cmd': ssh_cmd,
 
223
                               'out': str(out),
 
224
                               'err': str(err)})
 
225
 
 
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')
 
230
                % {'cmd': ssh_cmd,
 
231
                   'out': str(out),
 
232
                   'err': str(err)})
 
233
        header = portips.pop(0)
 
234
        for portip_line in portips:
 
235
            try:
 
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')
 
243
                              % {'cmd': ssh_cmd,
 
244
                                 'out': str(out),
 
245
                                 'err': str(err)})
 
246
            try:
 
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 '
 
255
                      'Details: %(msg)s\n'
 
256
                      'Command: %(cmd)s\n '
 
257
                      'stdout: %(out)s\n stderr: %(err)s')
 
258
                            % {'msg': str(e),
 
259
                               'cmd': ssh_cmd,
 
260
                               'out': str(out),
 
261
                               'err': str(err)})
 
262
                raise exception.VolumeBackendAPIException(
 
263
                        data=exception_message)
 
264
 
 
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)
 
271
            else:
 
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,
 
278
                                 'out': str(out),
 
279
                                 'err': str(err)})
 
280
 
 
281
        iscsi_ipv4_conf = []
 
282
        iscsi_ipv6_conf = []
 
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'],
 
287
                                        'ip': node['ipv4'],
 
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'],
 
291
                                        'ip': node['ipv6'],
 
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')
 
298
                                % node['id'])
 
299
 
 
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})
 
307
 
 
308
        self.iscsi_ipv4_conf = iscsi_ipv4_conf
 
309
        self.iscsi_ipv6_conf = iscsi_ipv6_conf
 
310
 
 
311
        LOG.debug(_('leave: check_for_setup_error'))
 
312
 
 
313
    def _check_num_perc(self, value):
 
314
        """Return True if value is either a number or a percentage."""
 
315
        if value.endswith('%'):
 
316
            value = value[0:-1]
 
317
        return value.isdigit()
 
318
 
 
319
    def _check_flags(self):
 
320
        """Ensure that the flags are set properly."""
 
321
 
 
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)
 
328
 
 
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'))
 
336
 
 
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\''))
 
343
 
 
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'))
 
350
 
 
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'))
 
357
 
 
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 '
 
364
                  'True or False'))
 
365
 
 
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\''))
 
373
 
 
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')
 
382
                      % flashcopy_timeout)
 
383
 
 
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 '
 
390
                         'True or False'))
 
391
 
 
392
    def do_setup(self, context):
 
393
        """Validate the flags."""
 
394
        LOG.debug(_('enter: do_setup'))
 
395
        self._check_flags()
 
396
        LOG.debug(_('leave: do_setup'))
 
397
 
 
398
    def create_volume(self, volume):
 
399
        """Create a new volume - uses the internal method."""
 
400
        return self._create_volume(volume, units='gb')
 
401
 
 
402
    def _create_volume(self, volume, units='gb'):
 
403
        """Create a new volume."""
 
404
 
 
405
        default_size = '1'  # 1GB
 
406
        name = volume['name']
 
407
        model_update = None
 
408
 
 
409
        LOG.debug(_('enter: create_volume: volume %s ') % name)
 
410
 
 
411
        if int(volume['size']) > 0:
 
412
            size = int(volume['size'])
 
413
        else:
 
414
            size = default_size
 
415
 
 
416
        if getattr(FLAGS, 'storwize_svc_vol_autoexpand'):
 
417
            autoexpand = '-autoexpand'
 
418
        else:
 
419
            autoexpand = ''
 
420
 
 
421
        # Set space-efficient options
 
422
        if getattr(FLAGS, 'storwize_svc_vol_rsize').strip() == '-1':
 
423
            ssh_cmd_se_opt = ''
 
424
        else:
 
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'
 
430
            else:
 
431
                ssh_cmd_se_opt = ssh_cmd_se_opt + ('-grainsize %(grain)s' %
 
432
                       {'grain': getattr(FLAGS, 'storwize_svc_vol_grainsize')})
 
433
 
 
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'
 
437
                    % {'name': name,
 
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)})
 
448
 
 
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)})
 
458
 
 
459
        LOG.debug(_('leave: create_volume: volume %(name)s ') % {'name': name})
 
460
 
 
461
    def delete_volume(self, volume):
 
462
        self._delete_volume(volume, False)
 
463
 
 
464
    def _delete_volume(self, volume, force_opt):
 
465
        """Driver entry point for destroying existing volumes."""
 
466
 
 
467
        name = volume['name']
 
468
        LOG.debug(_('enter: delete_volume: volume %(name)s ') % {'name': name})
 
469
 
 
470
        if force_opt:
 
471
            force_flag = '-force'
 
472
        else:
 
473
            force_flag = ''
 
474
 
 
475
        volume_defined = self._is_volume_defined(name)
 
476
        # Try to delete volume only if found on the storage
 
477
        if volume_defined:
 
478
            out, err = self._run_ssh('rmvdisk %(force)s %(name)s'
 
479
                                    % {'force': force_flag,
 
480
                                       'name': name})
 
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')
 
485
                                % {'name': name,
 
486
                                   'out': str(out),
 
487
                                   'err': str(err)})
 
488
        else:
 
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})
 
492
 
 
493
        LOG.debug(_('leave: delete_volume: volume %(name)s ') % {'name': name})
 
494
 
 
495
    def ensure_export(self, context, volume):
 
496
        """Check that the volume exists on the storage.
 
497
 
 
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.
 
500
        """
 
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')
 
504
                       % volume['name'])
 
505
 
 
506
    def create_export(self, context, volume):
 
507
        model_update = None
 
508
        return model_update
 
509
 
 
510
    def remove_export(self, context, volume):
 
511
        pass
 
512
 
 
513
    def check_for_export(self, context, volume_id):
 
514
        raise NotImplementedError()
 
515
 
 
516
    def initialize_connection(self, volume, connector):
 
517
        """Perform the necessary work so that an iSCSI connection can be made.
 
518
 
 
519
        To be able to create an iSCSI connection from a given iSCSI name to a
 
520
        volume, we must:
 
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.
 
526
        """
 
527
        LOG.debug(_('enter: initialize_connection: volume %(vol)s with '
 
528
                    'connector %(conn)s') % {'vol': str(volume),
 
529
                    'conn': str(connector)})
 
530
 
 
531
        initiator_name = connector['initiator']
 
532
        volume_name = volume['name']
 
533
 
 
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,
 
539
                                               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.'))
 
543
 
 
544
        lun_id = self._map_vol_to_host(volume_name, host_name)
 
545
 
 
546
        # Get preferred path
 
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
 
557
                    break
 
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,
 
563
                            'vol': volume_name})
 
564
        else:
 
565
            # Get 1st node
 
566
            preferred_node_entry = self.iscsi_ipv4_conf[0]
 
567
            LOG.error(
 
568
                _('initialize_connection: did not find a preferred node '
 
569
                  'for volume %s in iSCSI configuration') % volume_name)
 
570
 
 
571
        properties = {}
 
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']
 
581
 
 
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)})
 
587
 
 
588
        return {'driver_volume_type': 'iscsi', 'data': properties, }
 
589
 
 
590
    def terminate_connection(self, volume, connector):
 
591
        """Cleanup after an iSCSI connection has been terminated.
 
592
 
 
593
        When we clean up a terminated connection between a given iSCSI name
 
594
        and volume, we:
 
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)
 
599
        """
 
600
        LOG.debug(_('enter: terminate_connection: volume %(vol)s with '
 
601
                    'connector %(conn)s') % {'vol': str(volume),
 
602
                    'conn': str(connector)})
 
603
 
 
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)
 
612
 
 
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
 
619
            # rmvdiskhostmap
 
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')
 
624
                                 % {'vol': vol_name,
 
625
                                    'host': host_name,
 
626
                                    'out': str(out),
 
627
                                    'err': str(err)})
 
628
            del mapping_data[vol_name]
 
629
        else:
 
630
            LOG.error(_('terminate_connection: no mapping of volume '
 
631
                        '%(vol)s to host %(host)s found') %
 
632
                        {'vol': vol_name, 'host': host_name})
 
633
 
 
634
        # If this host has no more mappings, delete it
 
635
        if not mapping_data:
 
636
            self._delete_host(host_name)
 
637
 
 
638
        LOG.debug(_('leave: terminate_connection: volume %(vol)s with '
 
639
                    'connector %(conn)s') % {'vol': str(volume),
 
640
                    'conn': str(connector)})
 
641
 
 
642
    def _flashcopy_cleanup(self, fc_map_id, source, target):
 
643
        """Clean up a failed FlashCopy operation."""
 
644
 
 
645
        try:
 
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,
 
654
                           'source': source,
 
655
                           'target': target,
 
656
                           'out': e.stdout,
 
657
                           'err': e.stderr})
 
658
 
 
659
    def _run_flashcopy(self, source, target):
 
660
        """Create a FlashCopy mapping from the source to the target."""
 
661
 
 
662
        LOG.debug(
 
663
            _('enter: _run_flashcopy: execute FlashCopy from source '
 
664
              '%(source)s to target %(target)s') % {'source': source,
 
665
              'target': target})
 
666
 
 
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')
 
674
                            % {'source': source,
 
675
                                'target': target,
 
676
                                'out': str(out),
 
677
                                'err': str(err)})
 
678
 
 
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')
 
687
                            % {'source': source,
 
688
                               'target': target,
 
689
                               'out': str(out),
 
690
                               'err': str(err)})
 
691
 
 
692
        try:
 
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')
 
698
                            % {'source': source,
 
699
                               'target': target,
 
700
                               'out': str(out),
 
701
                               'err': str(err)})
 
702
        except IndexError:
 
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')
 
707
                            % {'source': source,
 
708
                               'target': target,
 
709
                               'out': str(out),
 
710
                               'err': str(err)})
 
711
        try:
 
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')
 
718
                            % {'source': source,
 
719
                               'target': target,
 
720
                               'out': e.stdout,
 
721
                               'err': e.stderr})
 
722
                self._flashcopy_cleanup(fc_map_id, source, target)
 
723
 
 
724
        mapping_ready = False
 
725
        wait_time = 5
 
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(
 
731
                                                            fc_map_id)
 
732
            if (mapping_attributes is None or
 
733
                    'status' not in mapping_attributes):
 
734
                break
 
735
            if mapping_attributes['status'] == 'prepared':
 
736
                mapping_ready = True
 
737
                break
 
738
            elif mapping_attributes['status'] != 'preparing':
 
739
                # Unexpected mapping status
 
740
                exception_msg = (_('unexecpted mapping status %(status)s '
 
741
                                   'for mapping %(id)s. Attributes: '
 
742
                                   '%(attr)s')
 
743
                                 % {'status': mapping_attributes['status'],
 
744
                                    'id': fc_map_id,
 
745
                                    'attr': mapping_attributes})
 
746
                raise exception.VolumeBackendAPIException(
 
747
                        data=exception_msg)
 
748
            # Need to wait for mapping to be prepared, wait a few seconds
 
749
            time.sleep(wait_time)
 
750
 
 
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,
 
755
                               'to': getattr(
 
756
                                FLAGS, 'storwize_svc_flashcopy_timeout')})
 
757
            LOG.error(_('_run_flashcopy: fail to start FlashCopy '
 
758
                        'from %(source)s to %(target)s with '
 
759
                        'exception %(ex)s')
 
760
                        % {'source': source,
 
761
                           'target': target,
 
762
                           'ex': exception_msg})
 
763
            self._flashcopy_cleanup(fc_map_id, source, target)
 
764
            raise exception.InvalidSnapshot(
 
765
                reason=_('_run_flashcopy: %s') % exception_msg)
 
766
 
 
767
        try:
 
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')
 
774
                            % {'source': source,
 
775
                               'target': target,
 
776
                               'out': e.stdout,
 
777
                               'err': e.stderr})
 
778
                self._flashcopy_cleanup(fc_map_id, source, target)
 
779
 
 
780
        LOG.debug(_('leave: _run_flashcopy: FlashCopy started from '
 
781
                    '%(source)s to %(target)s') % {'source': source,
 
782
                    'target': target})
 
783
 
 
784
    def create_volume_from_snapshot(self, volume, snapshot):
 
785
        """Create a new snapshot from volume."""
 
786
 
 
787
        source_volume = snapshot['name']
 
788
        tgt_volume = volume['name']
 
789
 
 
790
        LOG.debug(_('enter: create_volume_from_snapshot: snapshot %(tgt)s '
 
791
                    'from volume %(src)s') % {'tgt': tgt_volume,
 
792
                    'src': source_volume})
 
793
 
 
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)
 
801
 
 
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']
 
808
 
 
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)
 
816
 
 
817
        snapshot_volume = {}
 
818
        snapshot_volume['name'] = tgt_volume
 
819
        snapshot_volume['size'] = src_volume_size
 
820
 
 
821
        self._create_volume(snapshot_volume, units='b')
 
822
 
 
823
        try:
 
824
            self._run_flashcopy(source_volume, tgt_volume)
 
825
        except Exception:
 
826
            with excutils.save_and_reraise_exception():
 
827
                # Clean up newly-created snapshot if the FlashCopy failed
 
828
                self._delete_volume(snapshot_volume, True)
 
829
 
 
830
        LOG.debug(
 
831
            _('leave: create_volume_from_snapshot: %s created successfully')
 
832
            % tgt_volume)
 
833
 
 
834
    def create_snapshot(self, snapshot):
 
835
        """Create a new snapshot using FlashCopy."""
 
836
 
 
837
        src_volume = snapshot['volume_name']
 
838
        tgt_volume = snapshot['name']
 
839
 
 
840
        # Flag to keep track of created volumes in case FlashCopy
 
841
        tgt_volume_created = False
 
842
 
 
843
        LOG.debug(_('enter: create_snapshot: snapshot %(tgt)s from '
 
844
                    'volume %(src)s') % {'tgt': tgt_volume,
 
845
                    'src': src_volume})
 
846
 
 
847
        src_volume_attributes = self._get_volume_attributes(src_volume)
 
848
        if src_volume_attributes is None:
 
849
            exception_msg = (
 
850
                _('create_snapshot: source volume %s does not exist')
 
851
                % src_volume)
 
852
            LOG.error(exception_msg)
 
853
            raise exception.VolumeNotFound(exception_msg,
 
854
                                           volume_id=src_volume)
 
855
 
 
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})
 
861
 
 
862
        source_volume_size = src_volume_attributes['capacity']
 
863
 
 
864
        tgt_volume_attributes = self._get_volume_attributes(tgt_volume)
 
865
        # Does the snapshot target exist?
 
866
        snapshot_volume = {}
 
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
 
873
        else:
 
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:
 
882
                exception_msg = (
 
883
                    _('create_snapshot: source %(src)s and target '
 
884
                      'volume %(tgt)s have different capacities '
 
885
                      '(source:%(ssize)s target:%(tsize)s)') %
 
886
                        {'src': src_volume,
 
887
                         'tgt': tgt_volume,
 
888
                         'ssize': source_volume_size,
 
889
                         'tsize': target_volume_size})
 
890
                LOG.error(exception_msg)
 
891
                raise exception.InvalidSnapshot(reason=exception_msg)
 
892
 
 
893
        try:
 
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)
 
900
 
 
901
        LOG.debug(_('leave: create_snapshot: %s created successfully')
 
902
                  % tgt_volume)
 
903
 
 
904
    def delete_snapshot(self, snapshot):
 
905
        self._delete_snapshot(snapshot, False)
 
906
 
 
907
    def _delete_snapshot(self, snapshot, force_opt):
 
908
        """Delete a snapshot from the storage."""
 
909
        LOG.debug(_('enter: delete_snapshot: snapshot %s') % snapshot)
 
910
 
 
911
        snapshot_defined = self._is_volume_defined(snapshot['name'])
 
912
        if snapshot_defined:
 
913
            if force_opt:
 
914
                self._delete_volume(snapshot, force_opt)
 
915
            else:
 
916
                self.delete_volume(snapshot)
 
917
 
 
918
        LOG.debug(_('leave: delete_snapshot: snapshot %s') % snapshot)
 
919
 
 
920
    def _get_host_from_iscsiname(self, iscsi_name):
 
921
        """List the hosts defined in the storage.
 
922
 
 
923
        Return the host name with the given iSCSI name, or None if there is
 
924
        no host name with that iSCSI name.
 
925
        """
 
926
 
 
927
        LOG.debug(_('enter: _get_host_from_iscsiname: iSCSI initiator %s')
 
928
                   % iscsi_name)
 
929
 
 
930
        # Get list of host in the storage
 
931
        ssh_cmd = 'lshost -delim !'
 
932
        out, err = self._run_ssh(ssh_cmd)
 
933
 
 
934
        if (len(out.strip()) == 0):
 
935
            return None
 
936
 
 
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,
 
941
                                    'out': str(out),
 
942
                                    'err': str(err)}
 
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')
 
948
 
 
949
        hosts = map(lambda x: x.split('!')[name_index], host_lines)
 
950
        hostname = None
 
951
 
 
952
        # For each host, get its details and check for its iSCSI name
 
953
        for host in hosts:
 
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')
 
960
                        % {'cmd': ssh_cmd,
 
961
                           'out': str(out),
 
962
                           'err': str(err)})
 
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:
 
968
                        hostname = host
 
969
                        break
 
970
            if hostname is not None:
 
971
                break
 
972
 
 
973
        LOG.debug(_('leave: _get_host_from_iscsiname: iSCSI initiator %s')
 
974
                   % iscsi_name)
 
975
 
 
976
        return hostname
 
977
 
 
978
    def _create_new_host(self, host_name, initiator_name):
 
979
        """Create a new host on the storage system.
 
980
 
 
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.
 
984
        """
 
985
 
 
986
        LOG.debug(_('enter: _create_new_host: host %(name)s with iSCSI '
 
987
                    'initiator %(init)s') % {'name': host_name,
 
988
                    'init': initiator_name})
 
989
 
 
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)
 
994
        else:
 
995
            msg = _('_create_new_host: cannot clean host name. Host name '
 
996
                    'is not unicode or string')
 
997
            LOG.error(msg)
 
998
            raise exception.NoValidHost(reason=msg)
 
999
 
 
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,
 
1013
                     'out': str(out),
 
1014
                     'err': str(err)})
 
1015
 
 
1016
        LOG.debug(_('leave: _create_new_host: host %(host)s with iSCSI '
 
1017
                    'initiator %(init)s') % {'host': host_name,
 
1018
                    'init': initiator_name})
 
1019
 
 
1020
        return host_name
 
1021
 
 
1022
    def _delete_host(self, host_name):
 
1023
        """Delete a host and associated iSCSI initiator name."""
 
1024
 
 
1025
        LOG.debug(_('enter: _delete_host: host %s ') % host_name)
 
1026
 
 
1027
        # Check if host exists on system, expect to find the host
 
1028
        is_defined = self._is_host_defined(host_name)
 
1029
        if is_defined:
 
1030
            # Delete host
 
1031
            out, err = self._run_ssh('rmhost %s ' % host_name)
 
1032
        else:
 
1033
            LOG.info(_('warning: tried to delete host %(name)s but '
 
1034
                       'it does not exist.') % {'name': host_name})
 
1035
 
 
1036
        LOG.debug(_('leave: _delete_host: host %s ') % host_name)
 
1037
 
 
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:
 
1046
            return False
 
1047
        else:
 
1048
            return True
 
1049
 
 
1050
    def _is_host_defined(self, host_name):
 
1051
        """Check if a host is defined on the storage."""
 
1052
 
 
1053
        LOG.debug(_('enter: _is_host_defined: host %s ') % host_name)
 
1054
 
 
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 !'
 
1059
                                % host_name)
 
1060
        if len(out.strip()) == 0:
 
1061
            return False
 
1062
 
 
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')
 
1067
                % {'out': str(out),
 
1068
                   'err': str(err)})
 
1069
 
 
1070
        if len(lines) == 2:
 
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 '
 
1076
                      '%(host2)s.\n '
 
1077
                      'stdout: %(out)s\n stderr: %(err)s\n')
 
1078
                      % {'host1': host_name_from_storage,
 
1079
                         'host2': host_name,
 
1080
                         'out': str(out),
 
1081
                         'err': str(err)})
 
1082
        else:  # 0 or 1 lines
 
1083
            host_name_from_storage = None
 
1084
 
 
1085
        LOG.debug(_('leave: _is_host_defined: host %(host)s with %(str)s ') % {
 
1086
                   'host': host_name,
 
1087
                   'str': host_name_from_storage is not None})
 
1088
 
 
1089
        if host_name_from_storage is None:
 
1090
            return False
 
1091
        else:
 
1092
            return True
 
1093
 
 
1094
    def _get_hostvdisk_mappings(self, host_name):
 
1095
        """Return the defined storage mappings for a host."""
 
1096
 
 
1097
        return_data = {}
 
1098
        ssh_cmd = 'lshostvdiskmap -delim ! %s' % host_name
 
1099
        out, err = self._run_ssh(ssh_cmd)
 
1100
 
 
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
 
1107
 
 
1108
        return return_data
 
1109
 
 
1110
    def _map_vol_to_host(self, volume_name, host_name):
 
1111
        """Create a mapping between a volume to a host."""
 
1112
 
 
1113
        LOG.debug(_('enter: _map_vol_to_host: volume %(vol)s to '
 
1114
                    'host %(host)s') % {'vol': volume_name,
 
1115
                    'host': host_name})
 
1116
 
 
1117
        # Check if this volume is already mapped to this host
 
1118
        mapping_data = self._get_hostvdisk_mappings(host_name)
 
1119
 
 
1120
        mapped_flag = False
 
1121
        result_lun = '-1'
 
1122
        if volume_name in mapping_data:
 
1123
            mapped_flag = True
 
1124
            result_lun = mapping_data[volume_name]['SCSI_id']
 
1125
        else:
 
1126
            lun_used = []
 
1127
            for k, v in mapping_data.iteritems():
 
1128
                lun_used.append(int(v['SCSI_id']))
 
1129
            lun_used.sort()
 
1130
            # Assume all luns are taken to this point, and then try to find
 
1131
            # an unused one
 
1132
            result_lun = str(len(lun_used))
 
1133
            for index, n in enumerate(lun_used):
 
1134
                if n > index:
 
1135
                    result_lun = str(index)
 
1136
 
 
1137
        # Volume is not mapped to host, create a new LUN
 
1138
        if not mapped_flag:
 
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,
 
1148
                      'vol': volume_name,
 
1149
                      'lun': result_lun,
 
1150
                      'out': str(out),
 
1151
                      'err': str(err)})
 
1152
 
 
1153
        LOG.debug(_('leave: _map_vol_to_host: LUN %(lun)s, volume %(vol)s, '
 
1154
                    'host %(host)s') % {'lun': result_lun, 'vol': volume_name,
 
1155
                    'host': host_name})
 
1156
 
 
1157
        return result_lun
 
1158
 
 
1159
    def _get_flashcopy_mapping_attributes(self, fc_map_id):
 
1160
        """Return the attributes of a FlashCopy mapping.
 
1161
 
 
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).
 
1167
        """
 
1168
 
 
1169
        LOG.debug(_('enter: _get_flashcopy_mapping_attributes: mapping %s')
 
1170
                   % fc_map_id)
 
1171
        # Get the lunid to be used
 
1172
 
 
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,
 
1180
                   'out': str(out),
 
1181
                   'err': str(err)})
 
1182
 
 
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,
 
1192
                               'out': str(out),
 
1193
                               'err': str(err)})
 
1194
 
 
1195
        if len(lines) == 2:
 
1196
            attributes = self._get_hdr_dic(lines[0], lines[1], '!')
 
1197
        else:  # 0 or 1 lines
 
1198
            attributes = None
 
1199
 
 
1200
        LOG.debug(_('leave: _get_flashcopy_mapping_attributes: mapping '
 
1201
                    '%(id)s, attributes %(attr)s') %
 
1202
                   {'id': fc_map_id,
 
1203
                    'attr': attributes})
 
1204
 
 
1205
        return attributes
 
1206
 
 
1207
    def _get_volume_attributes(self, volume_name):
 
1208
        """Return volume attributes, or None if volume does not exist
 
1209
 
 
1210
        Exception is raised if the information from system can not be
 
1211
        parsed/matched to a single volume.
 
1212
        """
 
1213
 
 
1214
        LOG.debug(_('enter: _get_volume_attributes: volume %s')
 
1215
                   % volume_name)
 
1216
        # Get the lunid to be used
 
1217
 
 
1218
        try:
 
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') %
 
1225
                      {'cmd': ssh_cmd,
 
1226
                       'out': e.stdout,
 
1227
                       'err': e.stderr})
 
1228
            return None
 
1229
 
 
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')
 
1234
                        % {'cmd': ssh_cmd,
 
1235
                           'out': str(out),
 
1236
                           'err': str(err)})
 
1237
        attributes = {}
 
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
 
1243
 
 
1244
        LOG.debug(_('leave: _get_volume_attributes:\n volume %(vol)s\n '
 
1245
                    'attributes: %(attr)s')
 
1246
                  % {'vol': volume_name,
 
1247
                     'attr': str(attributes)})
 
1248
 
 
1249
        return attributes