Coverage for drivers/util.py : 44%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (C) Citrix Systems Inc.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published
5# by the Free Software Foundation; version 2.1 only.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU Lesser General Public License for more details.
11#
12# You should have received a copy of the GNU Lesser General Public License
13# along with this program; if not, write to the Free Software Foundation, Inc.,
14# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15#
16# Miscellaneous utility functions
17#
19import os
20import re
21import sys
22import subprocess
23import shutil
24import tempfile
25import signal
26import time
27import datetime
28import errno
29import socket
30import xml.dom.minidom
31import scsiutil
32import stat
33import xs_errors
34import XenAPI # pylint: disable=import-error
35import xmlrpc.client
36import base64
37import syslog
38import resource
39import traceback
40import glob
41import copy
42import tempfile
44from functools import reduce
46NO_LOGGING_STAMPFILE = '/etc/xensource/no_sm_log'
48IORETRY_MAX = 20 # retries
49IORETRY_PERIOD = 1.0 # seconds
51LOGGING = not (os.path.exists(NO_LOGGING_STAMPFILE))
52_SM_SYSLOG_FACILITY = syslog.LOG_LOCAL2
53LOG_EMERG = syslog.LOG_EMERG
54LOG_ALERT = syslog.LOG_ALERT
55LOG_CRIT = syslog.LOG_CRIT
56LOG_ERR = syslog.LOG_ERR
57LOG_WARNING = syslog.LOG_WARNING
58LOG_NOTICE = syslog.LOG_NOTICE
59LOG_INFO = syslog.LOG_INFO
60LOG_DEBUG = syslog.LOG_DEBUG
62ISCSI_REFDIR = '/var/run/sr-ref'
64CMD_DD = "/bin/dd"
66FIST_PAUSE_PERIOD = 30 # seconds
69class SMException(Exception):
70 """Base class for all SM exceptions for easier catching & wrapping in
71 XenError"""
74class CommandException(SMException):
75 def error_message(self, code):
76 if code > 0:
77 return os.strerror(code)
78 elif code < 0:
79 return "Signalled %s" % (abs(code))
80 return "Success"
82 def __init__(self, code, cmd="", reason='exec failed'):
83 self.code = code
84 self.cmd = cmd
85 self.reason = reason
86 Exception.__init__(self, self.error_message(code))
89class SRBusyException(SMException):
90 """The SR could not be locked"""
91 pass
94def logException(tag):
95 info = sys.exc_info()
96 if info[0] == SystemExit: 96 ↛ 98line 96 didn't jump to line 98, because the condition on line 96 was never true
97 # this should not be happening when catching "Exception", but it is
98 sys.exit(0)
99 tb = reduce(lambda a, b: "%s%s" % (a, b), traceback.format_tb(info[2]))
100 str = "***** %s: EXCEPTION %s, %s\n%s" % (tag, info[0], info[1], tb)
101 SMlog(str)
104def roundup(divisor, value):
105 """Retruns the rounded up value so it is divisible by divisor."""
107 if value == 0: 107 ↛ 108line 107 didn't jump to line 108, because the condition on line 107 was never true
108 value = 1
109 if value % divisor != 0:
110 return ((int(value) // divisor) + 1) * divisor
111 return value
114def to_plain_string(obj):
115 if obj is None:
116 return None
117 if type(obj) == str:
118 return obj
119 return str(obj)
122def shellquote(arg):
123 return '"%s"' % arg.replace('"', '\\"')
126def make_WWN(name):
127 hex_prefix = name.find("0x")
128 if (hex_prefix >= 0): 128 ↛ 131line 128 didn't jump to line 131, because the condition on line 128 was never false
129 name = name[name.find("0x") + 2:len(name)]
130 # inject dashes for each nibble
131 if (len(name) == 16): # sanity check 131 ↛ 135line 131 didn't jump to line 135, because the condition on line 131 was never false
132 name = name[0:2] + "-" + name[2:4] + "-" + name[4:6] + "-" + \
133 name[6:8] + "-" + name[8:10] + "-" + name[10:12] + "-" + \
134 name[12:14] + "-" + name[14:16]
135 return name
138def _logToSyslog(ident, facility, priority, message):
139 syslog.openlog(ident, 0, facility)
140 syslog.syslog(priority, "[%d] %s" % (os.getpid(), message))
141 syslog.closelog()
144def SMlog(message, ident="SM", priority=LOG_INFO):
145 if LOGGING: 145 ↛ exitline 145 didn't return from function 'SMlog', because the condition on line 145 was never false
146 for message_line in str(message).split('\n'):
147 _logToSyslog(ident, _SM_SYSLOG_FACILITY, priority, message_line)
150def _getDateString():
151 d = datetime.datetime.now()
152 t = d.timetuple()
153 return "%s-%s-%s:%s:%s:%s" % \
154 (t[0], t[1], t[2], t[3], t[4], t[5])
157def doexec(args, inputtext=None, new_env=None, text=True):
158 """Execute a subprocess, then return its return code, stdout and stderr"""
159 env = None
160 if new_env:
161 env = dict(os.environ)
162 env.update(new_env)
163 proc = subprocess.Popen(args, stdin=subprocess.PIPE,
164 stdout=subprocess.PIPE,
165 stderr=subprocess.PIPE,
166 close_fds=True, env=env,
167 universal_newlines=text)
169 if not text and inputtext is not None: 169 ↛ 170line 169 didn't jump to line 170, because the condition on line 169 was never true
170 inputtext = inputtext.encode()
172 (stdout, stderr) = proc.communicate(inputtext)
174 rc = proc.returncode
175 return rc, stdout, stderr
178def is_string(value):
179 return isinstance(value, str)
182# These are partially tested functions that replicate the behaviour of
183# the original pread,pread2 and pread3 functions. Potentially these can
184# replace the original ones at some later date.
185#
186# cmdlist is a list of either single strings or pairs of strings. For
187# each pair, the first component is passed to exec while the second is
188# written to the logs.
189def pread(cmdlist, close_stdin=False, scramble=None, expect_rc=0,
190 quiet=False, new_env=None, text=True):
191 cmdlist_for_exec = []
192 cmdlist_for_log = []
193 for item in cmdlist:
194 if is_string(item): 194 ↛ 204line 194 didn't jump to line 204, because the condition on line 194 was never false
195 cmdlist_for_exec.append(item)
196 if scramble: 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true
197 if item.find(scramble) != -1:
198 cmdlist_for_log.append("<filtered out>")
199 else:
200 cmdlist_for_log.append(item)
201 else:
202 cmdlist_for_log.append(item)
203 else:
204 cmdlist_for_exec.append(item[0])
205 cmdlist_for_log.append(item[1])
207 if not quiet: 207 ↛ 209line 207 didn't jump to line 209, because the condition on line 207 was never false
208 SMlog(cmdlist_for_log)
209 (rc, stdout, stderr) = doexec(cmdlist_for_exec, new_env=new_env, text=text)
210 if rc != expect_rc:
211 SMlog("FAILED in util.pread: (rc %d) stdout: '%s', stderr: '%s'" % \
212 (rc, stdout, stderr))
213 if quiet: 213 ↛ 214line 213 didn't jump to line 214, because the condition on line 213 was never true
214 SMlog("Command was: %s" % cmdlist_for_log)
215 if '' == stderr: 215 ↛ 216line 215 didn't jump to line 216, because the condition on line 215 was never true
216 stderr = stdout
217 raise CommandException(rc, str(cmdlist), stderr.strip())
218 if not quiet: 218 ↛ 220line 218 didn't jump to line 220, because the condition on line 218 was never false
219 SMlog(" pread SUCCESS")
220 return stdout
223# POSIX guaranteed atomic within the same file system.
224# Supply directory to ensure tempfile is created
225# in the same directory.
226def atomicFileWrite(targetFile, directory, text):
228 file = None
229 try:
230 # Create file only current pid can write/read to
231 # our responsibility to clean it up.
232 _, tempPath = tempfile.mkstemp(dir=directory)
233 file = open(tempPath, 'w')
234 file.write(text)
236 # Ensure flushed to disk.
237 file.flush()
238 os.fsync(file.fileno())
239 file.close()
241 os.rename(tempPath, targetFile)
242 except OSError:
243 SMlog("FAILED to atomic write to %s" % (targetFile))
245 finally:
246 if (file is not None) and (not file.closed):
247 file.close()
249 if os.path.isfile(tempPath):
250 os.remove(tempPath)
253#Read STDOUT from cmdlist and discard STDERR output
254def pread2(cmdlist, quiet=False, text=True):
255 return pread(cmdlist, quiet=quiet, text=text)
258#Read STDOUT from cmdlist, feeding 'text' to STDIN
259def pread3(cmdlist, text):
260 SMlog(cmdlist)
261 (rc, stdout, stderr) = doexec(cmdlist, text)
262 if rc:
263 SMlog("FAILED in util.pread3: (errno %d) stdout: '%s', stderr: '%s'" % \
264 (rc, stdout, stderr))
265 if '' == stderr:
266 stderr = stdout
267 raise CommandException(rc, str(cmdlist), stderr.strip())
268 SMlog(" pread3 SUCCESS")
269 return stdout
272def listdir(path, quiet=False):
273 cmd = ["ls", path, "-1", "--color=never"]
274 try:
275 text = pread2(cmd, quiet=quiet)[:-1]
276 if len(text) == 0:
277 return []
278 return text.split('\n')
279 except CommandException as inst:
280 if inst.code == errno.ENOENT:
281 raise CommandException(errno.EIO, inst.cmd, inst.reason)
282 else:
283 raise CommandException(inst.code, inst.cmd, inst.reason)
286def gen_uuid():
287 cmd = ["uuidgen", "-r"]
288 return pread(cmd)[:-1]
291def match_uuid(s):
292 regex = re.compile("^[0-9a-f]{8}-(([0-9a-f]{4})-){3}[0-9a-f]{12}")
293 return regex.search(s, 0)
296def findall_uuid(s):
297 regex = re.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
298 return regex.findall(s, 0)
301def exactmatch_uuid(s):
302 regex = re.compile("^[0-9a-f]{8}-(([0-9a-f]{4})-){3}[0-9a-f]{12}$")
303 return regex.search(s, 0)
306def start_log_entry(srpath, path, args):
307 logstring = str(datetime.datetime.now())
308 logstring += " log: "
309 logstring += srpath
310 logstring += " " + path
311 for element in args:
312 logstring += " " + element
313 try:
314 file = open(srpath + "/filelog.txt", "a")
315 file.write(logstring)
316 file.write("\n")
317 file.close()
318 except:
319 pass
321 # failed to write log ...
323def end_log_entry(srpath, path, args):
324 # for teminating, use "error" or "done"
325 logstring = str(datetime.datetime.now())
326 logstring += " end: "
327 logstring += srpath
328 logstring += " " + path
329 for element in args:
330 logstring += " " + element
331 try:
332 file = open(srpath + "/filelog.txt", "a")
333 file.write(logstring)
334 file.write("\n")
335 file.close()
336 except:
337 pass
339 # failed to write log ...
340 # for now print
341 # print "%s" % logstring
343def ioretry(f, errlist=[errno.EIO], maxretry=IORETRY_MAX, period=IORETRY_PERIOD, **ignored):
344 retries = 0
345 while True:
346 try:
347 return f()
348 except OSError as ose:
349 err = int(ose.errno)
350 if not err in errlist:
351 raise CommandException(err, str(f), "OSError")
352 except CommandException as ce:
353 if not int(ce.code) in errlist:
354 raise
356 retries += 1
357 if retries >= maxretry:
358 break
360 time.sleep(period)
362 raise CommandException(errno.ETIMEDOUT, str(f), "Timeout")
365def ioretry_stat(path, maxretry=IORETRY_MAX):
366 # this ioretry is similar to the previous method, but
367 # stat does not raise an error -- so check its return
368 retries = 0
369 while retries < maxretry:
370 stat = os.statvfs(path)
371 if stat.f_blocks != -1:
372 return stat
373 time.sleep(1)
374 retries += 1
375 raise CommandException(errno.EIO, "os.statvfs")
378def sr_get_capability(sr_uuid, session=None):
379 result = []
380 local_session = None
381 if session is None: 381 ↛ 385line 381 didn't jump to line 385, because the condition on line 381 was never false
382 local_session = get_localAPI_session()
383 session = local_session
385 try:
386 sr_ref = session.xenapi.SR.get_by_uuid(sr_uuid)
387 sm_type = session.xenapi.SR.get_record(sr_ref)['type']
388 sm_rec = session.xenapi.SM.get_all_records_where(
389 "field \"type\" = \"%s\"" % sm_type)
391 # SM expects at least one entry of any SR type
392 if len(sm_rec) > 0:
393 result = list(sm_rec.values())[0]['capabilities']
395 return result
396 finally:
397 if local_session: 397 ↛ exitline 397 didn't return from function 'sr_get_capability', because the return on line 395 wasn't executed
398 local_session.xenapi.session.logout()
400def sr_get_driver_info(driver_info):
401 results = {}
402 # first add in the vanilla stuff
403 for key in ['name', 'description', 'vendor', 'copyright', \
404 'driver_version', 'required_api_version']:
405 results[key] = driver_info[key]
406 # add the capabilities (xmlrpc array)
407 # enforcing activate/deactivate for blktap2
408 caps = driver_info['capabilities']
409 if "ATOMIC_PAUSE" in caps: 409 ↛ 410line 409 didn't jump to line 410, because the condition on line 409 was never true
410 for cap in ("VDI_ACTIVATE", "VDI_DEACTIVATE"):
411 if not cap in caps:
412 caps.append(cap)
413 elif "VDI_ACTIVATE" in caps or "VDI_DEACTIVATE" in caps: 413 ↛ 414line 413 didn't jump to line 414, because the condition on line 413 was never true
414 SMlog("Warning: vdi_[de]activate present for %s" % driver_info["name"])
416 results['capabilities'] = caps
417 # add in the configuration options
418 options = []
419 for option in driver_info['configuration']:
420 options.append({'key': option[0], 'description': option[1]})
421 results['configuration'] = options
422 return xmlrpc.client.dumps((results, ), "", True)
425def return_nil():
426 return xmlrpc.client.dumps((None, ), "", True, allow_none=True)
429def SRtoXML(SRlist):
430 dom = xml.dom.minidom.Document()
431 driver = dom.createElement("SRlist")
432 dom.appendChild(driver)
434 for key in SRlist.keys():
435 dict = SRlist[key]
436 entry = dom.createElement("SR")
437 driver.appendChild(entry)
439 e = dom.createElement("UUID")
440 entry.appendChild(e)
441 textnode = dom.createTextNode(key)
442 e.appendChild(textnode)
444 if 'size' in dict:
445 e = dom.createElement("Size")
446 entry.appendChild(e)
447 textnode = dom.createTextNode(str(dict['size']))
448 e.appendChild(textnode)
450 if 'storagepool' in dict:
451 e = dom.createElement("StoragePool")
452 entry.appendChild(e)
453 textnode = dom.createTextNode(str(dict['storagepool']))
454 e.appendChild(textnode)
456 if 'aggregate' in dict:
457 e = dom.createElement("Aggregate")
458 entry.appendChild(e)
459 textnode = dom.createTextNode(str(dict['aggregate']))
460 e.appendChild(textnode)
462 return dom.toprettyxml()
465def pathexists(path):
466 try:
467 os.lstat(path)
468 return True
469 except OSError as inst:
470 if inst.errno == errno.EIO: 470 ↛ 471line 470 didn't jump to line 471, because the condition on line 470 was never true
471 time.sleep(1)
472 try:
473 listdir(os.path.realpath(os.path.dirname(path)))
474 os.lstat(path)
475 return True
476 except:
477 pass
478 raise CommandException(errno.EIO, "os.lstat(%s)" % path, "failed")
479 return False
482def force_unlink(path):
483 try:
484 os.unlink(path)
485 except OSError as e:
486 if e.errno != errno.ENOENT: 486 ↛ 487line 486 didn't jump to line 487, because the condition on line 486 was never true
487 raise
490def create_secret(session, secret):
491 ref = session.xenapi.secret.create({'value': secret})
492 return session.xenapi.secret.get_uuid(ref)
495def get_secret(session, uuid):
496 try:
497 ref = session.xenapi.secret.get_by_uuid(uuid)
498 return session.xenapi.secret.get_value(ref)
499 except:
500 raise xs_errors.XenError('InvalidSecret', opterr='Unable to look up secret [%s]' % uuid)
503def get_real_path(path):
504 "Follow symlinks to the actual file"
505 absPath = path
506 directory = ''
507 while os.path.islink(absPath):
508 directory = os.path.dirname(absPath)
509 absPath = os.readlink(absPath)
510 absPath = os.path.join(directory, absPath)
511 return absPath
514def wait_for_path(path, timeout):
515 for i in range(0, timeout): 515 ↛ 519line 515 didn't jump to line 519, because the loop on line 515 didn't complete
516 if len(glob.glob(path)): 516 ↛ 518line 516 didn't jump to line 518, because the condition on line 516 was never false
517 return True
518 time.sleep(1)
519 return False
522def wait_for_nopath(path, timeout):
523 for i in range(0, timeout):
524 if not os.path.exists(path):
525 return True
526 time.sleep(1)
527 return False
530def wait_for_path_multi(path, timeout):
531 for i in range(0, timeout):
532 paths = glob.glob(path)
533 SMlog("_wait_for_paths_multi: paths = %s" % paths)
534 if len(paths):
535 SMlog("_wait_for_paths_multi: return first path: %s" % paths[0])
536 return paths[0]
537 time.sleep(1)
538 return ""
541def isdir(path):
542 try:
543 st = os.stat(path)
544 return stat.S_ISDIR(st.st_mode)
545 except OSError as inst:
546 if inst.errno == errno.EIO: 546 ↛ 547line 546 didn't jump to line 547, because the condition on line 546 was never true
547 raise CommandException(errno.EIO, "os.stat(%s)" % path, "failed")
548 return False
551def get_single_entry(path):
552 f = open(path, 'r')
553 line = f.readline()
554 f.close()
555 return line.rstrip()
558def get_fs_size(path):
559 st = ioretry_stat(path)
560 return st.f_blocks * st.f_frsize
563def get_fs_utilisation(path):
564 st = ioretry_stat(path)
565 return (st.f_blocks - st.f_bfree) * \
566 st.f_frsize
569def ismount(path):
570 """Test whether a path is a mount point"""
571 try:
572 s1 = os.stat(path)
573 s2 = os.stat(os.path.join(path, '..'))
574 except OSError as inst:
575 raise CommandException(inst.errno, "os.stat")
576 dev1 = s1.st_dev
577 dev2 = s2.st_dev
578 if dev1 != dev2:
579 return True # path/.. on a different device as path
580 ino1 = s1.st_ino
581 ino2 = s2.st_ino
582 if ino1 == ino2:
583 return True # path/.. is the same i-node as path
584 return False
587def makedirs(name, mode=0o777):
588 head, tail = os.path.split(name)
589 if not tail: 589 ↛ 590line 589 didn't jump to line 590, because the condition on line 589 was never true
590 head, tail = os.path.split(head)
591 if head and tail and not pathexists(head):
592 makedirs(head, mode)
593 if tail == os.curdir: 593 ↛ 594line 593 didn't jump to line 594, because the condition on line 593 was never true
594 return
595 try:
596 os.mkdir(name, mode)
597 except OSError as exc:
598 if exc.errno == errno.EEXIST and os.path.isdir(name): 598 ↛ 599line 598 didn't jump to line 599, because the condition on line 598 was never true
599 if mode:
600 os.chmod(name, mode)
601 pass
602 else:
603 raise
606def zeroOut(path, fromByte, bytes):
607 """write 'bytes' zeros to 'path' starting from fromByte (inclusive)"""
608 blockSize = 4096
610 fromBlock = fromByte // blockSize
611 if fromByte % blockSize:
612 fromBlock += 1
613 bytesBefore = fromBlock * blockSize - fromByte
614 if bytesBefore > bytes:
615 bytesBefore = bytes
616 bytes -= bytesBefore
617 cmd = [CMD_DD, "if=/dev/zero", "of=%s" % path, "bs=1",
618 "seek=%s" % fromByte, "count=%s" % bytesBefore]
619 try:
620 pread2(cmd)
621 except CommandException:
622 return False
624 blocks = bytes // blockSize
625 bytes -= blocks * blockSize
626 fromByte = (fromBlock + blocks) * blockSize
627 if blocks:
628 cmd = [CMD_DD, "if=/dev/zero", "of=%s" % path, "bs=%s" % blockSize,
629 "seek=%s" % fromBlock, "count=%s" % blocks]
630 try:
631 pread2(cmd)
632 except CommandException:
633 return False
635 if bytes:
636 cmd = [CMD_DD, "if=/dev/zero", "of=%s" % path, "bs=1",
637 "seek=%s" % fromByte, "count=%s" % bytes]
638 try:
639 pread2(cmd)
640 except CommandException:
641 return False
643 return True
646def wipefs(blockdev):
647 "Wipe filesystem signatures from `blockdev`"
648 pread2(["/usr/sbin/wipefs", "-a", blockdev])
651def match_rootdev(s):
652 regex = re.compile("^PRIMARY_DISK")
653 return regex.search(s, 0)
656def getrootdev():
657 filename = '/etc/xensource-inventory'
658 try:
659 f = open(filename, 'r')
660 except:
661 raise xs_errors.XenError('EIO', \
662 opterr="Unable to open inventory file [%s]" % filename)
663 rootdev = ''
664 for line in filter(match_rootdev, f.readlines()):
665 rootdev = line.split("'")[1]
666 if not rootdev: 666 ↛ 667line 666 didn't jump to line 667, because the condition on line 666 was never true
667 raise xs_errors.XenError('NoRootDev')
668 return rootdev
671def getrootdevID():
672 rootdev = getrootdev()
673 try:
674 rootdevID = scsiutil.getSCSIid(rootdev)
675 except:
676 SMlog("util.getrootdevID: Unable to verify serial or SCSIid of device: %s" \
677 % rootdev)
678 return ''
680 if not len(rootdevID):
681 SMlog("util.getrootdevID: Unable to identify scsi device [%s] via scsiID" \
682 % rootdev)
684 return rootdevID
687def get_localAPI_session():
688 # First acquire a valid session
689 session = XenAPI.xapi_local()
690 try:
691 session.xenapi.login_with_password('root', '', '', 'SM')
692 except:
693 raise xs_errors.XenError('APISession')
694 return session
697def get_this_host():
698 uuid = None
699 f = open("/etc/xensource-inventory", 'r')
700 for line in f.readlines():
701 if line.startswith("INSTALLATION_UUID"):
702 uuid = line.split("'")[1]
703 f.close()
704 return uuid
707def get_master_ref(session):
708 pools = session.xenapi.pool.get_all()
709 return session.xenapi.pool.get_master(pools[0])
712def is_master(session):
713 return get_this_host_ref(session) == get_master_ref(session)
716def get_localhost_ref(session):
717 filename = '/etc/xensource-inventory'
718 try:
719 f = open(filename, 'r')
720 except:
721 raise xs_errors.XenError('EIO', \
722 opterr="Unable to open inventory file [%s]" % filename)
723 domid = ''
724 for line in filter(match_domain_id, f.readlines()):
725 domid = line.split("'")[1]
726 if not domid:
727 raise xs_errors.XenError('APILocalhost')
729 vms = session.xenapi.VM.get_all_records_where('field "uuid" = "%s"' % domid)
730 for vm in vms:
731 record = vms[vm]
732 if record["uuid"] == domid:
733 hostid = record["resident_on"]
734 return hostid
735 raise xs_errors.XenError('APILocalhost')
738def match_domain_id(s):
739 regex = re.compile("^CONTROL_DOMAIN_UUID")
740 return regex.search(s, 0)
743def get_hosts_attached_on(session, vdi_uuids):
744 host_refs = {}
745 for vdi_uuid in vdi_uuids:
746 try:
747 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
748 except XenAPI.Failure:
749 SMlog("VDI %s not in db, ignoring" % vdi_uuid)
750 continue
751 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
752 for key in [x for x in sm_config.keys() if x.startswith('host_')]:
753 host_refs[key[len('host_'):]] = True
754 return host_refs.keys()
756def get_this_host_address(session):
757 host_uuid = get_this_host()
758 host_ref = session.xenapi.host.get_by_uuid(host_uuid)
759 return session.xenapi.host.get_record(host_ref)['address']
761def get_host_addresses(session):
762 addresses = []
763 hosts = session.xenapi.host.get_all_records()
764 for record in hosts.values():
765 addresses.append(record['address'])
766 return addresses
768def get_this_host_ref(session):
769 host_uuid = get_this_host()
770 host_ref = session.xenapi.host.get_by_uuid(host_uuid)
771 return host_ref
774def get_slaves_attached_on(session, vdi_uuids):
775 "assume this host is the SR master"
776 host_refs = get_hosts_attached_on(session, vdi_uuids)
777 master_ref = get_this_host_ref(session)
778 return [x for x in host_refs if x != master_ref]
781def get_online_hosts(session):
782 online_hosts = []
783 hosts = session.xenapi.host.get_all_records()
784 for host_ref, host_rec in hosts.items():
785 metricsRef = host_rec["metrics"]
786 metrics = session.xenapi.host_metrics.get_record(metricsRef)
787 if metrics["live"]:
788 online_hosts.append(host_ref)
789 return online_hosts
792def get_all_slaves(session):
793 "assume this host is the SR master"
794 host_refs = get_online_hosts(session)
795 master_ref = get_this_host_ref(session)
796 return [x for x in host_refs if x != master_ref]
799def is_attached_rw(sm_config):
800 for key, val in sm_config.items():
801 if key.startswith("host_") and val == "RW":
802 return True
803 return False
806def attached_as(sm_config):
807 for key, val in sm_config.items():
808 if key.startswith("host_") and (val == "RW" or val == "RO"): 808 ↛ 809line 808 didn't jump to line 809, because the condition on line 808 was never true
809 return val
812def find_my_pbd_record(session, host_ref, sr_ref):
813 try:
814 pbds = session.xenapi.PBD.get_all_records()
815 for pbd_ref in pbds.keys():
816 if pbds[pbd_ref]['host'] == host_ref and pbds[pbd_ref]['SR'] == sr_ref:
817 return [pbd_ref, pbds[pbd_ref]]
818 return None
819 except Exception as e:
820 SMlog("Caught exception while looking up PBD for host %s SR %s: %s" % (str(host_ref), str(sr_ref), str(e)))
821 return None
824def find_my_pbd(session, host_ref, sr_ref):
825 ret = find_my_pbd_record(session, host_ref, sr_ref)
826 if ret is not None:
827 return ret[0]
828 else:
829 return None
832def test_hostPBD_devs(session, sr_uuid, devs):
833 host = get_localhost_ref(session)
834 sr = session.xenapi.SR.get_by_uuid(sr_uuid)
835 try:
836 pbds = session.xenapi.PBD.get_all_records()
837 except:
838 raise xs_errors.XenError('APIPBDQuery')
839 for dev in devs.split(','):
840 for pbd in pbds:
841 record = pbds[pbd]
842 # it's ok if it's *our* PBD
843 if record["SR"] == sr:
844 break
845 if record["host"] == host:
846 devconfig = record["device_config"]
847 if 'device' in devconfig:
848 for device in devconfig['device'].split(','):
849 if os.path.realpath(device) == os.path.realpath(dev):
850 return True
851 return False
854def test_hostPBD_lun(session, targetIQN, LUNid):
855 host = get_localhost_ref(session)
856 try:
857 pbds = session.xenapi.PBD.get_all_records()
858 except:
859 raise xs_errors.XenError('APIPBDQuery')
860 for pbd in pbds:
861 record = pbds[pbd]
862 if record["host"] == host:
863 devconfig = record["device_config"]
864 if 'targetIQN' in devconfig and 'LUNid' in devconfig:
865 if devconfig['targetIQN'] == targetIQN and \
866 devconfig['LUNid'] == LUNid:
867 return True
868 return False
871def test_SCSIid(session, sr_uuid, SCSIid):
872 if sr_uuid is not None:
873 sr = session.xenapi.SR.get_by_uuid(sr_uuid)
874 try:
875 pbds = session.xenapi.PBD.get_all_records()
876 except:
877 raise xs_errors.XenError('APIPBDQuery')
878 for pbd in pbds:
879 record = pbds[pbd]
880 # it's ok if it's *our* PBD
881 # During FC SR creation, devscan.py passes sr_uuid as None
882 if sr_uuid is not None:
883 if record["SR"] == sr:
884 break
885 devconfig = record["device_config"]
886 sm_config = session.xenapi.SR.get_sm_config(record["SR"])
887 if 'SCSIid' in devconfig and devconfig['SCSIid'] == SCSIid:
888 return True
889 elif 'SCSIid' in sm_config and sm_config['SCSIid'] == SCSIid:
890 return True
891 elif 'scsi-' + SCSIid in sm_config:
892 return True
893 return False
896class TimeoutException(SMException):
897 pass
900def timeout_call(timeoutseconds, function, *arguments):
901 def handler(signum, frame):
902 raise TimeoutException()
903 signal.signal(signal.SIGALRM, handler)
904 signal.alarm(timeoutseconds)
905 try:
906 return function(*arguments)
907 finally:
908 signal.alarm(0)
911def _incr_iscsiSR_refcount(targetIQN, uuid):
912 if not os.path.exists(ISCSI_REFDIR):
913 os.mkdir(ISCSI_REFDIR)
914 filename = os.path.join(ISCSI_REFDIR, targetIQN)
915 try:
916 f = open(filename, 'a+')
917 except:
918 raise xs_errors.XenError('LVMRefCount', \
919 opterr='file %s' % filename)
921 f.seek(0)
922 found = False
923 refcount = 0
924 for line in filter(match_uuid, f.readlines()):
925 refcount += 1
926 if line.find(uuid) != -1:
927 found = True
928 if not found:
929 f.write("%s\n" % uuid)
930 refcount += 1
931 f.close()
932 return refcount
935def _decr_iscsiSR_refcount(targetIQN, uuid):
936 filename = os.path.join(ISCSI_REFDIR, targetIQN)
937 if not os.path.exists(filename):
938 return 0
939 try:
940 f = open(filename, 'a+')
941 except:
942 raise xs_errors.XenError('LVMRefCount', \
943 opterr='file %s' % filename)
945 f.seek(0)
946 output = []
947 refcount = 0
948 for line in filter(match_uuid, f.readlines()):
949 if line.find(uuid) == -1:
950 output.append(line.rstrip())
951 refcount += 1
952 if not refcount:
953 os.unlink(filename)
954 return refcount
956 # Re-open file and truncate
957 f.close()
958 f = open(filename, 'w')
959 for i in range(0, refcount):
960 f.write("%s\n" % output[i])
961 f.close()
962 return refcount
965# The agent enforces 1 PBD per SR per host, so we
966# check for active SR entries not attached to this host
967def test_activePoolPBDs(session, host, uuid):
968 try:
969 pbds = session.xenapi.PBD.get_all_records()
970 except:
971 raise xs_errors.XenError('APIPBDQuery')
972 for pbd in pbds:
973 record = pbds[pbd]
974 if record["host"] != host and record["SR"] == uuid \
975 and record["currently_attached"]:
976 return True
977 return False
980def remove_mpathcount_field(session, host_ref, sr_ref, SCSIid):
981 try:
982 pbdref = find_my_pbd(session, host_ref, sr_ref)
983 if pbdref is not None:
984 key = "mpath-" + SCSIid
985 session.xenapi.PBD.remove_from_other_config(pbdref, key)
986 except:
987 pass
991def _testHost(hostname, port, errstring):
992 SMlog("_testHost: Testing host/port: %s,%d" % (hostname, port))
993 try:
994 sockinfo = socket.getaddrinfo(hostname, int(port))[0]
995 except:
996 logException('Exception occured getting IP for %s' % hostname)
997 raise xs_errors.XenError('DNSError')
999 timeout = 5
1001 sock = socket.socket(sockinfo[0], socket.SOCK_STREAM)
1002 # Only allow the connect to block for up to timeout seconds
1003 sock.settimeout(timeout)
1004 try:
1005 sock.connect(sockinfo[4])
1006 # Fix for MS storage server bug
1007 sock.send(b'\n')
1008 sock.close()
1009 except socket.error as reason:
1010 SMlog("_testHost: Connect failed after %d seconds (%s) - %s" \
1011 % (timeout, hostname, reason))
1012 raise xs_errors.XenError(errstring)
1015def match_scsiID(s, id):
1016 regex = re.compile(id)
1017 return regex.search(s, 0)
1020def _isSCSIid(s):
1021 regex = re.compile("^scsi-")
1022 return regex.search(s, 0)
1025def test_scsiserial(session, device):
1026 device = os.path.realpath(device)
1027 if not scsiutil._isSCSIdev(device):
1028 SMlog("util.test_scsiserial: Not a serial device: %s" % device)
1029 return False
1030 serial = ""
1031 try:
1032 serial += scsiutil.getserial(device)
1033 except:
1034 # Error allowed, SCSIid is the important one
1035 pass
1037 try:
1038 scsiID = scsiutil.getSCSIid(device)
1039 except:
1040 SMlog("util.test_scsiserial: Unable to verify serial or SCSIid of device: %s" \
1041 % device)
1042 return False
1043 if not len(scsiID):
1044 SMlog("util.test_scsiserial: Unable to identify scsi device [%s] via scsiID" \
1045 % device)
1046 return False
1048 try:
1049 SRs = session.xenapi.SR.get_all_records()
1050 except:
1051 raise xs_errors.XenError('APIFailure')
1052 for SR in SRs:
1053 record = SRs[SR]
1054 conf = record["sm_config"]
1055 if 'devserial' in conf:
1056 for dev in conf['devserial'].split(','):
1057 if _isSCSIid(dev):
1058 if match_scsiID(dev, scsiID):
1059 return True
1060 elif len(serial) and dev == serial:
1061 return True
1062 return False
1065def default(self, field, thunk):
1066 try:
1067 return getattr(self, field)
1068 except:
1069 return thunk()
1072def list_VDI_records_in_sr(sr):
1073 """Helper function which returns a list of all VDI records for this SR
1074 stored in the XenAPI server, useful for implementing SR.scan"""
1075 sr_ref = sr.session.xenapi.SR.get_by_uuid(sr.uuid)
1076 vdis = sr.session.xenapi.VDI.get_all_records_where("field \"SR\" = \"%s\"" % sr_ref)
1077 return vdis
1080# Given a partition (e.g. sda1), get a disk name:
1081def diskFromPartition(partition):
1082 # check whether this is a device mapper device (e.g. /dev/dm-0)
1083 m = re.match('(/dev/)?(dm-[0-9]+)(p[0-9]+)?$', partition)
1084 if m is not None: 1084 ↛ 1085line 1084 didn't jump to line 1085, because the condition on line 1084 was never true
1085 return m.group(2)
1087 numlen = 0 # number of digit characters
1088 m = re.match(r"\D+(\d+)", partition)
1089 if m is not None: 1089 ↛ 1090line 1089 didn't jump to line 1090, because the condition on line 1089 was never true
1090 numlen = len(m.group(1))
1092 # is it a cciss?
1093 if True in [partition.startswith(x) for x in ['cciss', 'ida', 'rd']]: 1093 ↛ 1094line 1093 didn't jump to line 1094, because the condition on line 1093 was never true
1094 numlen += 1 # need to get rid of trailing 'p'
1096 # is it a mapper path?
1097 if partition.startswith("mapper"): 1097 ↛ 1098line 1097 didn't jump to line 1098, because the condition on line 1097 was never true
1098 if re.search("p[0-9]*$", partition):
1099 numlen = len(re.match(r"\d+", partition[::-1]).group(0)) + 1
1100 SMlog("Found mapper part, len %d" % numlen)
1101 else:
1102 numlen = 0
1104 # is it /dev/disk/by-id/XYZ-part<k>?
1105 if partition.startswith("disk/by-id"): 1105 ↛ 1106line 1105 didn't jump to line 1106, because the condition on line 1105 was never true
1106 return partition[:partition.rfind("-part")]
1108 return partition[:len(partition) - numlen]
1111def dom0_disks():
1112 """Disks carrying dom0, e.g. ['/dev/sda']"""
1113 disks = []
1114 with open("/etc/mtab", 'r') as f:
1115 for line in f:
1116 (dev, mountpoint, fstype, opts, freq, passno) = line.split(' ')
1117 if mountpoint == '/':
1118 disk = diskFromPartition(dev)
1119 if not (disk in disks):
1120 disks.append(disk)
1121 SMlog("Dom0 disks: %s" % disks)
1122 return disks
1125def set_scheduler_sysfs_node(node, scheds):
1126 """
1127 Set the scheduler for a sysfs node (e.g. '/sys/block/sda')
1128 according to prioritized list schedulers
1129 Try to set the first item, then fall back to the next on failure
1130 """
1132 path = os.path.join(node, "queue", "scheduler")
1133 if not os.path.exists(path): 1133 ↛ 1137line 1133 didn't jump to line 1137, because the condition on line 1133 was never false
1134 SMlog("no path %s" % path)
1135 return
1137 stored_error = None
1138 for sched in scheds:
1139 try:
1140 with open(path, 'w') as file:
1141 file.write("%s\n" % sched)
1142 SMlog("Set scheduler to [%s] on [%s]" % (sched, node))
1143 return
1144 except (OSError, IOError) as err:
1145 stored_error = err
1147 SMlog("Error setting schedulers to [%s] on [%s], %s" % (scheds, node, str(stored_error)))
1150def set_scheduler(dev, schedulers=None):
1151 if schedulers is None: 1151 ↛ 1154line 1151 didn't jump to line 1154, because the condition on line 1151 was never false
1152 schedulers = ["none", "noop"]
1154 devices = []
1155 if not scsiutil.match_dm(dev): 1155 ↛ 1159line 1155 didn't jump to line 1159, because the condition on line 1155 was never false
1156 # Remove partition numbers
1157 devices.append(diskFromPartition(dev).replace('/', '!'))
1158 else:
1159 rawdev = diskFromPartition(dev)
1160 devices = [os.path.realpath(x)[5:] for x in scsiutil._genReverseSCSIidmap(rawdev.split('/')[-1])]
1162 for d in devices:
1163 set_scheduler_sysfs_node("/sys/block/%s" % d, schedulers)
1166# This function queries XAPI for the existing VDI records for this SR
1167def _getVDIs(srobj):
1168 VDIs = []
1169 try:
1170 sr_ref = getattr(srobj, 'sr_ref')
1171 except AttributeError:
1172 return VDIs
1174 refs = srobj.session.xenapi.SR.get_VDIs(sr_ref)
1175 for vdi in refs:
1176 ref = srobj.session.xenapi.VDI.get_record(vdi)
1177 ref['vdi_ref'] = vdi
1178 VDIs.append(ref)
1179 return VDIs
1182def _getVDI(srobj, vdi_uuid):
1183 vdi = srobj.session.xenapi.VDI.get_by_uuid(vdi_uuid)
1184 ref = srobj.session.xenapi.VDI.get_record(vdi)
1185 ref['vdi_ref'] = vdi
1186 return ref
1189def _convertDNS(name):
1190 addr = socket.getaddrinfo(name, None)[0][4][0]
1191 return addr
1194def _containsVDIinuse(srobj):
1195 VDIs = _getVDIs(srobj)
1196 for vdi in VDIs:
1197 if not vdi['managed']:
1198 continue
1199 sm_config = vdi['sm_config']
1200 if 'SRRef' in sm_config:
1201 try:
1202 PBDs = srobj.session.xenapi.SR.get_PBDs(sm_config['SRRef'])
1203 for pbd in PBDs:
1204 record = PBDs[pbd]
1205 if record["host"] == srobj.host_ref and \
1206 record["currently_attached"]:
1207 return True
1208 except:
1209 pass
1210 return False
1213def isVDICommand(cmd):
1214 if cmd is None or cmd in ["vdi_attach", "vdi_detach",
1215 "vdi_activate", "vdi_deactivate",
1216 "vdi_epoch_begin", "vdi_epoch_end"]:
1217 return True
1218 else:
1219 return False
1222#########################
1223# Daemon helper functions
1224def p_id_fork():
1225 try:
1226 p_id = os.fork()
1227 except OSError as e:
1228 print("Fork failed: %s (%d)" % (e.strerror, e.errno))
1229 sys.exit(-1)
1231 if (p_id == 0):
1232 os.setsid()
1233 try:
1234 p_id = os.fork()
1235 except OSError as e:
1236 print("Fork failed: %s (%d)" % (e.strerror, e.errno))
1237 sys.exit(-1)
1238 if (p_id == 0):
1239 os.chdir('/opt/xensource/sm')
1240 os.umask(0)
1241 else:
1242 os._exit(0)
1243 else:
1244 os._exit(0)
1247def daemon():
1248 p_id_fork()
1249 # Query the max file descriptor parameter for this process
1250 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1252 # Close any fds that are open
1253 for fd in range(0, maxfd):
1254 try:
1255 os.close(fd)
1256 except:
1257 pass
1259 # Redirect STDIN to STDOUT and STDERR
1260 os.open('/dev/null', os.O_RDWR)
1261 os.dup2(0, 1)
1262 os.dup2(0, 2)
1264################################################################################
1265#
1266# Fist points
1267#
1269# * The global variable 'fistpoint' define the list of all possible fistpoints;
1270#
1271# * To activate a fistpoint called 'name', you need to create the file '/tmp/fist_name'
1272# on the SR master;
1273#
1274# * At the moment, activating a fist point can lead to two possible behaviors:
1275# - if '/tmp/fist_LVHDRT_exit' exists, then the function called during the fistpoint is _exit;
1276# - otherwise, the function called is _pause.
1278def _pause(secs, name):
1279 SMlog("Executing fist point %s: sleeping %d seconds ..." % (name, secs))
1280 time.sleep(secs)
1281 SMlog("Executing fist point %s: done" % name)
1284def _exit(name):
1285 SMlog("Executing fist point %s: exiting the current process ..." % name)
1286 raise xs_errors.XenError('FistPoint', opterr='%s' % name)
1289class FistPoint:
1290 def __init__(self, points):
1291 #SMlog("Fist points loaded")
1292 self.points = points
1294 def is_legal(self, name):
1295 return (name in self.points)
1297 def is_active(self, name):
1298 return os.path.exists("/tmp/fist_%s" % name)
1300 def mark_sr(self, name, sruuid, started):
1301 session = get_localAPI_session()
1302 try:
1303 sr = session.xenapi.SR.get_by_uuid(sruuid)
1305 if started:
1306 session.xenapi.SR.add_to_other_config(sr, name, "active")
1307 else:
1308 session.xenapi.SR.remove_from_other_config(sr, name)
1309 finally:
1310 session.xenapi.session.logout()
1312 def activate(self, name, sruuid):
1313 if name in self.points:
1314 if self.is_active(name):
1315 self.mark_sr(name, sruuid, True)
1316 if self.is_active("LVHDRT_exit"): 1316 ↛ 1317line 1316 didn't jump to line 1317, because the condition on line 1316 was never true
1317 self.mark_sr(name, sruuid, False)
1318 _exit(name)
1319 else:
1320 _pause(FIST_PAUSE_PERIOD, name)
1321 self.mark_sr(name, sruuid, False)
1322 else:
1323 SMlog("Unknown fist point: %s" % name)
1325 def activate_custom_fn(self, name, fn):
1326 if name in self.points: 1326 ↛ 1332line 1326 didn't jump to line 1332, because the condition on line 1326 was never false
1327 if self.is_active(name): 1327 ↛ 1328line 1327 didn't jump to line 1328, because the condition on line 1327 was never true
1328 SMlog("Executing fist point %s: starting ..." % name)
1329 fn()
1330 SMlog("Executing fist point %s: done" % name)
1331 else:
1332 SMlog("Unknown fist point: %s" % name)
1335def list_find(f, seq):
1336 for item in seq:
1337 if f(item):
1338 return item
1340GCPAUSE_FISTPOINT = "GCLoop_no_pause"
1342fistpoint = FistPoint(["LVHDRT_finding_a_suitable_pair",
1343 "LVHDRT_inflating_the_parent",
1344 "LVHDRT_resizing_while_vdis_are_paused",
1345 "LVHDRT_coalescing_VHD_data",
1346 "LVHDRT_coalescing_before_inflate_grandparent",
1347 "LVHDRT_relinking_grandchildren",
1348 "LVHDRT_before_create_relink_journal",
1349 "LVHDRT_xapiSM_serialization_tests",
1350 "LVHDRT_clone_vdi_after_create_journal",
1351 "LVHDRT_clone_vdi_after_shrink_parent",
1352 "LVHDRT_clone_vdi_after_first_snap",
1353 "LVHDRT_clone_vdi_after_second_snap",
1354 "LVHDRT_clone_vdi_after_parent_hidden",
1355 "LVHDRT_clone_vdi_after_parent_ro",
1356 "LVHDRT_clone_vdi_before_remove_journal",
1357 "LVHDRT_clone_vdi_after_lvcreate",
1358 "LVHDRT_clone_vdi_before_undo_clone",
1359 "LVHDRT_clone_vdi_after_undo_clone",
1360 "LVHDRT_inflate_after_create_journal",
1361 "LVHDRT_inflate_after_setSize",
1362 "LVHDRT_inflate_after_zeroOut",
1363 "LVHDRT_inflate_after_setSizePhys",
1364 "LVHDRT_inflate_after_setSizePhys",
1365 "LVHDRT_coaleaf_before_coalesce",
1366 "LVHDRT_coaleaf_after_coalesce",
1367 "LVHDRT_coaleaf_one_renamed",
1368 "LVHDRT_coaleaf_both_renamed",
1369 "LVHDRT_coaleaf_after_vdirec",
1370 "LVHDRT_coaleaf_before_delete",
1371 "LVHDRT_coaleaf_after_delete",
1372 "LVHDRT_coaleaf_before_remove_j",
1373 "LVHDRT_coaleaf_undo_after_rename",
1374 "LVHDRT_coaleaf_undo_after_rename2",
1375 "LVHDRT_coaleaf_undo_after_refcount",
1376 "LVHDRT_coaleaf_undo_after_deflate",
1377 "LVHDRT_coaleaf_undo_end",
1378 "LVHDRT_coaleaf_stop_after_recovery",
1379 "LVHDRT_coaleaf_finish_after_inflate",
1380 "LVHDRT_coaleaf_finish_end",
1381 "LVHDRT_coaleaf_delay_1",
1382 "LVHDRT_coaleaf_delay_2",
1383 "LVHDRT_coaleaf_delay_3",
1384 "testsm_clone_allow_raw",
1385 "xenrt_default_vdi_type_legacy",
1386 "blktap_activate_inject_failure",
1387 "blktap_activate_error_handling",
1388 GCPAUSE_FISTPOINT,
1389 "cleanup_coalesceVHD_inject_failure",
1390 "cleanup_tracker_no_progress",
1391 "FileSR_fail_hardlink",
1392 "FileSR_fail_snap1",
1393 "FileSR_fail_snap2",
1394 "LVM_journaler_exists",
1395 "LVM_journaler_none",
1396 "LVM_journaler_badname",
1397 "LVM_journaler_readfail",
1398 "LVM_journaler_writefail"])
1401def set_dirty(session, sr):
1402 try:
1403 session.xenapi.SR.add_to_other_config(sr, "dirty", "")
1404 SMlog("set_dirty %s succeeded" % (repr(sr)))
1405 except:
1406 SMlog("set_dirty %s failed (flag already set?)" % (repr(sr)))
1409def doesFileHaveOpenHandles(fileName):
1410 SMlog("Entering doesFileHaveOpenHandles with file: %s" % fileName)
1411 (retVal, processAndPidTuples) = \
1412 findRunningProcessOrOpenFile(fileName, False)
1414 if not retVal:
1415 SMlog("Failed to determine if file %s has open handles." % \
1416 fileName)
1417 # err on the side of caution
1418 return True
1419 else:
1420 if len(processAndPidTuples) > 0:
1421 return True
1422 else:
1423 return False
1426# extract SR uuid from the passed in devmapper entry and return
1427# /dev/mapper/VG_XenStorage--c3d82e92--cb25--c99b--b83a--482eebab4a93-MGT
1428def extractSRFromDevMapper(path):
1429 try:
1430 path = os.path.basename(path)
1431 path = path[len('VG_XenStorage-') + 1:]
1432 path = path.replace('--', '/')
1433 path = path[0:path.rfind('-')]
1434 return path.replace('/', '-')
1435 except:
1436 return ''
1439def pid_is_alive(pid):
1440 """
1441 Try to kill PID with signal 0.
1442 If we succeed, the PID is alive, so return True.
1443 If we get an EPERM error, the PID is alive but we are not allowed to
1444 signal it. Still return true.
1445 Any other error (e.g. ESRCH), return False
1446 """
1447 try:
1448 os.kill(pid, 0)
1449 return True
1450 except OSError as e:
1451 if e.errno == errno.EPERM:
1452 return True
1453 return False
1456# Looks at /proc and figures either
1457# If a process is still running (default), returns open file names
1458# If any running process has open handles to the given file (process = False)
1459# returns process names and pids
1460def findRunningProcessOrOpenFile(name, process=True):
1461 retVal = True
1462 links = []
1463 processandpids = []
1464 sockets = set()
1465 try:
1466 SMlog("Entering findRunningProcessOrOpenFile with params: %s" % \
1467 [name, process])
1469 # Look at all pids
1470 pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
1471 for pid in sorted(pids):
1472 try:
1473 try:
1474 f = None
1475 f = open(os.path.join('/proc', pid, 'cmdline'), 'r')
1476 prog = f.read()[:-1]
1477 if prog: 1477 ↛ 1486line 1477 didn't jump to line 1486, because the condition on line 1477 was never false
1478 # Just want the process name
1479 argv = prog.split('\x00')
1480 prog = argv[0]
1481 except IOError as e:
1482 if e.errno in (errno.ENOENT, errno.ESRCH):
1483 SMlog("ERROR %s reading %s, ignore" % (e.errno, pid))
1484 continue
1485 finally:
1486 if f is not None: 1486 ↛ 1471, 1486 ↛ 14892 missed branches: 1) line 1486 didn't jump to line 1471, because the continue on line 1484 wasn't executed, 2) line 1486 didn't jump to line 1489, because the condition on line 1486 was never false
1487 f.close() 1487 ↛ 1471line 1487 didn't jump to line 1471, because the continue on line 1484 wasn't executed
1489 try:
1490 fd_dir = os.path.join('/proc', pid, 'fd')
1491 files = os.listdir(fd_dir)
1492 except OSError as e:
1493 if e.errno in (errno.ENOENT, errno.ESRCH):
1494 SMlog("ERROR %s reading fds for %s, ignore" % (e.errno, pid))
1495 # Ignore pid that are no longer valid
1496 continue
1497 else:
1498 raise
1500 for file in files:
1501 try:
1502 link = os.readlink(os.path.join(fd_dir, file))
1503 except OSError:
1504 continue
1506 if process: 1506 ↛ 1511line 1506 didn't jump to line 1511, because the condition on line 1506 was never false
1507 if name == prog: 1507 ↛ 1500line 1507 didn't jump to line 1500, because the condition on line 1507 was never false
1508 links.append(link)
1509 else:
1510 # need to return process name and pid tuples
1511 if link == name:
1512 processandpids.append((prog, pid))
1514 # Get the connected sockets
1515 if name == prog:
1516 sockets.update(get_connected_sockets(pid))
1518 # We will only have a non-empty processandpids if some fd entries were found.
1519 # Before returning them, verify that all the PIDs in question are properly alive.
1520 # There is no specific guarantee of when a PID's /proc directory will disappear
1521 # when it exits, particularly relative to filedescriptor cleanup, so we want to
1522 # make sure we're not reporting a false positive.
1523 processandpids = [x for x in processandpids if pid_is_alive(int(x[1]))]
1524 for pp in processandpids: 1524 ↛ 1525line 1524 didn't jump to line 1525, because the loop on line 1524 never started
1525 SMlog(f"File {name} has an open handle with process {pp[0]} with pid {pp[1]}")
1527 except Exception as e:
1528 SMlog("Exception checking running process or open file handles. " \
1529 "Error: %s" % str(e))
1530 retVal = False
1532 if process: 1532 ↛ 1535line 1532 didn't jump to line 1535, because the condition on line 1532 was never false
1533 return retVal, links, sockets
1534 else:
1535 return retVal, processandpids
1538def get_connected_sockets(pid):
1539 sockets = set()
1540 try:
1541 # Lines in /proc/<pid>/net/unix are formatted as follows
1542 # (see Linux source net/unix/af_unix.c, unix_seq_show() )
1543 # - Pointer address to socket (hex)
1544 # - Refcount (HEX)
1545 # - 0
1546 # - State (HEX, 0 or __SO_ACCEPTCON)
1547 # - Type (HEX - but only 0001 of interest)
1548 # - Connection state (HEX - but only 03, SS_CONNECTED of interest)
1549 # - Inode number
1550 # - Path (optional)
1551 open_sock_matcher = re.compile(
1552 r'^[0-9a-f]+: [0-9A-Fa-f]+ [0-9A-Fa-f]+ [0-9A-Fa-f]+ 0001 03 \d+ (.*)$')
1553 with open(
1554 os.path.join('/proc', str(pid), 'net', 'unix'), 'r') as f:
1555 lines = f.readlines()
1556 for line in lines:
1557 match = open_sock_matcher.match(line)
1558 if match:
1559 sockets.add(match[1])
1560 except OSError as e:
1561 if e.errno in (errno.ENOENT, errno.ESRCH):
1562 # Ignore pid that are no longer valid
1563 SMlog("ERROR %s reading sockets for %s, ignore" %
1564 (e.errno, pid))
1565 else:
1566 raise
1567 return sockets
1570def retry(f, maxretry=20, period=3, exceptions=[Exception]):
1571 retries = 0
1572 while True:
1573 try:
1574 return f()
1575 except Exception as e:
1576 for exception in exceptions:
1577 if isinstance(e, exception):
1578 SMlog('Got exception: {}. Retry number: {}'.format(
1579 str(e), retries
1580 ))
1581 break
1582 else:
1583 SMlog('Got bad exception: {}. Raising...'.format(e))
1584 raise e
1586 retries += 1
1587 if retries >= maxretry:
1588 break
1590 time.sleep(period)
1592 return f()
1595def getCslDevPath(svid):
1596 basepath = "/dev/disk/by-csldev/"
1597 if svid.startswith("NETAPP_"):
1598 # special attention for NETAPP SVIDs
1599 svid_parts = svid.split("__")
1600 globstr = basepath + "NETAPP__LUN__" + "*" + svid_parts[2] + "*" + svid_parts[-1] + "*"
1601 else:
1602 globstr = basepath + svid + "*"
1604 return globstr
1607# Use device in /dev pointed to by cslg path which consists of svid
1608def get_scsiid_from_svid(md_svid):
1609 cslg_path = getCslDevPath(md_svid)
1610 abs_path = glob.glob(cslg_path)
1611 if abs_path:
1612 real_path = os.path.realpath(abs_path[0])
1613 return scsiutil.getSCSIid(real_path)
1614 else:
1615 return None
1618def get_isl_scsiids(session):
1619 # Get cslg type SRs
1620 SRs = session.xenapi.SR.get_all_records_where('field "type" = "cslg"')
1622 # Iterate through the SR to get the scsi ids
1623 scsi_id_ret = []
1624 for SR in SRs:
1625 sr_rec = SRs[SR]
1626 # Use the md_svid to get the scsi id
1627 scsi_id = get_scsiid_from_svid(sr_rec['sm_config']['md_svid'])
1628 if scsi_id:
1629 scsi_id_ret.append(scsi_id)
1631 # Get the vdis in the SR and do the same procedure
1632 vdi_recs = session.xenapi.VDI.get_all_records_where('field "SR" = "%s"' % SR)
1633 for vdi_rec in vdi_recs:
1634 vdi_rec = vdi_recs[vdi_rec]
1635 scsi_id = get_scsiid_from_svid(vdi_rec['sm_config']['SVID'])
1636 if scsi_id:
1637 scsi_id_ret.append(scsi_id)
1639 return scsi_id_ret
1642class extractXVA:
1643 # streams files as a set of file and checksum, caller should remove
1644 # the files, if not needed. The entire directory (Where the files
1645 # and checksum) will only be deleted as part of class cleanup.
1646 HDR_SIZE = 512
1647 BLOCK_SIZE = 512
1648 SIZE_LEN = 12 - 1 # To remove \0 from tail
1649 SIZE_OFFSET = 124
1650 ZERO_FILLED_REC = 2
1651 NULL_IDEN = '\x00'
1652 DIR_IDEN = '/'
1653 CHECKSUM_IDEN = '.checksum'
1654 OVA_FILE = 'ova.xml'
1656 # Init gunzips the file using a subprocess, and reads stdout later
1657 # as and when needed
1658 def __init__(self, filename):
1659 self.__extract_path = ''
1660 self.__filename = filename
1661 cmd = 'gunzip -cd %s' % filename
1662 try:
1663 self.spawn_p = subprocess.Popen(
1664 cmd, shell=True, \
1665 stdin=subprocess.PIPE, stdout=subprocess.PIPE, \
1666 stderr=subprocess.PIPE, close_fds=True)
1667 except Exception as e:
1668 SMlog("Error: %s. Uncompress failed for %s" % (str(e), filename))
1669 raise Exception(str(e))
1671 # Create dir to extract the files
1672 self.__extract_path = tempfile.mkdtemp()
1674 def __del__(self):
1675 shutil.rmtree(self.__extract_path)
1677 # Class supports Generator expression. 'for f_name, checksum in getTuple()'
1678 # returns filename, checksum content. Returns filename, '' in case
1679 # of checksum file missing. e.g. ova.xml
1680 def getTuple(self):
1681 zerod_record = 0
1682 ret_f_name = ''
1683 ret_base_f_name = ''
1685 try:
1686 # Read tar file as sets of file and checksum.
1687 while True:
1688 # Read the output of spawned process, or output of gunzip
1689 f_hdr = self.spawn_p.stdout.read(self.HDR_SIZE)
1691 # Break out in case of end of file
1692 if f_hdr == '':
1693 if zerod_record == extractXVA.ZERO_FILLED_REC:
1694 break
1695 else:
1696 SMlog('Error. Expects %d zero records', \
1697 extractXVA.ZERO_FILLED_REC)
1698 raise Exception('Unrecognized end of file')
1700 # Watch out for zero records, two zero records
1701 # denote end of file.
1702 if f_hdr == extractXVA.NULL_IDEN * extractXVA.HDR_SIZE:
1703 zerod_record += 1
1704 continue
1706 f_name = f_hdr[:f_hdr.index(extractXVA.NULL_IDEN)]
1707 # File header may be for a folder, if so ignore the header
1708 if not f_name.endswith(extractXVA.DIR_IDEN):
1709 f_size_octal = f_hdr[extractXVA.SIZE_OFFSET: \
1710 extractXVA.SIZE_OFFSET + extractXVA.SIZE_LEN]
1711 f_size = int(f_size_octal, 8)
1712 if f_name.endswith(extractXVA.CHECKSUM_IDEN):
1713 if f_name.rstrip(extractXVA.CHECKSUM_IDEN) == \
1714 ret_base_f_name:
1715 checksum = self.spawn_p.stdout.read(f_size)
1716 yield(ret_f_name, checksum)
1717 else:
1718 # Expects file followed by its checksum
1719 SMlog('Error. Sequence mismatch starting with %s', \
1720 ret_f_name)
1721 raise Exception( \
1722 'Files out of sequence starting with %s', \
1723 ret_f_name)
1724 else:
1725 # In case of ova.xml, read the contents into a file and
1726 # return the file name to the caller. For other files,
1727 # read the contents into a file, it will
1728 # be used when a .checksum file is encountered.
1729 ret_f_name = '%s/%s' % (self.__extract_path, f_name)
1730 ret_base_f_name = f_name
1732 # Check if the folder exists on the target location,
1733 # else create it.
1734 folder_path = ret_f_name[:ret_f_name.rfind('/')]
1735 if not os.path.exists(folder_path):
1736 os.mkdir(folder_path)
1738 # Store the file to the tmp folder, strip the tail \0
1739 f = open(ret_f_name, 'w')
1740 f.write(self.spawn_p.stdout.read(f_size))
1741 f.close()
1742 if f_name == extractXVA.OVA_FILE:
1743 yield(ret_f_name, '')
1745 # Skip zero'd portion of data block
1746 round_off = f_size % extractXVA.BLOCK_SIZE
1747 if round_off != 0:
1748 zeros = self.spawn_p.stdout.read(
1749 extractXVA.BLOCK_SIZE - round_off)
1750 except Exception as e:
1751 SMlog("Error: %s. File set extraction failed %s" % (str(e), \
1752 self.__filename))
1754 # Kill and Drain stdout of the gunzip process,
1755 # else gunzip might block on stdout
1756 os.kill(self.spawn_p.pid, signal.SIGTERM)
1757 self.spawn_p.communicate()
1758 raise Exception(str(e))
1760illegal_xml_chars = [(0x00, 0x08), (0x0B, 0x1F), (0x7F, 0x84), (0x86, 0x9F),
1761 (0xD800, 0xDFFF), (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF),
1762 (0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), (0x3FFFE, 0x3FFFF),
1763 (0x4FFFE, 0x4FFFF), (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF),
1764 (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF), (0x9FFFE, 0x9FFFF),
1765 (0xAFFFE, 0xAFFFF), (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF),
1766 (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF), (0xFFFFE, 0xFFFFF),
1767 (0x10FFFE, 0x10FFFF)]
1769illegal_ranges = ["%s-%s" % (chr(low), chr(high))
1770 for (low, high) in illegal_xml_chars
1771 if low < sys.maxunicode]
1773illegal_xml_re = re.compile(u'[%s]' % u''.join(illegal_ranges))
1776def isLegalXMLString(s):
1777 """Tells whether this is a valid XML string (i.e. it does not contain
1778 illegal XML characters specified in
1779 http://www.w3.org/TR/2004/REC-xml-20040204/#charsets).
1780 """
1782 if len(s) > 0:
1783 return re.search(illegal_xml_re, s) is None
1784 else:
1785 return True
1788def unictrunc(string, max_bytes):
1789 """
1790 Given a string, returns the largest number of elements for a prefix
1791 substring of it, such that the UTF-8 encoding of this substring takes no
1792 more than the given number of bytes.
1794 The string may be given as a unicode string or a UTF-8 encoded byte
1795 string, and the number returned will be in characters or bytes
1796 accordingly. Note that in the latter case, the substring will still be a
1797 valid UTF-8 encoded string (which is to say, it won't have been truncated
1798 part way through a multibyte sequence for a unicode character).
1800 string: the string to truncate
1801 max_bytes: the maximum number of bytes the truncated string can be
1802 """
1803 if isinstance(string, str):
1804 return_chars = True
1805 else:
1806 return_chars = False
1807 string = string.decode('UTF-8')
1809 cur_chars = 0
1810 cur_bytes = 0
1811 for char in string:
1812 charsize = len(char.encode('UTF-8'))
1813 if cur_bytes + charsize > max_bytes:
1814 break
1815 else:
1816 cur_chars += 1
1817 cur_bytes += charsize
1818 return cur_chars if return_chars else cur_bytes
1821def hideValuesInPropMap(propmap, propnames):
1822 """
1823 Worker function: input simple map of prop name/value pairs, and
1824 a list of specific propnames whose values we want to hide.
1825 Loop through the "hide" list, and if any are found, hide the
1826 value and return the altered map.
1827 If none found, return the original map
1828 """
1829 matches = []
1830 for propname in propnames:
1831 if propname in propmap: 1831 ↛ 1832line 1831 didn't jump to line 1832, because the condition on line 1831 was never true
1832 matches.append(propname)
1834 if matches: 1834 ↛ 1835line 1834 didn't jump to line 1835, because the condition on line 1834 was never true
1835 deepCopyRec = copy.deepcopy(propmap)
1836 for match in matches:
1837 deepCopyRec[match] = '******'
1838 return deepCopyRec
1840 return propmap
1841# define the list of propnames whose value we want to hide
1843PASSWD_PROP_KEYS = ['password', 'cifspassword', 'chappassword', 'incoming_chappassword']
1844DEFAULT_SEGMENT_LEN = 950
1847def hidePasswdInConfig(config):
1848 """
1849 Function to hide passwd values in a simple prop map,
1850 for example "device_config"
1851 """
1852 return hideValuesInPropMap(config, PASSWD_PROP_KEYS)
1855def hidePasswdInParams(params, configProp):
1856 """
1857 Function to hide password values in a specified property which
1858 is a simple map of prop name/values, and is itself an prop entry
1859 in a larger property map.
1860 For example, param maps containing "device_config", or
1861 "sm_config", etc
1862 """
1863 params[configProp] = hideValuesInPropMap(params[configProp], PASSWD_PROP_KEYS)
1864 return params
1867def hideMemberValuesInXmlParams(xmlParams, propnames=PASSWD_PROP_KEYS):
1868 """
1869 Function to hide password values in XML params, specifically
1870 for the XML format of incoming params to SR modules.
1871 Uses text parsing: loop through the list of specific propnames
1872 whose values we want to hide, and:
1873 - Assemble a full "prefix" containing each property name, e.g.,
1874 "<member><name>password</name><value>"
1875 - Test the XML if it contains that string, save the index.
1876 - If found, get the index of the ending tag
1877 - Truncate the return string starting with the password value.
1878 - Append the substitute "*******" value string.
1879 - Restore the rest of the original string starting with the end tag.
1880 """
1881 findStrPrefixHead = "<member><name>"
1882 findStrPrefixTail = "</name><value>"
1883 findStrSuffix = "</value>"
1884 strlen = len(xmlParams)
1886 for propname in propnames:
1887 findStrPrefix = findStrPrefixHead + propname + findStrPrefixTail
1888 idx = xmlParams.find(findStrPrefix)
1889 if idx != -1: # if found any of them
1890 idx += len(findStrPrefix)
1891 idx2 = xmlParams.find(findStrSuffix, idx)
1892 if idx2 != -1:
1893 retStr = xmlParams[0:idx]
1894 retStr += "******"
1895 retStr += xmlParams[idx2:strlen]
1896 return retStr
1897 else:
1898 return xmlParams
1899 return xmlParams
1902def splitXmlText(xmlData, segmentLen=DEFAULT_SEGMENT_LEN, showContd=False):
1903 """
1904 Split xml string data into substrings small enough for the
1905 syslog line length limit. Split at tag end markers ( ">" ).
1906 Usage:
1907 strList = []
1908 strList = splitXmlText( longXmlText, maxLineLen ) # maxLineLen is optional
1909 """
1910 remainingData = str(xmlData)
1912 # "Un-pretty-print"
1913 remainingData = remainingData.replace('\n', '')
1914 remainingData = remainingData.replace('\t', '')
1916 remainingChars = len(remainingData)
1917 returnData = ''
1919 thisLineNum = 0
1920 while remainingChars > segmentLen:
1921 thisLineNum = thisLineNum + 1
1922 index = segmentLen
1923 tmpStr = remainingData[:segmentLen]
1924 tmpIndex = tmpStr.rfind('>')
1925 if tmpIndex != -1:
1926 index = tmpIndex + 1
1928 tmpStr = tmpStr[:index]
1929 remainingData = remainingData[index:]
1930 remainingChars = len(remainingData)
1932 if showContd:
1933 if thisLineNum != 1:
1934 tmpStr = '(Cont\'d): ' + tmpStr
1935 tmpStr = tmpStr + ' (Cont\'d):'
1937 returnData += tmpStr + '\n'
1939 if showContd and thisLineNum > 0:
1940 remainingData = '(Cont\'d): ' + remainingData
1941 returnData += remainingData
1943 return returnData
1946def inject_failure():
1947 raise Exception('injected failure')
1950def open_atomic(path, mode=None):
1951 """Atomically creates a file if, and only if it does not already exist.
1952 Leaves the file open and returns the file object.
1954 path: the path to atomically open
1955 mode: "r" (read), "w" (write), or "rw" (read/write)
1956 returns: an open file object"""
1958 assert path
1960 flags = os.O_CREAT | os.O_EXCL
1961 modes = {'r': os.O_RDONLY, 'w': os.O_WRONLY, 'rw': os.O_RDWR}
1962 if mode:
1963 if mode not in modes:
1964 raise Exception('invalid access mode ' + mode)
1965 flags |= modes[mode]
1966 fd = os.open(path, flags)
1967 try:
1968 if mode:
1969 return os.fdopen(fd, mode)
1970 else:
1971 return os.fdopen(fd)
1972 except:
1973 os.close(fd)
1974 raise
1977def isInvalidVDI(exception):
1978 return exception.details[0] == "HANDLE_INVALID" or \
1979 exception.details[0] == "UUID_INVALID"
1982def get_pool_restrictions(session):
1983 """Returns pool restrictions as a map, @session must be already
1984 established."""
1985 return list(session.xenapi.pool.get_all_records().values())[0]['restrictions']
1988def read_caching_is_restricted(session):
1989 """Tells whether read caching is restricted."""
1990 if session is None: 1990 ↛ 1991line 1990 didn't jump to line 1991, because the condition on line 1990 was never true
1991 return True
1992 restrictions = get_pool_restrictions(session)
1993 if 'restrict_read_caching' in restrictions and \ 1993 ↛ 1995line 1993 didn't jump to line 1995, because the condition on line 1993 was never true
1994 restrictions['restrict_read_caching'] == "true":
1995 return True
1996 return False
1999def sessions_less_than_targets(other_config, device_config):
2000 if 'multihomelist' in device_config and 'iscsi_sessions' in other_config:
2001 sessions = int(other_config['iscsi_sessions'])
2002 targets = len(device_config['multihomelist'].split(','))
2003 SMlog("Targets %d and iscsi_sessions %d" % (targets, sessions))
2004 return (sessions < targets)
2005 else:
2006 return False
2009def enable_and_start_service(name, start):
2010 attempt = 0
2011 while True:
2012 attempt += 1
2013 fn = 'enable' if start else 'disable'
2014 args = ('systemctl', fn, '--now', name)
2015 (ret, out, err) = doexec(args)
2016 if ret == 0:
2017 return
2018 elif attempt >= 3:
2019 raise Exception(
2020 'Failed to {} {}: {} {}'.format(fn, name, out, err)
2021 )
2022 time.sleep(1)
2025def stop_service(name):
2026 args = ('systemctl', 'stop', name)
2027 (ret, out, err) = doexec(args)
2028 if ret == 0:
2029 return
2030 raise Exception('Failed to stop {}: {} {}'.format(name, out, err))
2033def restart_service(name):
2034 attempt = 0
2035 while True:
2036 attempt += 1
2037 SMlog('Restarting service {} {}...'.format(name, attempt))
2038 args = ('systemctl', 'restart', name)
2039 (ret, out, err) = doexec(args)
2040 if ret == 0:
2041 return
2042 elif attempt >= 3:
2043 SMlog('Restart service FAILED {} {}'.format(name, attempt))
2044 raise Exception(
2045 'Failed to restart {}: {} {}'.format(name, out, err)
2046 )
2047 time.sleep(1)
2050def check_pid_exists(pid):
2051 try:
2052 os.kill(pid, 0)
2053 except OSError:
2054 return False
2055 else:
2056 return True
2059def make_profile(name, function):
2060 """
2061 Helper to execute cProfile using unique log file.
2062 """
2064 import cProfile
2065 import itertools
2066 import os.path
2067 import time
2069 assert name
2070 assert function
2072 FOLDER = '/tmp/sm-perfs/'
2073 makedirs(FOLDER)
2075 filename = time.strftime('{}_%Y%m%d_%H%M%S.prof'.format(name))
2077 def gen_path(path):
2078 yield path
2079 root, ext = os.path.splitext(path)
2080 for i in itertools.count(start=1, step=1):
2081 yield root + '.{}.'.format(i) + ext
2083 for profile_path in gen_path(FOLDER + filename):
2084 try:
2085 file = open_atomic(profile_path, 'w')
2086 file.close()
2087 break
2088 except OSError as e:
2089 if e.errno == errno.EEXIST:
2090 pass
2091 else:
2092 raise
2094 try:
2095 SMlog('* Start profiling of {} ({}) *'.format(name, filename))
2096 cProfile.runctx('function()', None, locals(), profile_path)
2097 finally:
2098 SMlog('* End profiling of {} ({}) *'.format(name, filename))
2101def strtobool(str):
2102 # Note: `distutils` package is deprecated and slated for removal in Python 3.12.
2103 # There is not alternative for strtobool.
2104 # See: https://peps.python.org/pep-0632/#migration-advice
2105 # So this is a custom implementation with differences:
2106 # - A boolean is returned instead of integer
2107 # - Empty string and None are supported (False is returned in this case)
2108 if not str: 2108 ↛ 2110line 2108 didn't jump to line 2110, because the condition on line 2108 was never false
2109 return False
2110 str = str.lower()
2111 if str in ('y', 'yes', 't', 'true', 'on', '1'):
2112 return True
2113 if str in ('n', 'no', 'f', 'false', 'off', '0'):
2114 return False
2115 raise ValueError("invalid truth value '{}'".format(str))
2118def find_executable(name):
2119 return shutil.which(name)
2122def conditional_decorator(decorator, condition):
2123 def wrapper(func):
2124 if not condition: 2124 ↛ 2126line 2124 didn't jump to line 2126, because the condition on line 2124 was never false
2125 return func
2126 return decorator(func)
2127 return wrapper