summaryrefslogtreecommitdiff
path: root/rtslib/utils.py
blob: 26f453f7dfe50900a10c2b549d13e151ec7a2f3e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
'''
Provides various utility functions.

This file is part of RTSLib Community Edition.
Copyright (c) 2011 by RisingTide Systems LLC

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, version 3 (AGPLv3).

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
'''

import re
import os
import stat
import uuid
import glob
import socket
import subprocess

class RTSLibError(Exception):
    '''
    Generic rtslib error.
    '''
    pass

class RTSLibBrokenLink(RTSLibError):
    '''
    Broken link in configfs, i.e. missing LUN storage object.
    '''
    pass

class RTSLibNotInCFS(RTSLibError):
    '''
    The underlying configfs object does not exist. Happens when
    calling methods of an object that is instanciated but have
    been deleted from congifs, or when trying to lookup an
    object that does not exist.
    '''
    pass

def fwrite(path, string):
    '''
    This function writes a string to a file, and takes care of
    opening it and closing it. If the file does not exist, it
    will be created.

    >>> from rtslib.utils import *
    >>> fwrite("/tmp/test", "hello")
    >>> fread("/tmp/test")
    'hello'

    @param path: The file to write to.
    @type path: string
    @param string: The string to write to the file.
    @type string: string

    '''
    path = os.path.realpath(str(path))
    file_fd = open(path, 'w')
    try:
        file_fd.write("%s" % string)
    finally:
        file_fd.close()

def fread(path):
    '''
    This function reads the contents of a file.
    It takes care of opening and closing it.

    >>> from rtslib.utils import *
    >>> fwrite("/tmp/test", "hello")
    >>> fread("/tmp/test")
    'hello'
    >>> fread("/tmp/notexistingfile") # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    IOError: [Errno 2] No such file or directory: '/tmp/notexistingfile'

    @param path: The path to the file to read from.
    @type path: string
    @return: A string containing the file's contents.

    '''
    path = os.path.realpath(str(path))
    string = ""
    file_fd = open(path, 'r')
    try:
        string = file_fd.read().strip()
    finally:
        file_fd.close()

    return string

def is_dev_in_use(path):
    '''
    This function will check if the device or file referenced by path is
    already mounted or used as a storage object backend.  It works by trying to
    open the path with O_EXCL flag, which will fail if someone else already
    did.  Note that the file is closed before the function returns, so this
    does not guaranteed the device will still be available after the check.
    @param path: path to the file of device to check
    @type path: string
    @return: A boolean, True is we cannot get exclusive descriptor on the path,
             False if we can.
    '''
    path = os.path.realpath(str(path))
    try:
        file_fd = os.open(path, os.O_EXCL|os.O_NDELAY)
    except OSError:
        return True
    else:
        os.close(file_fd)
        return False

def is_disk_partition(path):
    '''
    Try to find out if path is a partition of a TYPE_DISK device.
    Handles both /dev/sdaX and /dev/disk/by-*/*-part? schemes.
    '''
    regex = re.match(r'([a-z/]+)([1-9]+)$', path)
    if not regex:
        regex = re.match(r'(/dev/disk/.+)(-part[1-9]+)$', path)
    if not regex:
        return False
    else:
        if get_block_type(regex.group(1)) == 0:
            return True

def get_disk_size(path):
    '''
    This function returns the size in bytes of a disk-type
    block device, or None if path does not point to a disk-
    type device.
    '''
    (major, minor) = get_block_numbers(path)
    if major is None:
        return None
    # list of [major, minor, #blocks (1K), name
    partitions = [ x.split()[0:4]
                  for x in fread("/proc/partitions").split("\n")[2:] if x]
    size = None
    for partition in partitions:
        if partition[0:2] == [str(major), str(minor)]:
            size = int(partition[2]) * 1024
            break
    return size

def get_block_numbers(path):
    '''
    This function returns a (major,minor) tuple for the block
    device found at path, or (None, None) if path is
    not a block device.
    '''
    dev = os.path.realpath(path)
    try:
        mode = os.stat(dev)
    except OSError:
        return (None, None)

    if not stat.S_ISBLK(mode[stat.ST_MODE]):
        return (None, None)

    major = os.major(mode.st_rdev)
    minor = os.minor(mode.st_rdev)
    return (major, minor)

def get_block_type(path):
    '''
    This function returns a block device's type.
    Example: 0 is TYPE_DISK
    If no match is found, None is returned.

    >>> from rtslib.utils import *
    >>> get_block_type("/dev/sda")
    0
    >>> get_block_type("/dev/sr0")
    5
    >>> get_block_type("/dev/scd0")
    5
    >>> get_block_type("/dev/nodevicehere") is None
    True

    @param path: path to the block device
    @type path: string
    @return: An int for the block device type, or None if not a block device.
    '''
    dev = os.path.realpath(path)
    # TODO: Make adding new majors on-the-fly possible, using some config file
    # for instance, maybe an additionnal list argument, or even a match all
    # mode for overrides ?

    # Make sure we are dealing with a block device
    (major, minor) = get_block_numbers(dev)
    if major is None:
        return None

    # Treat disk partitions as TYPE_DISK
    if is_disk_partition(path):
        return 0

    # These devices are disk type block devices, but might not report this
    # correctly in /sys/block/xxx/device/type, so use their major number.
    type_disk_known_majors = [1,    # RAM disk
                              8,    # SCSI disk devices
                              9,    # Metadisk RAID devices
                              13,   # 8-bit MFM/RLL/IDE controller
                              19,   # "Double" compressed disk
                              21,   # Acorn MFM hard drive interface
                              30,   # FIXME: Normally 'Philips LMS CM-205
                                    # CD-ROM' in the Linux devices list but
                                    # used by Cirtas devices.
                              35,   # Slow memory ramdisk
                              36,   # MCA ESDI hard disk
                              37,   # Zorro II ramdisk
                              43,   # Network block devices
                              44,   # Flash Translation Layer (FTL) filesystems
                              45,   # Parallel port IDE disk devices
                              47,   # Parallel port ATAPI disk devices
                              48,   # Mylex DAC960 PCI RAID controller
                              48,   # Mylex DAC960 PCI RAID controller
                              49,   # Mylex DAC960 PCI RAID controller
                              50,   # Mylex DAC960 PCI RAID controller
                              51,   # Mylex DAC960 PCI RAID controller
                              52,   # Mylex DAC960 PCI RAID controller
                              53,   # Mylex DAC960 PCI RAID controller
                              54,   # Mylex DAC960 PCI RAID controller
                              55,   # Mylex DAC960 PCI RAID controller
                              58,   # Reserved for logical volume manager
                              59,   # Generic PDA filesystem device
                              60,   # LOCAL/EXPERIMENTAL USE
                              61,   # LOCAL/EXPERIMENTAL USE
                              62,   # LOCAL/EXPERIMENTAL USE
                              63,   # LOCAL/EXPERIMENTAL USE
                              64,   # Scramdisk/DriveCrypt encrypted devices
                              65,   # SCSI disk devices (16-31)
                              66,   # SCSI disk devices (32-47)
                              67,   # SCSI disk devices (48-63)
                              68,   # SCSI disk devices (64-79)
                              69,   # SCSI disk devices (80-95)
                              70,   # SCSI disk devices (96-111)
                              71,   # SCSI disk devices (112-127)
                              72,   # Compaq Intelligent Drive Array
                              73,   # Compaq Intelligent Drive Array
                              74,   # Compaq Intelligent Drive Array
                              75,   # Compaq Intelligent Drive Array
                              76,   # Compaq Intelligent Drive Array
                              77,   # Compaq Intelligent Drive Array
                              78,   # Compaq Intelligent Drive Array
                              79,   # Compaq Intelligent Drive Array
                              80,   # I2O hard disk
                              80,   # I2O hard disk
                              81,   # I2O hard disk
                              82,   # I2O hard disk
                              83,   # I2O hard disk
                              84,   # I2O hard disk
                              85,   # I2O hard disk
                              86,   # I2O hard disk
                              87,   # I2O hard disk
                              93,   # NAND Flash Translation Layer filesystem
                              94,   # IBM S/390 DASD block storage
                              96,   # Inverse NAND Flash Translation Layer
                              98,   # User-mode virtual block device
                              99,   # JavaStation flash disk
                              101,  # AMI HyperDisk RAID controller
                              102,  # Compressed block device
                              104,  # Compaq Next Generation Drive Array
                              105,  # Compaq Next Generation Drive Array
                              106,  # Compaq Next Generation Drive Array
                              107,  # Compaq Next Generation Drive Array
                              108,  # Compaq Next Generation Drive Array
                              109,  # Compaq Next Generation Drive Array
                              110,  # Compaq Next Generation Drive Array
                              111,  # Compaq Next Generation Drive Array
                              112,  # IBM iSeries virtual disk
                              114,  # IDE BIOS powered software RAID interfaces
                              115,  # NetWare (NWFS) Devices (0-255)
                              117,  # Enterprise Volume Management System
                              120,  # LOCAL/EXPERIMENTAL USE
                              121,  # LOCAL/EXPERIMENTAL USE
                              122,  # LOCAL/EXPERIMENTAL USE
                              123,  # LOCAL/EXPERIMENTAL USE
                              124,  # LOCAL/EXPERIMENTAL USE
                              125,  # LOCAL/EXPERIMENTAL USE
                              126,  # LOCAL/EXPERIMENTAL USE
                              127,  # LOCAL/EXPERIMENTAL USE
                              128,  # SCSI disk devices (128-143)
                              129,  # SCSI disk devices (144-159)
                              130,  # SCSI disk devices (160-175)
                              131,  # SCSI disk devices (176-191)
                              132,  # SCSI disk devices (192-207)
                              133,  # SCSI disk devices (208-223)
                              134,  # SCSI disk devices (224-239)
                              135,  # SCSI disk devices (240-255)
                              136,  # Mylex DAC960 PCI RAID controller
                              137,  # Mylex DAC960 PCI RAID controller
                              138,  # Mylex DAC960 PCI RAID controller
                              139,  # Mylex DAC960 PCI RAID controller
                              140,  # Mylex DAC960 PCI RAID controller
                              141,  # Mylex DAC960 PCI RAID controller
                              142,  # Mylex DAC960 PCI RAID controller
                              143,  # Mylex DAC960 PCI RAID controller
                              144,  # Non-device (e.g. NFS) mounts
                              145,  # Non-device (e.g. NFS) mounts
                              146,  # Non-device (e.g. NFS) mounts
                              147,  # DRBD device
                              152,  # EtherDrive Block Devices
                              153,  # Enhanced Metadisk RAID storage units
                              160,  # Carmel 8-port SATA Disks
                              161,  # Carmel 8-port SATA Disks
                              199,  # Veritas volume manager (VxVM) volumes
                              201,  # Veritas VxVM dynamic multipathing driver
                              230,  # ZFS ZVols
                              240,  # LOCAL/EXPERIMENTAL USE
                              241,  # LOCAL/EXPERIMENTAL USE
                              242,  # LOCAL/EXPERIMENTAL USE
                              243,  # LOCAL/EXPERIMENTAL USE
                              244,  # LOCAL/EXPERIMENTAL USE
                              245,  # LOCAL/EXPERIMENTAL USE
                              246,  # LOCAL/EXPERIMENTAL USE
                              247,  # LOCAL/EXPERIMENTAL USE
                              248,  # LOCAL/EXPERIMENTAL USE
                              249,  # LOCAL/EXPERIMENTAL USE
                              250,  # LOCAL/EXPERIMENTAL USE
                              251,  # LOCAL/EXPERIMENTAL USE
                              252,  # LOCAL/EXPERIMENTAL USE
                              253,  # LOCAL/EXPERIMENTAL USE
                              254   # LOCAL/EXPERIMENTAL USE
                             ]
    if major in type_disk_known_majors:
        return 0

    # Same for LVM LVs, but as we cannot use major here
    # (it varies accross distros), use the realpath to check
    if os.path.dirname(dev) == "/dev/mapper":
        return 0

    # list of (major, minor, type) tuples
    blocks = [(fread("%s/dev" % fdev).split(':')[0],
        fread("%s/dev" % fdev).split(':')[1],
        fread("%s/device/type" % fdev))
        for fdev in glob.glob("/sys/block/*")
        if os.path.isfile("%s/device/type" % fdev)]

    for block in blocks:
        if int(block[0]) == major and int(block[1]) == minor:
            return int(block[2])

    return None

