1 '''
2 Provides various utility functions.
3
4 This file is part of RTSLib.
5 Copyright (c) 2011-2013 by Datera, Inc
6
7 Licensed under the Apache License, Version 2.0 (the "License"); you may
8 not use this file except in compliance with the License. You may obtain
9 a copy of the License at
10
11 http://www.apache.org/licenses/LICENSE-2.0
12
13 Unless required by applicable law or agreed to in writing, software
14 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 License for the specific language governing permissions and limitations
17 under the License.
18 '''
19
20 import re
21 import os
22 import stat
23 import uuid
24 import socket
25 import subprocess
26 from contextlib import contextmanager
29 '''
30 Generic rtslib error.
31 '''
32 pass
33
35 '''
36 Broken link in configfs, i.e. missing LUN storage object.
37 '''
38 pass
39
41 '''
42 The underlying configfs object does not exist. Happens when
43 calling methods of an object that is instantiated but have
44 been deleted from congifs, or when trying to lookup an
45 object that does not exist.
46 '''
47 pass
48
50 '''
51 This function writes a string to a file, and takes care of
52 opening it and closing it. If the file does not exist, it
53 will be created.
54
55 >>> from rtslib.utils import *
56 >>> fwrite("/tmp/test", "hello")
57 >>> fread("/tmp/test")
58 'hello'
59
60 @param path: The file to write to.
61 @type path: string
62 @param string: The string to write to the file.
63 @type string: string
64
65 '''
66 with open(path, 'w') as file_fd:
67 file_fd.write(str(string))
68
70 '''
71 This function reads the contents of a file.
72 It takes care of opening and closing it.
73
74 >>> from rtslib.utils import *
75 >>> fwrite("/tmp/test", "hello")
76 >>> fread("/tmp/test")
77 'hello'
78 >>> fread("/tmp/notexistingfile") # doctest: +ELLIPSIS
79 Traceback (most recent call last):
80 ...
81 IOError: [Errno 2] No such file or directory: '/tmp/notexistingfile'
82
83 @param path: The path to the file to read from.
84 @type path: string
85 @return: A string containing the file's contents.
86
87 '''
88 with open(path, 'r') as file_fd:
89 return file_fd.read().strip()
90
92 '''
93 This function will check if the device or file referenced by path is
94 already mounted or used as a storage object backend. It works by trying to
95 open the path with O_EXCL flag, which will fail if someone else already
96 did. Note that the file is closed before the function returns, so this
97 does not guaranteed the device will still be available after the check.
98 @param path: path to the file of device to check
99 @type path: string
100 @return: A boolean, True is we cannot get exclusive descriptor on the path,
101 False if we can.
102 '''
103 path = os.path.realpath(str(path))
104 try:
105 file_fd = os.open(path, os.O_EXCL|os.O_NDELAY)
106 except OSError:
107 return True
108 else:
109 os.close(file_fd)
110 return False
111
113 '''
114 Returns the size in logical blocks of a disk-type block device.
115 '''
116 name = os.path.basename(os.path.realpath(path))
117
118
119 def get_size(path, is_partition=False):
120 sect_size = int(fread("%s/size" % path))
121 if is_partition:
122 path = os.path.split(path)[0]
123 logical_block_size = int(fread("%s/queue/logical_block_size" % path))
124 return sect_size / (logical_block_size / 512)
125
126 try:
127 return get_size("/sys/block/%s" % name)
128 except IOError:
129
130 m = re.search(r'^([a-z0-9_-]+)(\d+)$', name)
131 if m:
132
133
134 disk = m.groups()[0]
135 if disk[-1] == 'p' and disk[-2].isdigit():
136 disk = disk[:-1]
137 return get_size("/sys/block/%s/%s" % (disk, m.group()), True)
138 else:
139 raise
140
141 get_block_size = get_blockdev_size
144 '''
145 This function returns a block device's type.
146 Example: 0 is TYPE_DISK
147 If no match is found, None is returned.
148
149 >>> from rtslib.utils import *
150 >>> get_blockdev_type("/dev/sda")
151 0
152 >>> get_blockdev_type("/dev/sr0")
153 5
154 >>> get_blockdev_type("/dev/scd0")
155 5
156 >>> get_blockdev_type("/dev/nodevicehere") is None
157 True
158
159 @param path: path to the block device
160 @type path: string
161 @return: An int for the block device type, or None if not a block device.
162 '''
163 dev = os.path.realpath(path)
164
165
166 try:
167 mode = os.stat(dev)
168 except OSError:
169 return None
170
171 if not stat.S_ISBLK(mode[stat.ST_MODE]):
172 return None
173
174
175 disk_type = 0
176 with ignored(IOError):
177 disk_type = int(fread("/sys/block/%s/device/type" % os.path.basename(dev)))
178
179 return disk_type
180
181 get_block_type = get_blockdev_type
184 '''
185 This function returns the SCSI ID in H:C:T:L form for the block
186 device being mapped to the udev path specified.
187 If no match is found, None is returned.
188
189 >>> import rtslib.utils as utils
190 >>> utils.convert_scsi_path_to_hctl('/dev/scd0')
191 (2, 0, 0, 0)
192 >>> utils.convert_scsi_path_to_hctl('/dev/sr0')
193 (2, 0, 0, 0)
194 >>> utils.convert_scsi_path_to_hctl('/dev/sda')
195 (3, 0, 0, 0)
196 >>> utils.convert_scsi_path_to_hctl('/dev/sda1')
197 >>> utils.convert_scsi_path_to_hctl('/dev/sdb')
198 (3, 0, 1, 0)
199 >>> utils.convert_scsi_path_to_hctl('/dev/sdc')
200 (3, 0, 2, 0)
201
202 @param path: The udev path to the SCSI block device.
203 @type path: string
204 @return: An (host, controller, target, lun) tuple of integer
205 values representing the SCSI ID of the device, or None if no
206 match is found.
207 '''
208 devname = os.path.basename(os.path.realpath(path))
209 try:
210 hctl = os.listdir("/sys/block/%s/device/scsi_device"
211 % devname)[0].split(':')
212 except:
213 return None
214
215 return [int(data) for data in hctl]
216
218 '''
219 This function returns a udev path pointing to the block device being
220 mapped to the SCSI device that has the provided H:C:T:L.
221
222 >>> import rtslib.utils as utils
223 >>> utils.convert_scsi_hctl_to_path(0,0,0,0)
224 ''
225 >>> utils.convert_scsi_hctl_to_path(2,0,0,0) # doctest: +ELLIPSIS
226 '/dev/s...0'
227 >>> utils.convert_scsi_hctl_to_path(3,0,2,0)
228 '/dev/sdc'
229
230 @param host: The SCSI host id.
231 @type host: int
232 @param controller: The SCSI controller id.
233 @type controller: int
234 @param target: The SCSI target id.
235 @type target: int
236 @param lun: The SCSI Logical Unit Number.
237 @type lun: int
238 @return: A string for the canonical path to the device, or empty string.
239 '''
240 try:
241 host = int(host)
242 controller = int(controller)
243 target = int(target)
244 lun = int(lun)
245 except ValueError:
246 raise RTSLibError(
247 "The host, controller, target and lun parameter must be integers.")
248
249 for devname in os.listdir("/sys/block"):
250 path = "/dev/%s" % devname
251 hctl = [host, controller, target, lun]
252 if convert_scsi_path_to_hctl(path) == hctl:
253 return os.path.realpath(path)
254 return ''
255
257 '''
258 Generates a random WWN of the specified type:
259 - unit_serial: T10 WWN Unit Serial.
260 - iqn: iSCSI IQN
261 - naa: SAS NAA address
262 @param wwn_type: The WWN address type.
263 @type wwn_type: str
264 @returns: A string containing the WWN.
265 '''
266 wwn_type = wwn_type.lower()
267 if wwn_type == 'free':
268 return str(uuid.uuid4())
269 if wwn_type == 'unit_serial':
270 return str(uuid.uuid4())
271 elif wwn_type == 'iqn':
272 localname = socket.gethostname().split(".")[0]
273 localarch = os.uname()[4].replace("_", "")
274 prefix = "iqn.2003-01.org.linux-iscsi.%s.%s" % (localname, localarch)
275 prefix = prefix.strip().lower()
276 serial = "sn.%s" % str(uuid.uuid4())[24:]
277 return "%s:%s" % (prefix, serial)
278 elif wwn_type == 'naa':
279
280
281
282
283 return "naa.5001405" + uuid.uuid4().get_hex()[-9:]
284 elif wwn_type == 'eui':
285 return "eui.001405" + uuid.uuid4().get_hex()[-10:]
286 else:
287 raise ValueError("Unknown WWN type: %s." % wwn_type)
288
290 '''
291 helper function to add colons every 2 chars
292 '''
293 return ":".join(str[i:i+2] for i in range(0, len(str), 2))
294
296 '''
297 Some wwns may have alternate text representations. Adjust to our
298 preferred representation.
299 '''
300 wwn = str(wwn.strip()).lower()
301
302 if wwn_type in ('naa', 'eui', 'ib'):
303 if wwn.startswith("0x"):
304 wwn = wwn[2:]
305 wwn = wwn.replace("-", "")
306 wwn = wwn.replace(":", "")
307
308 if not (wwn.startswith("naa.") or wwn.startswith("eui.") or \
309 wwn.startswith("ib.")):
310 wwn = wwn_type + "." + wwn
311
312 return wwn
313
315 '''
316 Take a WWN as given by the user and convert it to a standard text
317 representation. If possible_wwns is not None, verify that
318 the given WWN is on that list.
319
320 Returns (normalized_wwn, wwn_type), or exception if invalid wwn.
321 '''
322 wwn_test = {
323 'free': lambda wwn: True,
324 'iqn': lambda wwn: \
325 re.match("iqn\.[0-9]{4}-[0-1][0-9]\..*\..*", wwn) \
326 and not re.search(' ', wwn) \
327 and not re.search('_', wwn),
328 'naa': lambda wwn: re.match("naa\.[125][0-9a-fA-F]{15}$", wwn),
329 'eui': lambda wwn: re.match("eui\.[0-9a-f]{16}$", wwn),
330 'ib': lambda wwn: re.match("ib\.[0-9a-f]{32}$", wwn),
331 'unit_serial': lambda wwn: \
332 re.match("[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$", wwn),
333 }
334
335 for wwn_type in wwn_types:
336 clean_wwn = _cleanse_wwn(wwn_type, wwn)
337 found_type = wwn_test[wwn_type](clean_wwn)
338 if found_type:
339 break
340 else:
341 raise RTSLibError("WWN not valid as: %s" % ", ".join(wwn_types))
342
343 return (clean_wwn, wwn_type)
344
346 '''
347 List all currently loaded kernel modules
348 '''
349 return [line.split(" ")[0] for line in
350 fread("/proc/modules").split('\n') if line]
351
353 '''
354 Load the specified kernel module if needed.
355 @param module: The name of the kernel module to be loaded.
356 @type module: str
357 '''
358 if module in list_loaded_kernel_modules():
359 return
360
361 try:
362 import kmod
363 kmod.Kmod().modprobe(module)
364 except ImportError:
365 process = subprocess.Popen(("modprobe", module),
366 stdout=subprocess.PIPE,
367 stderr=subprocess.PIPE)
368 (stdoutdata, stderrdata) = process.communicate()
369 if process.returncode != 0:
370 raise RTSLibError(stderrdata)
371
373 if not os.path.ismount("/sys/kernel/config"):
374 cmdline = "mount -t configfs none /sys/kernel/config"
375 process = subprocess.Popen(cmdline.split(),
376 stdout=subprocess.PIPE,
377 stderr=subprocess.PIPE)
378 (stdoutdata, stderrdata) = process.communicate()
379 if process.returncode != 0:
380 raise RTSLibError("Cannot mount configfs.")
381
383 for item in items:
384 if item in d:
385 del d[item]
386
387 @contextmanager
388 -def ignored(*exceptions):
389 try:
390 yield
391 except exceptions:
392 pass
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407 -def _get_auth_attr(self, attribute, ignore=False):
408 self._check_self()
409 path = "%s/%s" % (self.path, attribute)
410 try:
411 value = fread(path)
412 except:
413 if not ignore:
414 raise
415 return None
416 if value == "NULL":
417 return ''
418 else:
419 return value
420
423 self._check_self()
424 path = "%s/%s" % (self.path, attribute)
425 value = value.strip()
426 if value == "NULL":
427 raise ValueError("'NULL' is not a permitted value")
428 if value == '':
429 value = "NULL"
430 try:
431 fwrite(path, "%s" % value)
432 except:
433 if not ignore:
434 raise
435
442
449
451 '''Run the doctests'''
452 import doctest
453 doctest.testmod()
454
455 if __name__ == "__main__":
456 _test()
457