Revision ed2ee198

b/svc/.gitignore
1
*pyc
2
*.swp
b/svc/TODO
1
- Fix _is_vdisk_defined (see FIXME comment there)
2
- Code duplication (subprocess wrappers, anything else?)
3
- Config file opts 'parsing' (too complex? )
4
- Consistent use of datatypes (int for VOL_SIZE, port, host_id, string
5
  for everything else). Related to the above.
6
- Make consistent use of exceptions / consistent error handling
7
- What are the semantics of the verify op?
8
- Refactoring:
9
    *) _get_params / _read_config are not 'consistent' (set attrs and
10
       return some opts for SVC connection)
11
    *) make the interface more clear (how do we init the provider,
12
       SVC connection, how to we issue the ops, do we need oop /
13
       classes
14
- Check code taken from the original cinder driver (driver_assert,
15
  paramiko code, svc cmds)
16
- Verify multipath / dmsetup / scsi rescan cmds work as intended
17
  (and that they work ok with udev, when certain rules are disabled)
18
- Lots of rtts to the SVC for no reason (is_vdisk_defined, get_attrs
19
  etc). Maybe have a separate volume class (overkill) or private attrs
20
  for volume specific stuff?
21
- Unittests probably need rewriting from scratch
22
- We currently rely on rescan-scsi-bus etc for scanning. Check which
23
  package provides it (probably scsi-tools). Fallback to manual
24
  scanning (via sysfs files) if not available?
25
- Fix vdisk attribute getting (wrapper fun to return SVCError on key
26
  error / malformed vdiks attrs
b/svc/example_san.config
1
[svc_san_identifier]
2
host = 10.0.2.1
3
port = 22
4
user = myuser
5
password = uberpass
6
pool = synnefo
7
host_id = 2
b/svc/parameter.list
1
svc_san SVC SAN identifier in the INI config file
b/svc/storwize_svc.py
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())
b/svc/test_configs/bad_config_bad_hostid
1
[svc_san_identifier]
2
host = 10.0.2.1
3
port = 22
4
user = myuser
5
password = uberpass
6
pool = synnefo
7
host_id = -2
b/svc/test_configs/bad_config_bad_port
1
[svc_san_identifier]
2
host = 10.0.2.1
3
port = test
4
user = myuser
5
password = uberpass
6
pool = synnefo
7
host_id = 2
b/svc/test_configs/bad_config_empty_host
1
[svc_san_identifier]
2
host =
3
port = 22
4
user = myuser
5
password = uberpass
6
pool = synnefo
7
host_id = 2
b/svc/test_configs/bad_config_invalid_format
1
[svc_san_identifier]
2
host = 10.0.2.1
3
foo = bar
4
port = 22
5
user = myuser
6
password = uberpass
7
pool = synnefo
8
host_id = 2
b/svc/test_configs/bad_config_missing_fields
1
[svc_san_identifier]
2
host = 10.0.2.1
3
port = 22
4
password = uberpass
5
pool = synnefo
6
host_id = 2
b/svc/test_configs/bad_config_missing_section
1
[svc_san_identifier1]
2
host = 10.0.2.1
3
port = 22
4
user = myuser
5
password = uberpass
6
pool = synnefo
7
host_id = 2
b/svc/test_configs/bad_config_section
1
host = 10.0.2.1
2
port = 22
3
user = myuser
4
password = uberpass
5
pool = synnefo
6
host_id = 2
b/svc/test_configs/good_config
1
[svc_san_identifier]
2
host = 10.0.2.1
3
port = 22
4
user = myuser
5
password = uberpass
6
pool = VM_POOL_SYNNEFO
7
host_id = 2
b/svc/test_configs/good_config_extra_empty_sections
1
[svc_san_identifier]
2
host = 10.0.2.1
3
port = 22
4
user = myuser
5
password = uberpass
6
pool = synnefo
7
host_id = 2
8

  
9
[svc_san_identifier1]
10
host = 10.0.2.1
11
pool = synnefo
12
host_id = 2
13
[svc_san2]
b/svc/test_configs/good_config_extra_sections
1
[svc_san_identifier]
2
host = 10.0.2.1
3
port = 22
4
user = myuser
5
password = uberpass
6
pool = synnefo
7
host_id = 2
8

  
9
[svc_san_identifier1]
10
host = 10.0.2.1
11
pool = synnefo
12
host_id = 2
b/svc/tests.py
1
#!/usr/bin/env python
2
#
3
# Copyright (c) 2010-2013 Greek Research and Technology Network S.A.
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13
# General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18
# 02110-1301, USA.
19

  
20

  
21
try:
22
    import unittest2 as unittest
23
except ImportError:
24
    import unittest
25

  
26
import os
27
from mock import patch
28
from storwize_svc import *
29
import subprocess
30

  
31

  
32
@patch('paramiko.SSHClient', autospec=True)
33
class SVCConnectionTest(unittest.TestCase):
34
    def test_ssh_injection_1(self, ssh_conn):
35
        self.conn = SVCConnection('127.0.0.1', 22, 'user', 'userpass')
36
        cmd_list = ['ls', '- l']
37
        self.assertRaises(SVCError, self.conn.run_ssh_cmd, cmd_list)
38

  
39
    def test_ssh_injection_2(self, ssh_conn):
40
        self.conn = SVCConnection('127.0.0.1', 22, 'user', 'userpass')
41
        cmd_list = ['ls', ';cat>/dev/sda</dev/zero']
42
        self.assertRaises(SVCError, self.conn.run_ssh_cmd, cmd_list)
43

  
44
    @patch('paramiko.ChannelFile')
45
    def test_initial_connection_ok(self, chan, ssh_conn):
46
        self.conn = SVCConnection('127.0.0.1', 22, 'user', 'userpass')
47
        cmd_list = ['ls', '-l']
48
        chan.channel.recv_exit_status.return_value = 0
49
        ssh_conn.return_value.exec_command.return_value = (chan, chan, chan)
50
        self.conn.run_ssh_cmd(cmd_list)
51
        kwargs = {'port': 22,
52
                  'username': 'user',
53
                  'password': 'userpass',
54
                  'timeout': 30,
55
                  'look_for_keys': False,
56
                  'allow_agent': False}
57

  
58
        ssh_conn.return_value.connect.assert_called_once_with('127.0.0.1',
59
                                                              **kwargs)
60

  
61
    @patch('paramiko.ChannelFile')
62
    def test_initial_connection_pooled(self, chan, ssh_conn):
63
        self.conn = SVCConnection('127.0.0.1', 22, 'user', 'userpass')
64
        cmd_list = ['ls', '-l']
65
        chan.channel.recv_exit_status.return_value = 0
66
        ssh_conn.return_value.exec_command.return_value = (chan, chan, chan)
67
        self.conn.run_ssh_cmd(cmd_list)
68
        self.conn.run_ssh_cmd(cmd_list)
69
        kwargs = {'port': 22,
70
                  'username': 'user',
71
                  'password': 'userpass',
72
                  'timeout': 30,
73
                  'look_for_keys': False,
74
                  'allow_agent': False}
75

  
76
        ssh_conn.return_value.connect.assert_called_once_with('127.0.0.1',
77
                                                              **kwargs)
78

  
79
    @patch('paramiko.ChannelFile')
80
    def test_initial_connection_fail(self, chan, ssh_conn):
81
        self.conn = SVCConnection('127.0.0.1', 22, 'user', 'userpass')
82
        cmd_list = ['ls', '-l']
83
        chan.channel.recv_exit_status.return_value = -1
84
        ssh_conn.return_value.exec_command.return_value = (chan, chan, chan)
85
        self.assertRaises(SVCError, self.conn.run_ssh_cmd, cmd_list)
86

  
87
    @patch('paramiko.ChannelFile')
88
    def test_initial_connection_out(self, chan, ssh_conn):
89
        self.conn = SVCConnection('127.0.0.1', 22, 'user', 'userpass')
90
        cmd_list = ['ls', '-l']
91
        chan.channel.recv_exit_status.return_value = 0
92
        chan.read.return_value = "test"
93
        ssh_conn.return_value.exec_command.return_value = (chan, chan, chan)
94
        out, err = self.conn.run_ssh_cmd(cmd_list)
95
        self.assertEqual(out, "test")
96
        self.assertEqual(err, "test")
97

  
98
    @patch('paramiko.ChannelFile')
99
    def test_initial_connection_pooled_retry(self, chan, ssh_conn):
100
        self.conn = SVCConnection('127.0.0.1', 22, 'user', 'userpass')
101
        cmd_list = ['ls', '-l']
102
        chan.channel.recv_exit_status.return_value = 0
103
        ssh_conn.return_value.exec_command.return_value = (chan, chan, chan)
104
        self.conn.run_ssh_cmd(cmd_list)
105
        transport = ssh_conn.return_value.get_transport.return_value
106
        transport.is_active.return_value = False
107
        self.conn.run_ssh_cmd(cmd_list)
108
        self.assertEqual(ssh_conn.return_value.connect.call_count, 2)
109

  
110

  
111
@patch('storwize_svc.SVCConnection', autospec=True)
112
class SVCInitTest(unittest.TestCase):
113
    def test_missing_config(self, svc_conn):
114
        self.assertRaises(TypeError, SVCProvider)
115

  
116
    def test_missing_env(self, svc_conn):
117
        self.assertRaises(SVCError, SVCProvider, './test_configs/some_config')
118

  
119
    @patch.dict(os.environ, {'VOL_SIZE': '1', 'EXTP_SVC_SAN': 'mysan'})
120
    def test_missing_vol_name(self, svc_conn):
121
        self.assertRaises(SVCError, SVCProvider, './test_configs/some_config')
122

  
123
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '1'})
124
    def test_missing_san(self, svc_conn):