def list_scsi_hbas():
    '''
    This function returns the list of HBA indexes for existing SCSI HBAs.
    '''
    return list(set([int(device.partition(":")[0])
        for device in os.listdir("/sys/bus/scsi/devices")
        if re.match("[0-9:]+", device)]))

def convert_scsi_path_to_hctl(path):
    '''
    This function returns the SCSI ID in H:C:T:L form for the block
    device being mapped to the udev path specified.
    If no match is found, None is returned.

    >>> import rtslib.utils as utils
    >>> utils.convert_scsi_path_to_hctl('/dev/scd0')
    (2, 0, 0, 0)
    >>> utils.convert_scsi_path_to_hctl('/dev/sr0')
    (2, 0, 0, 0)
    >>> utils.convert_scsi_path_to_hctl('/dev/sda')
    (3, 0, 0, 0)
    >>> utils.convert_scsi_path_to_hctl('/dev/sda1')
    >>> utils.convert_scsi_path_to_hctl('/dev/sdb')
    (3, 0, 1, 0)
    >>> utils.convert_scsi_path_to_hctl('/dev/sdc')
    (3, 0, 2, 0)

    @param path: The udev path to the SCSI block device.
    @type path: string
    @return: An (host, controller, target, lun) tuple of integer
    values representing the SCSI ID of the device, or None if no
    match is found.
    '''
    dev = os.path.realpath(path)
    scsi_devices = [os.path.basename(scsi_dev).split(':')
            for scsi_dev in glob.glob("/sys/class/scsi_device/*")]
    for (host, controller, target, lun) in scsi_devices:
        scsi_dev = convert_scsi_hctl_to_path(host, controller, target, lun)
        if dev == scsi_dev:
            return (int(host), int(controller), int(target), int(lun))

    return None

