Statistics
| Branch: | Revision:

root / svc / storwize_svc.py @ ed2ee198

History | View | Annotate | Download (31.7 kB)

1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2

    
3
# Copyright 2013 GRNET S.A.
4
# Copyright 2013 IBM Corp.
5
# Copyright 2012 OpenStack LLC.
6
# All Rights Reserved.
7
#
8
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
9
#    not use this file except in compliance with the License. You may obtain
10
#    a copy of the License at
11
#
12
#         http://www.apache.org/licenses/LICENSE-2.0
13
#
14
#    Unless required by applicable law or agreed to in writing, software
15
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17
#    License for the specific language governing permissions and limitations
18
#    under the License.
19
#
20
# Authors:
21
#   Stratos Psomadakis <psomas@grnet.gr>
22
#   Ronen Kat <ronenkat@il.ibm.com>
23
#   Avishay Traeger <avishay@il.ibm.com>
24

    
25
""" Ganeti extstorage provider for IBM Storwize family and SVC storage systems
26
(ported from the Cinder Storwize / SVC volume driver).
27

28
The script takes its input from environment variables. Specifically the
29
following variables should be present:
30

31
 - VOL_NAME: The name of the new Image file
32
 - EXTP_SVC_SAN: The provider's configuration file section / identifier for the
33
                 SVC SAN
34

35
The following environmental variables are optional and should be present only
36
for specific commands:
37

38
 - VOL_SIZE: The size of the Image file
39

40
Based on the EXTP_SVC_SAN variable, the script will also read the SVC
41
configration parameters / options from the corresponding section of the INI SVC
42
configuration file. Currently, the config file path is hardcoded in
43
SVC_CONFIG_PATH (/etc/ganeti/extstorage/svc.conf). Each SAN / SVC section of
44
the INI file, which should set the following options:
45
 - host: The management hostname / IP for the SVC
46
 - port: The SSH daemon port for the SVC
47
 - user: The user to use to connect (SSH) to the SVC
48
 - password: The SSH password for the user
49
 - pool: The SVC volume pool to use to create vdisks / volumes
50
 - host_id: The id of the host in the SVC hosts table
51

52
Example SAN SVC INI config file:
53
    [san1]
54
    host = mysvchost1
55
    port = mysvcport1
56
    user = mysvcuser1
57
    password = mysvcpass1
58
    pool = mypool1
59
    host_id = mysvchostid1
60
    [san2]
61
    host = mysvchost2
62
    port = mysvcport2
63
    user = mysvcuser2
64
    password = mysvcpass2
65
    pool = mypool2
66
    host_id = mysvchostid2
67

68
The code branches to the correct function, depending on the name (sys.argv[0])
69
of the executed script (attach, create, etc.).
70

71
Returns O after successful completion, 1 on failure.
72

73
Limitations:
74
1. The provider expects CLI output in English, error messages may be in a
75
   localized format.
76
2. iSCSI support, public key authentication and detailed configuration for the
77
   SVC pool, volumes etc. not implemented yet.
78
3. The current version of the provider makes lots of assumptions about the
79
   running environment. It needs to be run on a Debian-like distro with various
80
   SCSI tools installed and multipath support as well. Multipath udev triggers
81
   should be removed, to ensure reliable operation of the provider.
82

83
"""
84

    
85
import ConfigParser
86
import glob
87
import os
88
import re
89
import subprocess
90
import sys
91
import traceback
92

    
93
import paramiko
94

    
95

    
96
# Hardcoded path to the SVC (Ganeti) config file
97
_SVC_CONFIG_PATH = "/etc/ganeti/extstorage/svc.conf"
98

    
99
# Hardcoded pool name, used for assertions
100
_SVC_POOL_NAME = "VM_POOL_SYNNEFO"
101

    
102
# Hardcoded host id range for HDIKA Synnefo nodes, used for assertions
103
_SVC_VALID_RANGE = range(7, 12) + range(19, 23)
104

    
105
# Hardcoded timeout for SSH (paramiko), in seconds
106
SVC_SSH_TIMEOUT = 30
107

    
108
# sysfs FC-related paths and tools, needed for adding (rescanning) and deleting
109
# FC disks / volumes.
110
_RESCAN_SCSI_CMD = 'rescan-scsi-bus'
111
_MULTIPATH_CMD = 'multipath'
112
_DMSETUP_CMD = 'dmsetup'
113
# Might be needing these below, in the future
114
#_SYSTOOL_CMD = 'systool'
115
#_SYSFS_FCHOSTS_PATH = '/sys/class/fc_host/'
116
#_SYSFS_FCSCAN_PATH = '/sys/class/scsi_host/%(fchost)s/scan'
117
#_SYSFS_FCSCAN_COMMAND = '- - -'
118
_SYSFS_BLOCK_PATH = '/sys/block/%(blodkdev)s/'
119
_SYSFS_FCDELETE_PATH = os.path.join(_SYSFS_BLOCK_PATH, '/device/delete')
120
_SYSFS_SLAVES_PATH = os.path.join(_SYSFS_BLOCK_PATH, '/slaves/')
121
_DEV_BYPATH = '/dev/disk/by-path/'
122

    
123
_SCSI_DEV_PREFIX = '3'
124

    
125

    
126
class SVCError(Exception):
127
    """ Base exception for SVC extstorage provider.
128

129
    """
130
    pass
131

    
132

    
133
class SVCConnection(object):
134
    """ SSH connection (paramiko) abstraction class.
135

136
    """
137
    def __init__(self, host, port, user, password):
138
        """ Initialize the SVCConnection object
139

140
        @type host: string
141
        @param host: host to connect to
142
        @type port: integer
143
        @param port: SSH port to connect to
144
        @type user: string
145
        @param user: SSH user to use
146
        @type password: string
147
        @param password: SSH password to use
148

149
        """
150
        super(SVCConnection, self).__init__()
151

    
152
        self._host = host
153
        self._port = port
154
        self._user = user
155
        self._password = password
156

    
157
        self._conn = None
158

    
159
    @staticmethod
160
    def _check_ssh_injection(cmd_list):
161
        """ Check for ssh injection patterns in the cmd list.
162

163
        @type cmd_list: list of strings
164
        @param cmd_list: a list containing the command to be executed plus any
165
                         arguments to the command
166
        """
167
        ssh_injection_pattern = ['`', '$', '|', '||', ';', '&', '&&', '>',
168
                                 '>>', '<']
169

    
170
        # Check whether injection attacks exist
171
        for arg in cmd_list:
172
            arg = arg.strip()
173
            # First, check no space in the middle of arg
174
            arg_len = len(arg.split())
175
            if arg_len > 1:
176
                raise SVCError("SSH injection detected: %s" % cmd_list)
177

    
178
            # Second, check whether danger character in command. So the shell
179
            # special operator must be a single argument.
180
            for char in ssh_injection_pattern:
181
                if arg == char:
182
                    continue
183

    
184
                result = arg.find(char)
185
                if not result == -1:
186
                    if result == 0 or not arg[result - 1] == '\\':
187
                        raise SVCError("SSH injection detected: %s" % cmd_list)
188

    
189
    def _create_connection(self):
190
        """ Create a paramiko SSH connection to the SVC.
191

192
        """
193
        self._conn = paramiko.SSHClient()
194
        self._conn.set_missing_host_key_policy(paramiko.AutoAddPolicy())
195
        self._conn.connect(self._host, port=self._port,
196
                           username=self._user,
197
                           password=self._password,
198
                           timeout=SVC_SSH_TIMEOUT,
199
                           look_for_keys=False,
200
                           allow_agent=False)
201

    
202
        transport = self._conn.get_transport()
203
        transport.sock.settimeout(None)
204
        transport.set_keepalive(SVC_SSH_TIMEOUT)
205

    
206
    def run_ssh_cmd(self, cmdlist):
207
        """ Connect to host and run the command specified in cmdlist.
208

209
        @type cmd_list: list of strings
210
        @param cmd_list: a list containing the command to be executed plus any
211
                         arguments to the command
212

213
        """
214
        self._check_ssh_injection(cmdlist)
215

    
216
        cmd = ' '.join(cmdlist)
217

    
218
        try:
219
            if self._conn:
220
                if not self._conn.get_transport().is_active():
221
                    self._conn.close()
222
                    self._create_connection()
223
            else:
224
                self._create_connection()
225
        except Exception as exc:
226
            raise SVCError("%sCannot create SSH connection to SVC: %s"
227
                           % (traceback.format_exc(), exc))
228

    
229
        conn = self._conn
230
        stderr = ''
231

    
232
        try:
233
            stdin_stream, stdout_stream, stderr_stream = conn.exec_command(cmd)
234
            channel = stdout_stream.channel
235

    
236
            # NOTE(justinsb): This seems suspicious...
237
            # ...other SSH clients have buffering issues with this approach
238
            stdout = stdout_stream.read()
239
            stderr = stderr_stream.read()
240
            stdin_stream.close()
241

    
242
            exit_status = channel.recv_exit_status()
243

    
244
            if exit_status != 0:
245
                raise Exception("Non-zero exit status %s" % exit_status)
246

    
247
            return (stdout, stderr)
248
        except Exception as exc:
249
            raise SVCError("%sCannot run SSH command '%s' on SVC :%s (%s)" %
250
                           (traceback.format_exc(), cmd, stderr, exc))
251

    
252

    
253
class SVCProvider(object):
254
    """ SVC provider class.
255

256
    """
257

    
258
    def __init__(self, config_file):
259
        """ Initializes the SVC provider.
260

261
        The method will read env variables and the SVC ini config file to fill
262
        in all the needed info for the SVC provider ops and initialize the
263
        SSH connection to the SVC (L{SVCConnection}).
264

265
        @type config_file: string
266
        @param config_file: path to the SVC INI configuration file
267

268
        """
269
        super(SVCProvider, self).__init__()
270

    
271
        self._svc_pool = None
272

    
273
        self._host_id = None
274

    
275
        self._vol_name = None
276
        self._vol_size = None
277

    
278
        (host, port, user, password) = self._get_params(config_file)
279

    
280
        self._svc_connection = SVCConnection(host, port, user, password)
281

    
282
    def _parse_svc_config(self, config_file, san):
283
        """"Parse the SVC (Ganeti) configuration file.
284

285
        The config should be in 'INI' format.
286

287
        @type config_file: string
288
        @param config_file: path to the INI config file
289
        @type san: string
290
        @param san: SVC SAN identifier / section in the INI file
291
        @rtype: list of the SVC config file options
292
        @return: list of the SVC config file options
293

294
        """
295
        config = ConfigParser.SafeConfigParser()
296

    
297
        try:
298
            if not config.read(config_file):
299
                raise ConfigParser.Error("Unable to read config file")
300

    
301
            expected_opts = frozenset(['host', 'port', 'user', 'password',
302
                                       'pool', 'host_id'])
303
            config_opts = config.options(san)
304

    
305
            if frozenset(config_opts) != expected_opts:
306
                raise ConfigParser.Error('Malformed config file')
307

    
308
            opts = []
309

    
310
            host = config.get(san, 'host')
311
            if host == '':
312
                raise ConfigParser.Error('Bad value for host')
313
            opts.append(host)
314

    
315
            port = config.get(san, 'port')
316
            try:
317
                port = int(port)
318
                if port <= 0 or port > 65535:
319
                    raise ValueError
320
            except ValueError:
321
                raise ConfigParser.Error('Bad value for port')
322
            opts.append(port)
323

    
324
            user = config.get(san, 'user')
325
            if user == '':
326
                raise ConfigParser.Error('Bad value for user')
327
            opts.append(user)
328

    
329
            password = config.get(san, 'password')
330
            if password == '':
331
                raise ConfigParser.Error('Bad value for password')
332
            opts.append(password)
333

    
334
            pool = config.get(san, 'pool')
335
            if pool == '':
336
                raise ConfigParser.Error('Bad value for pool')
337
            self._svc_pool = pool
338

    
339
            host_id = config.get(san, 'host_id')
340
            try:
341
                host_id = int(host_id)
342
                if host_id <= 0:
343
                    raise ValueError
344
            except ValueError:
345
                raise ConfigParser.Error('Bad value for host_id')
346
            self._host_id = host_id
347
        except ConfigParser.Error as config_error:
348
            raise SVCError('Error while reading svc config file: %s.\n' %
349
                           config_error)
350

    
351
        return opts
352

    
353
    def _get_params(self, config_file):
354
        """ Read env variables and the INI config file.
355

356
        Return the values of the environmental variables, set by Ganeti, and
357
        the SVC configuration parameters set in the SVC (Ganeti) config file.
358

359
        @type config_file: string
360
        @param config_file: path to the INI config file
361
        @rtype: list of the SVC config file options
362
        @return: list of the SVC config file options
363

364
        """
365

    
366
        self._vol_name = os.getenv("VOL_NAME")
367
        if self._vol_name is None:
368
            raise SVCError('The environment variable VOL_NAME is missing.\n')
369

    
370
        if self._vol_name == '':
371
            raise SVCError('Volume name cannot be empty.\n')
372

    
373
        self._vol_size = os.getenv("VOL_SIZE")
374
        if self._vol_size is not None:
375
            try:
376
                self._vol_size = int(self._vol_size)
377
                if self._vol_size <= 0:
378
                    raise ValueError
379
            except ValueError:
380
                raise SVCError('Volume size must be positive integer')
381

    
382
        san = os.getenv("EXTP_SVC_SAN")
383
        if san is None:
384
            raise SVCError('The environment variable EXTP_SVC_SAN is'
385
                           'missing.\n')
386
        if san == '':
387
            raise SVCError('EXTP_SVC_SAN cannot be none')
388

    
389
        return self._parse_svc_config(config_file, san)
390

    
391
    @staticmethod
392
    def _driver_assert(assert_condition, exception_message):
393
        """ Internal assertion mechanism for CLI output.
394

395
        @type assert_condition: boolean
396
        @param assert_condition: condition to assert against
397
        @type exception_message: string
398
        @param exception_message: exception message to print
399

400
        """
401
        if not assert_condition:
402
            raise SVCError(exception_message)
403

    
404
    def _assert_ssh_return(self, test, fun, ssh_cmd, out, err):
405
        """ Call internal assertion mechanism for SSH cmds.
406

407
        @type test: boolean
408
        @param test: condition to assert against
409
        @type fun: object
410
        @param fun: function / method which called us
411
        @type ssh_cmd: list of strings
412
        @param ssh_cmd: list of strings (command plus command arguments)
413
        @type out: buffer
414
        @param out: stdout buffer
415
        @type err: buffer
416
        @param err: stderr buffer
417

418
        """
419
        self._driver_assert(
420
            test,
421
            '%(fun)s: Failed with unexpected CLI output.\n '
422
            'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s'
423
            % {'fun': fun,
424
                'cmd': ssh_cmd,
425
                'out': str(out),
426
                'err': str(err)})
427

    
428
    def _exec_cmd_and_parse_attrs(self, ssh_cmd):
429
        """Execute command on the Storwize/SVC and parse attributes.
430

431
        Exception is raised if the information from the system
432
        can not be obtained.
433

434
        @type ssh_cmd: list of strings
435
        @param ssh_cmd: a list of the command and additional arguments
436
        @rtype: dict
437
        @return: a dictionary containing the attributes returned by ssh cmd
438

439
        """
440

    
441
        out, err = self._svc_connection.run_ssh_cmd(ssh_cmd)
442
        self._assert_ssh_return(len(out),
443
                                '_exec_cmd_and_parse_attrs',
444
                                ssh_cmd, out, err)
445

    
446
        attributes = {}
447
        for attrib_line in out.split('\n'):
448
            # If '!' not found, return the string and two empty strings
449
            attrib_name, _, attrib_value = attrib_line.partition('!')
450
            if attrib_name is not None and len(attrib_name.strip()):
451
                attributes[attrib_name] = attrib_value
452

    
453
        return attributes
454

    
455
    def _get_hdr_dic(self, header, row, delim):
456
        """Return CLI row data as a dictionary indexed by names from header.
457

458
        The strings are converted to columns using the delimiter in
459
        delim.
460

461
        @type header: string
462
        @param header: header string
463
        @type row: string
464
        @param row: row string
465
        @type delim: string
466
        @param delim: column delimiter
467
        @rtype: dict
468
        @return: dictionary containing CLI row data
469

470
        """
471

    
472
        attributes = header.split(delim)
473
        values = row.split(delim)
474

    
475
        self._driver_assert(
476
            len(values) ==
477
            len(attributes),
478
            '_get_hdr_dic: attribute headers and values do not match.\n '
479
            'Headers: %(header)s\n Values: %(row)s'
480
            % {'header': str(header),
481
               'row': str(row)})
482

    
483
        # Is it the same as dict(zip())?
484
        #dic = dict((a, v) for a, v in map(None, attributes, values))
485
        return dict(zip(attributes, values))
486

    
487
    def _get_hostvdisk_mappings(self, host_id):
488
        """ Return the defined storage mappings for a host.
489

490
        @type host_id: integer
491
        @param host_id: id of the host in the SVC
492
        @rtype: dict
493
        @return: dictionary containing the storage mappings for a host
494

495
        """
496

    
497
        return_data = {}
498
        ssh_cmd = ['svcinfo', 'lshostvdiskmap', '-delim', '!', str(host_id)]
499
        out, _ = self._svc_connection.run_ssh_cmd(ssh_cmd)
500

    
501
        mappings = out.strip().split('\n')
502
        if len(mappings):
503
            header = mappings.pop(0)
504
            for mapping_line in mappings:
505
                mapping_data = self._get_hdr_dic(header, mapping_line, '!')
506
                return_data[mapping_data['vdisk_name']] = mapping_data
507

    
508
        return return_data
509

    
510
    def _map_vol_to_host(self, volume_name, host_id):
511
        """ Create a mapping between a volume to a host.
512

513
        @type volume_name: string
514
        @param volume_name: volume name
515
        @type host_id: integer
516
        @param host_id: id of the host in the SVC
517

518
        """
519

    
520
        assert(host_id in _SVC_VALID_RANGE)
521
        ssh_cmd = ['svctask', 'mkvdiskhostmap', '-host', str(host_id),
522
                   volume_name]
523
        out, err = self._svc_connection.run_ssh_cmd(ssh_cmd)
524
        self._assert_ssh_return('successfully created' in out,
525
                                '_map_vol_to_host', ssh_cmd, out, err)
526

    
527
    def _unmap_vol_from_host(self, volume_name, host_id):
528
        """ Delete a mapping between a volume and a host.
529

530
        @type volume_name: string
531
        @param volume_name: volume name
532
        @type host_id: integer
533
        @param host_id: id of the host in the SVC
534

535
        """
536
        assert(host_id in _SVC_VALID_RANGE)
537
        ssh_cmd = ['svctask', 'rmvdiskhostmap', '-host', str(host_id),
538
                   volume_name]
539
        out, err = self._svc_connection.run_ssh_cmd(ssh_cmd)
540
        # Verify CLI behaviour - no output is returned from
541
        # rmvdiskhostmap
542
        self._assert_ssh_return(len(out.strip()) == 0,
543
                                'unmap_vol_from_host', ssh_cmd, out, err)
544

    
545
    def _get_vdisk_attributes(self, vdisk_name):
546
        """ Return vdisk attributes, or None if vdisk does not exist.
547

548
        Exception is raised if the information from system can not be
549
        parsed/matched to a single vdisk.
550

551
        @type vdisk_name: string
552
        @param vdisk_name: vdisk name
553
        @rtype: dict
554
        @return: a dictionary containing the vdisk attributes
555

556
        """
557

    
558
        ssh_cmd = ['svcinfo', 'lsvdisk', '-bytes', '-delim', '!', vdisk_name]
559
        return self._exec_cmd_and_parse_attrs(ssh_cmd)
560

    
561
    def _is_vdisk_defined(self, vdisk_name):
562
        """ Check if vdisk is defined.
563

564
        @type vdisk_name: string
565
        @param vdisk_name: vdisk name
566
        @rtype: bool
567
        @return: vdisk defined or not
568

569
        """
570
        # Maybe get list of disks and search instead of relying on empty
571
        # attr reply
572
        try:
573
            self._get_vdisk_attributes(vdisk_name)
574
            return True
575
        except SVCError:
576
            return False
577

    
578
    def _is_vdisk_mapped(self, vdisk_name, host_id):
579
        """ Check if vdisk is mapped to the host. """
580
        return vdisk_name in self._get_hostvdisk_mappings(host_id)
581

    
582
    @staticmethod
583
    def _check_call(cmdlist):
584
        """ Wrapper around subprocess.check_call.
585

586
        @type cmdlist: list of strings
587
        @param cmdlist: list of command plus arguments
588

589
        """
590
        try:
591
            subprocess.check_call(cmdlist, stderr=subprocess.STDOUT)
592
        except subprocess.CalledProcessError as exc:
593
            raise SVCError("Command %s failed: %s" % (' '.join(cmdlist),
594
                                                      exc.output))
595

    
596
    @staticmethod
597
    def _check_output(cmdlist):
598
        """ Wrapper around subprocess.check_output.
599

600
        @type cmdlist: list of strings
601
        @param cmdlist: list of command plus arguments
602
        @rtype: string
603
        @return: output of cmdlist
604

605
        """
606
        try:
607
            return subprocess.check_output(cmdlist, stderr=subprocess.STDOUT)
608
        except subprocess.CalledProcessError as exc:
609
            raise SVCError("Command %s failed: %s" % (' '.join(cmdlist),
610
                                                      exc.output))
611

    
612
    @staticmethod
613
    def _get_devices(lun):
614
        """ Get the devices mapped to host, corresponding to LUN id lun
615

616
        @type lun: integer
617
        @param lun: LUN id
618
        @rtype: list of strings
619
        @return: devices discovered by host or not
620

621
        """
622
        return glob.glob("%s/*lun%s" % (_DEV_BYPATH, lun))
623

    
624
    def _devices_discovered(self, lun):
625
        """ Make sure the devices get created on the host.
626

627
        @type lun: integer
628
        @param lun: LUN id
629
        @rtype: bool
630
        @return: devices discovered by host or not
631

632
        """
633
        return len(self._get_devices(lun)) == 4
634

    
635
    def _devices_add(self, lun):
636
        """ Rescan SCSI / FC bus for new devices.
637

638
        @type lun: integer
639
        @param lun: LUN id
640

641
        """
642
        self._check_call([_RESCAN_SCSI_CMD])
643
        if self._devices_discovered(lun) is False:
644
            raise SVCError("SCSI bus rescan failed to properly add the new "
645
                           "devices")
646

    
647
    def _devices_remove(self, lun):
648
        """ Remove scanned devices.
649

650
        @type device: string
651
        @param device: device string
652
        @type lun: integer
653
        @param lun: LUN id
654

655
        """
656
        # Which one is better?:
657
        #dm_device = os.path.split(os.readlink(device))[-1]
658
        #dm_slaves = os.listdir(_SYSFS_SLAVES_PATH % {'blockdev': dm_device})
659
        dm_slaves = self._get_devices(lun)
660

    
661
        # Delete the disks
662
        try:
663
            for slave in dm_slaves:
664
                delete_path = _SYSFS_FCDELETE_PATH % {'blockdev': slave}
665
                with open(delete_path, 'w') as delete_bdev_file:
666
                    delete_bdev_file.write('1')
667
        except IOError as exc:
668
            raise SVCError("Cannot delete FC devices: %s" % exc)
669

    
670
        if self._devices_discovered(lun) is True:
671
            raise SVCError('Failed to remove FC devices')
672

    
673
    def _multipath_ok(self, device):
674
        """ Check if multipath device is set up correctly.
675

676
        @type device: string
677
        @param device: device string
678
        @rtype: bool
679
        @return: multipath device ok or not
680

681
        """
682
        self._check_call([_DMSETUP_CMD, 'info', device])
683
        return self._check_output([_MULTIPATH_CMD, '-ll', device]) != ""
684

    
685
    def _multipath_setup(self, device):
686
        """ Setup multpath / dm for newly discovered devices.
687

688
        @type device: string
689
        @param device: device string
690

691
        """
692
        self._check_call([_MULTIPATH_CMD])
693
        if self._multipath_ok(device) is False:
694
            raise SVCError("Multipath / DM setup failed")
695

    
696
    def _multipath_remove(self, device):
697
        """ Tear down multipath.
698

699
        @type device: string
700
        @param device: device string
701

702
        """
703
        self._check_call([_MULTIPATH_CMD, '-f', device])
704
        if self._multipath_ok(device) is True:
705
            raise SVCError("Unable to reove multipath device")
706

    
707
    def _host_device_remove(self, device, lun):
708
        """ Tear down multipath and remove the scanned devices.
709

710
        @type device: string
711
        @param device: device string
712
        @type lun: integer
713
        @type lun: LUN id
714

715
        """
716
        self._multipath_remove(device)
717
        self._devices_remove(lun)
718

    
719
    def _host_device_setup(self, device, lun):
720
        """ Discover and setup the devices on the host machine.
721

722
        @type device: string
723
        @param device: device string
724
        @type lun: integer
725
        @type lun: LUN id
726

727
        """
728
        try:
729
            self._devices_add(lun)
730
            self._multipath_setup(device)
731
        except:
732
            # Cleanup any devices added by SCSI bus rescan
733
            # and remove device mappings
734
            self._host_device_remove(device, lun)
735
            raise
736

    
737
    def _get_device_info(self, volume):
738
        """ Return device_string and lun id for a volume.
739

740
        @type volume: string
741
        @param volume: volume name
742
        @rtype: tuple
743
        @return: tuple containing device string and LUN id
744

745
        """
746
        if not self._is_vdisk_defined(volume):
747
            raise SVCError("Volume not found")
748

    
749
        volume_attributes = self._get_vdisk_attributes(volume)
750

    
751
        try:
752
            vdisk_id = int(volume_attributes['vdisk_UID'])
753
            lun_id = int(volume_attributes['SCSI_id'])
754
        except (KeyError, ValueError):
755
            raise SVCError("Malformed vdisk attributes")
756

    
757
        device_string = "/dev/mapper/%s%s" % (_SCSI_DEV_PREFIX, vdisk_id)
758

    
759
        return (device_string, lun_id)
760

    
761
    def attach(self):
762
        """ Attach vdisk. """
763
        if self._vol_size is not None:
764
            raise SVCError("Volume size is set, when it shouldn't")
765

    
766
        volume_name = self._vol_name
767

    
768
        device_string, lun_id = self._get_device_info(volume_name)
769

    
770
        check_list = self._verify_checklist()
771
        if self._consistent_unmap(check_list):
772
            self._map_vol_to_host(volume_name, self._host_id)
773
            self._host_device_setup(device_string, lun_id)
774
            if not self._consistent_map(check_list):
775
                raise SVCError("Attach failed")
776
        elif not self._consistent_map(check_list):
777
            raise SVCError("Incosistent map state")
778

    
779
        sys.stdout.write('%s' % device_string)
780

    
781
    def create(self):
782
        """ Create a new vdisk. """
783

    
784
        if self._vol_size is None:
785
            raise SVCError("Missing volume size for create")
786

    
787
        if self._is_vdisk_defined(self._vol_name):
788
            raise SVCError("Volume exists")
789

    
790
        assert(self._svc_pool == _SVC_POOL_NAME)
791
        ssh_cmd = ['svctask', 'mkvdisk', '-name', self._vol_name, '-mdiskgrp',
792
                   self._svc_pool, '-size', str(self._vol_size)]
793
        out, err = self._svc_connection.run_ssh_cmd(ssh_cmd)
794
        self._assert_ssh_return(len(out.strip()), '_create_vdisk',
795
                                ssh_cmd, out, err)
796

    
797
        # Ensure that the output is as expected
798
        match_obj = re.search(r'Virtual Disk, id \[([0-9]+)\], '
799
                              'successfully created', out)
800
        # Make sure we got a "successfully created" message with vdisk id
801
        self._driver_assert(
802
            match_obj is not None,
803
            '_create_vdisk %(name)s - did not find '
804
            'success message in CLI output.\n '
805
            'stdout: %(out)s\n stderr: %(err)s'
806
            % {'name': self._vol_name, 'out': str(out), 'err': str(err)})
807

    
808
    def detach(self):
809
        """ Detach vdisk. """
810
        if self._vol_size is not None:
811
            raise SVCError("Volume size is set, when it shouldn't")
812

    
813
        volume_name = self._vol_name
814
        check_list = self._verify_checklist()
815

    
816
        if self._consistent_map(check_list):
817
            device_string, lun_id = self._get_device_info(volume_name)
818

    
819
            self._host_device_remove(device_string, lun_id)
820
            self._unmap_vol_from_host(volume_name, self._host_id)
821
            if not self._consistent_unmap(check_list):
822
                raise SVCError("Detatch failed")
823
        elif not self._consistent_unmap(check_list):
824
            raise SVCError("Incosistent map")
825

    
826
    def remove(self):
827
        """ Deletes existing vdisks. """
828
        if self._vol_size is not None:
829
            raise SVCError("Volume size is set, when it shouldn't")
830

    
831
        volume_name = self._vol_name
832

    
833
        if self._is_vdisk_mapped(volume_name, self._host_id):
834
            raise SVCError('Tried to delete mapped volume %s' % volume_name)
835

    
836
        # Try to delete volume only if found on the storage
837
        vdisk_defined = self._is_vdisk_defined(volume_name)
838
        if not vdisk_defined:
839
            raise SVCError('warning: Tried to delete vdisk %s but it does not '
840
                           'exist.' % volume_name)
841

    
842
        vdisk_attributes = self._get_vdisk_attributes(volume_name)
843
        assert(vdisk_attributes['mdisk_grp_name'] == _SVC_POOL_NAME)
844
        ssh_cmd = ['svctask', 'rmvdisk', '-force', volume_name]
845
        out, err = self._svc_connection.run_ssh_cmd(ssh_cmd)
846
        # No output should be returned from rmvdisk
847
        self._assert_ssh_return(len(out.strip()) == 0,
848
                                ('_delete_vdisk %(name)s')
849
                                % {'name': volume_name},
850
                                ssh_cmd, out, err)
851

    
852
    def grow(self):
853
        """ Resize vdisk. """
854
        volume_name = self._vol_name
855

    
856
        if self._vol_size is None:
857
            raise SVCError("Missing volume size for grow")
858

    
859
        new_size = self._vol_size
860

    
861
        # size is given in mb, convert to bytes
862
        new_size <<= 20
863

    
864
        vdisk_attributes = self._get_vdisk_attributes(volume_name)
865
        old_size = int(vdisk_attributes['capacity'])
866

    
867
        shrink = old_size > new_size
868
        diff = abs(new_size - old_size)
869

    
870
        assert(vdisk_attributes['mdisk_grp_name'] == _SVC_POOL_NAME)
871
        if shrink:
872
            ssh_cmd = (['svctask', 'shrinkvdisksize', '-size', str(diff),
873
                        '-unit', 'bytes', volume_name])
874
            out, err = self._svc_connection.run_ssh_cmd(ssh_cmd)
875
            # No output should be returned from expandvdisksize
876
            self._assert_ssh_return(len(out.strip()) == 0, 'extend_volume',
877
                                    ssh_cmd, out, err)
878
        else:
879
            ssh_cmd = (['svctask', 'expandvdisksize', '-size', str(diff),
880
                        '-unit', 'bytes', volume_name])
881
            out, err = self._svc_connection.run_ssh_cmd(ssh_cmd)
882
            # No output should be returned from expandvdisksize
883
            self._assert_ssh_return(len(out.strip()) == 0, 'extend_volume',
884
                                    ssh_cmd, out, err)
885

    
886
    def _verify_checklist(self):
887
        """ Check for consistent mappings.
888

889
        @rtype: list of integers
890
        @return: verification checks
891

892
        """
893

    
894
        device, lun = self._get_device_info(self._vol_name)
895

    
896
        check_list = [self._is_vdisk_defined(self._vol_name),
897
                      self._is_vdisk_mapped(self._vol_name, self._host_id),
898
                      self._devices_discovered(lun),
899
                      self._multipath_ok(device)]
900

    
901
        return check_list
902

    
903
    @staticmethod
904
    def _consistent_map(check_list):
905
        """ Check for consistent map.
906

907
        @type value: integer
908
        @param value: verification check sum
909
        @rtype: bool
910
        @return: volume mapped end-to-end or not
911

912
        """
913
        return all(check_list)
914

    
915
    @staticmethod
916
    def _consistent_unmap(check_list):
917
        """ Check for consistent unmap.
918

919
        @type value: integer
920
        @param value: verification check sum
921
        @rtype: bool
922
        @return: volume mapped end-to-end or not
923

924
        """
925
        return not any(check_list)
926

    
927
    def verify(self):
928
        """ Verify op.
929

930
        """
931
        if self._vol_size is not None:
932
            raise SVCError("Volume size is set, when it shouldn't")
933

    
934
        if not self._consistent_map(self._verify_checklist()):
935
            raise SVCError("Verify op failed")
936

    
937

    
938
def main():
939
    """Read env variables and SAN conf and branch to the requested function.
940

941
    """
942

    
943
    try:
944
        svc = SVCProvider(_SVC_CONFIG_PATH)
945

    
946
        try:
947
            action = {
948
                'attach': svc.attach,
949
                'create': svc.create,
950
                'detach': svc.detach,
951
                'grow': svc.grow,
952
                'remove': svc.remove,
953
                'verify': svc.verify,
954
            }[os.path.basename(sys.argv[0])]
955
        except KeyError:
956
            sys.stderr.write("Op not supported\n")
957
            return 1
958

    
959
        action()
960
        return 0
961
    except SVCError as exc:
962
        sys.stderr.write("%s\n" % exc)
963
    except Exception as exc:
964
        sys.stderr.write(traceback.format_exc())
965
        sys.stderr.write("Unexpected error: %s\n" % exc)
966

    
967
    return 1
968

    
969
if __name__ == '__main__':
970
    sys.exit(main())