125
        self.assertRaises(SVCError, SVCProvider, './test_configs/some_config')
126

  
127
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '1',
128
                             'EXTP_SVC_SAN': 'mysan'})
129
    def test_bad_config_path(self, svc_conn):
130
        self.assertRaises(SVCError, SVCProvider, './test_configs/some_config')
131

  
132
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'EXTP_SVC_SAN': 'mysan'})
133
    def test_bad_identifier(self, svc_conn):
134
        self.assertRaises(SVCError, SVCProvider, './test_configs/good_config')
135

  
136
    @patch.dict(os.environ, {'VOL_NAME': 'myvol',
137
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
138
    def test_missing_size(self, svc_conn):
139

  
140
        svc = SVCProvider('./test_configs/good_config')
141

  
142
        self.assertEqual(svc._vol_name, 'myvol')
143
        self.assertIsNone(svc._vol_size)
144
        self.assertEqual(svc._svc_pool, 'VM_POOL_SYNNEFO')
145
        self.assertEqual(svc._host_id, 2)
146

  
147
        svc_conn.assert_called_once_with('10.0.2.1', 22, 'myuser',
148
                                         'uberpass')
149

  
150
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
151
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
152
    def test_good_config(self, svc_conn):
153
        svc = SVCProvider('./test_configs/good_config')
154

  
155
        self.assertEqual(svc._vol_name, 'myvol')
156
        self.assertEqual(svc._vol_size, 100)
157
        self.assertEqual(svc._svc_pool, 'VM_POOL_SYNNEFO')
158
        self.assertEqual(svc._host_id, 2)
159

  
160
        svc_conn.assert_called_once_with('10.0.2.1', 22, 'myuser',
161
                                         'uberpass')
162

  
163
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
164
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
165
    def test_good_config_extra_sections(self, svc_conn):
166
        svc = SVCProvider('./test_configs/good_config_extra_sections')
167

  
168
        self.assertEqual(svc._vol_name, 'myvol')
169
        self.assertEqual(svc._vol_size, 100)
170
        self.assertEqual(svc._svc_pool, 'synnefo')
171
        self.assertEqual(svc._host_id, 2)
172

  
173
        svc_conn.assert_called_once_with('10.0.2.1', 22, 'myuser',
174
                                         'uberpass')
175

  
176
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
177
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
178
    def test_good_config_extra_empty_sections(self, svc_conn):
179
        svc = SVCProvider('./test_configs/good_config_extra_empty_sections')
180

  
181
        self.assertEqual(svc._vol_name, 'myvol')
182
        self.assertEqual(svc._vol_size, 100)
183
        self.assertEqual(svc._svc_pool, 'synnefo')
184
        self.assertEqual(svc._host_id, 2)
185

  
186
        svc_conn.assert_called_once_with('10.0.2.1', 22, 'myuser',
187
                                         'uberpass')
188

  
189
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
190
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
191
    def test_bad_config_section(self, svc_conn):
192
        self.assertRaises(SVCError, SVCProvider,
193
                          './test_configs/bad_config_section')
194

  
195
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
196
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
197
    def test_bad_config_missing_section(self, svc_conn):
198
        self.assertRaises(SVCError, SVCProvider,
199
                          './test_configs/bad_config_missing_section')
200

  
201
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
202
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
203
    def test_bad_config_missing_fields(self, svc_conn):
204
        self.assertRaises(SVCError, SVCProvider,
205
                          './test_configs/bad_config_missing_fields')
206

  
207
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
208
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
209
    def test_bad_config_invalid_format(self, svc_conn):
210
        self.assertRaises(SVCError, SVCProvider,
211
                          './test_configs/bad_config_invalid_format')
212

  
213
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
214
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
215
    def test_bad_config_empty_host(self, svc_conn):
216
        self.assertRaises(SVCError, SVCProvider,
217
                          './test_configs/bad_config_empty_host')
218

  
219
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
220
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
221
    def test_bad_config_bad_port(self, svc_conn):
222
        self.assertRaises(SVCError, SVCProvider,
223
                          './test_configs/bad_config_bad_port')
224

  
225
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
226
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
227
    def test_bad_config_bad_hostid(self, svc_conn):
228
        self.assertRaises(SVCError, SVCProvider,
229
                          './test_configs/bad_config_bad_hostid')
230

  
231
    @patch.dict(os.environ, {'VOL_NAME': '', 'VOL_SIZE': '1',
232
                             'EXTP_SVC_SAN': 'mysan'})
233
    def test_empty_volume_name(self, svc_conn):
234
        self.assertRaises(SVCError, SVCProvider, './some_config')
235

  
236
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '',
237
                             'EXTP_SVC_SAN': 'mysan'})