def convert_scsi_hctl_to_path(host, controller, target, lun):
    '''
    This function returns a udev path pointing to the block device being
    mapped to the SCSI device that has the provided H:C:T:L.

    >>> import rtslib.utils as utils
    >>> utils.convert_scsi_hctl_to_path(0,0,0,0)
    ''
    >>> utils.convert_scsi_hctl_to_path(2,0,0,0) # doctest: +ELLIPSIS
    '/dev/s...0'
    >>> utils.convert_scsi_hctl_to_path(3,0,2,0)
    '/dev/sdc'

    @param host: The SCSI host id.
    @type host: int
    @param controller: The SCSI controller id.
    @type controller: int
    @param target: The SCSI target id.
    @type target: int
    @param lun: The SCSI Logical Unit Number.
    @type lun: int
    @return: A string for the canonical path to the device, or empty string.
    '''
    try:
        host = int(host)
        controller = int(controller)
        target = int(target)
        lun = int(lun)
    except ValueError:
        raise RTSLibError(
            "The host, controller, target and lun parameter must be integers.")

    scsi_dev_path = "/sys/class/scsi_device"
    sysfs_names = [os.path.basename(name) for name
            in glob.glob("%s/%d:%d:%d:%d/device/block:*"
            % (scsi_dev_path, host, controller, target, lun))]
    if len(sysfs_names) == 0:
        sysfs_names = [os.path.basename(name) for name
                in glob.glob("%s/%d:%d:%d:%d/device/block/*"
                % (scsi_dev_path, host, controller, target, lun))]
    if len(sysfs_names) > 0:
        for name in sysfs_names:
            name1 = name.partition(":")[2].strip()
            if name1:
                name = name1
            dev = os.path.realpath("/dev/%s" % name)
            try:
                mode = os.stat(dev)[stat.ST_MODE]
            except OSError:
                pass
            if stat.S_ISBLK(mode):
                return dev
    else:
        return ''

def generate_wwn(wwn_type):
    '''
    Generates a random WWN of the specified type:
        - unit_serial: T10 WWN Unit Serial.
        - iqn: iSCSI IQN
        - naa: SAS NAA address
    @param wwn_type: The WWN address type.
    @type wwn_type: str
    @returns: A string containing the WWN.
    '''
    wwn_type = wwn_type.lower()
    if wwn_type == 'free':
        return str(uuid.uuid4())
    if wwn_type == 'unit_serial':
        return str(uuid.uuid4())
    elif wwn_type == 'iqn':
        localname = socket.gethostname().split(".")[0]
        localarch = os.uname()[4].replace("_","")
        prefix = "iqn.2003-01.org.linux-iscsi.%s.%s" % (localname, localarch)
        prefix = prefix.strip().lower()
        serial = "sn.%s" % str(uuid.uuid4())[24:]
        return "%s:%s" % (prefix, serial)
    elif wwn_type == 'naa':
        # see http://standards.ieee.org/develop/regauth/tut/fibre.pdf
        # 5 = IEEE registered
        # 001405 = OpenIB OUI (they let us use it I guess?)
        # rest = random
        return "naa.5001405" + uuid.uuid4().get_hex()[-9:]
    elif wwn_type == 'eui':
        return "eui.001405" + uuid.uuid4().get_hex()[-10:]
    else:
        raise ValueError("Unknown WWN type: %s." % wwn_type)

def colonize(str):
    '''
    helper function to add colons every 2 chars
    '''
    return ":".join(str[i:i+2] for i in range(0, len(str), 2))

def _cleanse_wwn(wwn_type, wwn):
    '''
    Some wwns may have alternate text representations. Adjust to our
    preferred representation.
    '''
    wwn = wwn.strip()

    if wwn_type in ('naa', 'eui'):
        if wwn.startswith("0x"):
            wwn = wwn[2:]
        wwn = wwn.translate(None, ":-")

        if not (wwn.startswith("naa.") or wwn.startswith("eui.")):
            wwn = wwn_type + "." + wwn

    return wwn

def normalize_wwn(wwn_types, wwn, possible_wwns=None):
    '''
    Take a WWN as given by the user and convert it to a standard text
    representation. If possible_wwns is not None, verify that
    the given WWN is on that list.

    Returns (normalized_wwn, wwn_type), or exception if invalid wwn.
    '''
    wwn_test = {
    'free' : lambda wwn: True,
    'iqn' : lambda wwn: \
        re.match("iqn\.[0-9]{4}-[0-1][0-9]\..*\..*", wwn) \
        and not re.search(' ', wwn) \
        and not re.search('_', wwn),
    'naa' : lambda wwn: re.match("naa\.[125][0-9a-fA-F]{15}$", wwn),
    'eui' : lambda wwn: re.match("eui\.[0-9a-f]{16}$", wwn),
    'unit_serial' : lambda wwn: \
        re.match("[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$", wwn),
    }

    for wwn_type in wwn_types:
        clean_wwn = _cleanse_wwn(wwn_type, wwn)
        found_type = wwn_test[wwn_type](clean_wwn)
        if found_type:
            break
    else:
        raise RTSLibError("WWN not valid as: %s" % ", ".join(wwn_types))

    if possible_wwns is not None and clean_wwn not in possible_wwns:
        raise RTSLibError("WWN not in possible WWNs")

    return (clean_wwn, wwn_type)

def list_loaded_kernel_modules():
    '''
    List all currently loaded kernel modules
    '''
    return [line.split(" ")[0] for line in
            fread("/proc/modules").split('\n') if line]

def modprobe(module):
    '''
    Load the specified kernel module if needed.
    @param module: The name of the kernel module to be loaded.
    @type module: str
    '''
    if module in list_loaded_kernel_modules():
        return

    try:
        import kmod
        kmod.Kmod().modprobe(module)
    except ImportError:
        process = subprocess.Popen(("modprobe", module),
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        (stdoutdata, stderrdata) = process.communicate()
        if process.returncode != 0:
            raise RTSLibError(stderrdata)

def dict_remove(d, items):
    for item in items:
        if item in d:
            del d[item]

def set_attributes(obj, attr_dict):
    for name, value in attr_dict.iteritems():
        try:
            obj.set_attribute(name, value)
        except RTSLibError:
            # Setting some attributes may return an error, before kernel 3.3
            pass

def set_parameters(obj, param_dict): 
    for name, value in param_dict.iteritems():
        try: 
            obj.set_parameter(name, value)
        except RTSLibError:
            # Setting some parameters may return an error, before kernel 3.3
            pass

def _test():
    '''Run the doctests'''
    import doctest
    doctest.testmod()

if __name__ == "__main__":
    _test()