238
    def test_empty_volume_size(self, svc_conn):
239
        self.assertRaises(SVCError, SVCProvider, './some_config')
240

  
241
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': 'test',
242
                             'EXTP_SVC_SAN': 'mysan'})
243
    def test_bad_volume_size(self, svc_conn):
244
        self.assertRaises(SVCError, SVCProvider, './some_config')
245

  
246
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '-1',
247
                             'EXTP_SVC_SAN': 'mysan'})
248
    def test_negative_volume_size(self, svc_conn):
249
        self.assertRaises(SVCError, SVCProvider, './some_config')
250

  
251
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '-1',
252
                             'EXTP_SVC_SAN': ''})
253
    def test_empty_svc_identifier(self, svc_conn):
254
        self.assertRaises(SVCError, SVCProvider, './some_config')
255

  
256

  
257
class SVCEnvOpsTest(unittest.TestCase):
258
    @patch.dict(os.environ, {'VOL_NAME': 'myvol',
259
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
260
    def test_create_no_size(self):
261
        svc = SVCProvider('./test_configs/good_config')
262
        self.assertRaises(SVCError, svc.create)
263

  
264
    @patch.dict(os.environ, {'VOL_NAME': 'myvol',
265
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
266
    def test_grow_no_size(self):
267
        svc = SVCProvider('./test_configs/good_config')
268
        self.assertRaises(SVCError, svc.grow)
269

  
270
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
271
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
272
    def test_detach_size(self):
273
        svc = SVCProvider('./test_configs/good_config')
274
        self.assertRaises(SVCError, svc.detach)
275

  
276
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
277
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
278
    def test_attach_size(self):
279
        svc = SVCProvider('./test_configs/good_config')
280
        self.assertRaises(SVCError, svc.attach)
281

  
282
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
283
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
284
    def test_verify_size(self):
285
        svc = SVCProvider('./test_configs/good_config')
286
        self.assertRaises(SVCError, svc.verify)
287

  
288
    @patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
289
                             'EXTP_SVC_SAN': 'svc_san_identifier'})
290
    def test_remove_size(self):
291
        svc = SVCProvider('./test_configs/good_config')
292
        self.assertRaises(SVCError, svc.remove)
293

  
294

  
295
@patch('subprocess.check_output', autospec=True)
296
@patch('subprocess.check_call', autospec=True)
297
@patch.dict(os.environ, {'VOL_NAME': 'myvol', 'VOL_SIZE': '100',
298
                         'EXTP_SVC_SAN': 'svc_san_identifier'})
299
class SVCInternalHostOpsTest(unittest.TestCase):
300
    def test_check_call_ok(self, call, out):
301
        svc = SVCProvider('./test_configs/good_config')
302
        svc._check_call(['ls', '-l'])
303

  
304
    def test_check_call_exc(self, call, out):
305
        svc = SVCProvider('./test_configs/good_config')
306
        call.side_effect = subprocess.CalledProcessError(1, "test",
307
                                                         output="test")
308
        self.assertRaises(SVCError, svc._check_call, ['ls', '-l'])
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff