4 # Copyright (C) 2006, 2007 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Ganeti small utilities
45 from cStringIO import StringIO
47 from ganeti import errors
48 from ganeti import constants
52 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
58 class RunResult(object):
59 """Simple class for holding the result of running external programs.
62 exit_code: the exit code of the program, or None (if the program
64 signal: numeric signal that caused the program to finish, or None
65 (if the program wasn't terminated by a signal)
66 stdout: the standard output of the program
67 stderr: the standard error of the program
68 failed: a Boolean value which is True in case the program was
69 terminated by a signal or exited with a non-zero exit code
70 fail_reason: a string detailing the termination reason
73 __slots__ = ["exit_code", "signal", "stdout", "stderr",
74 "failed", "fail_reason", "cmd"]
77 def __init__(self, exit_code, signal_, stdout, stderr, cmd):
79 self.exit_code = exit_code
83 self.failed = (signal_ is not None or exit_code != 0)
85 if self.signal is not None:
86 self.fail_reason = "terminated by signal %s" % self.signal
87 elif self.exit_code is not None:
88 self.fail_reason = "exited with exit code %s" % self.exit_code
90 self.fail_reason = "unable to determine termination reason"
93 logging.debug("Command '%s' failed (%s); output: %s",
94 self.cmd, self.fail_reason, self.output)
97 """Returns the combined stdout and stderr for easier usage.
100 return self.stdout + self.stderr
102 output = property(_GetOutput, None, None, "Return full output")
106 """Execute a (shell) command.
108 The command should not read from its standard input, as it will be
112 cmd: command to run. (str)
114 Returns: `RunResult` instance
118 raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
120 if isinstance(cmd, list):
121 cmd = [str(val) for val in cmd]
122 strcmd = " ".join(cmd)
127 logging.debug("RunCmd '%s'", strcmd)
128 env = os.environ.copy()
130 poller = select.poll()
131 child = subprocess.Popen(cmd, shell=shell,
132 stderr=subprocess.PIPE,
133 stdout=subprocess.PIPE,
134 stdin=subprocess.PIPE,
135 close_fds=True, env=env)
138 poller.register(child.stdout, select.POLLIN)
139 poller.register(child.stderr, select.POLLIN)
143 child.stdout.fileno(): (out, child.stdout),
144 child.stderr.fileno(): (err, child.stderr),
147 status = fcntl.fcntl(fd, fcntl.F_GETFL)
148 fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
151 for fd, event in poller.poll():
152 if event & select.POLLIN or event & select.POLLPRI:
153 data = fdmap[fd][1].read()
154 # no data from read signifies EOF (the same as POLLHUP)
156 poller.unregister(fd)
159 fdmap[fd][0].write(data)
160 if (event & select.POLLNVAL or event & select.POLLHUP or
161 event & select.POLLERR):
162 poller.unregister(fd)
168 status = child.wait()
176 return RunResult(exitcode, signal_, out, err, strcmd)
179 def RemoveFile(filename):
180 """Remove a file ignoring some errors.
182 Remove a file, ignoring non-existing ones or directories. Other
189 if err.errno not in (errno.ENOENT, errno.EISDIR):
193 def _FingerprintFile(filename):
194 """Compute the fingerprint of a file.
196 If the file does not exist, a None will be returned
200 filename - Filename (str)
203 if not (os.path.exists(filename) and os.path.isfile(filename)):
216 return fp.hexdigest()
219 def FingerprintFiles(files):
220 """Compute fingerprints for a list of files.
223 files - array of filenames. ( [str, ...] )
226 dictionary of filename: fingerprint for the files that exist
231 for filename in files:
232 cksum = _FingerprintFile(filename)
234 ret[filename] = cksum
239 def CheckDict(target, template, logname=None):
240 """Ensure a dictionary has a required set of keys.
242 For the given dictionaries `target` and `template`, ensure target
243 has all the keys from template. Missing keys are added with values
247 target - the dictionary to check
248 template - template dictionary
249 logname - a caller-chosen string to identify the debug log
250 entry; if None, no logging will be done
260 target[k] = template[k]
262 if missing and logname:
263 logging.warning('%s missing keys %s', logname, ', '.join(missing))
266 def IsProcessAlive(pid):
267 """Check if a given pid exists on the system.
269 Returns: true or false, depending on if the pid exists or not
271 Remarks: zombie processes treated as not alive, and giving a pid <=
272 0 makes the function to return False.
279 f = open("/proc/%d/status" % pid)
281 if err.errno in (errno.ENOENT, errno.ENOTDIR):
288 state = data[1].split()
289 if len(state) > 1 and state[1] == "Z":
297 def ReadPidFile(pidfile):
298 """Read the pid from a file.
300 @param pidfile: Path to a file containing the pid to be checked
301 @type pidfile: string (filename)
302 @return: The process id, if the file exista and contains a valid PID,
308 pf = open(pidfile, 'r')
309 except EnvironmentError, err:
310 if err.errno != errno.ENOENT:
311 logging.exception("Can't read pid file?!")
316 except ValueError, err:
317 logging.info("Can't parse pid file contents", exc_info=True)
323 def MatchNameComponent(key, name_list):
324 """Try to match a name against a list.
326 This function will try to match a name like test1 against a list
327 like ['test1.example.com', 'test2.example.com', ...]. Against this
328 list, 'test1' as well as 'test1.example' will match, but not
329 'test1.ex'. A multiple match will be considered as no match at all
330 (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
333 key: the name to be searched
334 name_list: the list of strings against which to search the key
337 None if there is no match *or* if there are multiple matches
338 otherwise the element from the list which matches
341 mo = re.compile("^%s(\..*)?$" % re.escape(key))
342 names_filtered = [name for name in name_list if mo.match(name) is not None]
343 if len(names_filtered) != 1:
345 return names_filtered[0]
349 """Class implementing resolver and hostname functionality
352 def __init__(self, name=None):
353 """Initialize the host name object.
355 If the name argument is not passed, it will use this system's
360 name = self.SysName()
363 self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
364 self.ip = self.ipaddrs[0]
367 """Returns the hostname without domain.
370 return self.name.split('.')[0]
374 """Return the current system's name.
376 This is simply a wrapper over socket.gethostname()
379 return socket.gethostname()
382 def LookupHostname(hostname):
386 hostname: hostname to look up
389 a tuple (name, aliases, ipaddrs) as returned by socket.gethostbyname_ex
390 in case of errors in resolving, we raise a ResolverError
394 result = socket.gethostbyname_ex(hostname)
395 except socket.gaierror, err:
396 # hostname not found in DNS
397 raise errors.ResolverError(hostname, err.args[0], err.args[1])
402 def ListVolumeGroups():
403 """List volume groups and their size
406 Dictionary with keys volume name and values the size of the volume
409 command = "vgs --noheadings --units m --nosuffix -o name,size"
410 result = RunCmd(command)
415 for line in result.stdout.splitlines():
417 name, size = line.split()
418 size = int(float(size))
419 except (IndexError, ValueError), err:
420 logging.error("Invalid output from vgs (%s): %s", err, line)
428 def BridgeExists(bridge):
429 """Check whether the given bridge exists in the system
432 True if it does, false otherwise.
435 return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
438 def NiceSort(name_list):
439 """Sort a list of strings based on digit and non-digit groupings.
441 Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
442 sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
444 The sort algorithm breaks each name in groups of either only-digits
445 or no-digits. Only the first eight such groups are considered, and
446 after that we just use what's left of the string.
449 - a copy of the list sorted according to our algorithm
452 _SORTER_BASE = "(\D+|\d+)"
453 _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
454 _SORTER_BASE, _SORTER_BASE,
455 _SORTER_BASE, _SORTER_BASE,
456 _SORTER_BASE, _SORTER_BASE)
457 _SORTER_RE = re.compile(_SORTER_FULL)
458 _SORTER_NODIGIT = re.compile("^\D*$")
460 """Attempts to convert a variable to integer."""
461 if val is None or _SORTER_NODIGIT.match(val):
466 to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
467 for name in name_list]
469 return [tup[1] for tup in to_sort]
472 def TryConvert(fn, val):
473 """Try to convert a value ignoring errors.
475 This function tries to apply function `fn` to `val`. If no
476 ValueError or TypeError exceptions are raised, it will return the
477 result, else it will return the original value. Any other exceptions
478 are propagated to the caller.
483 except (ValueError, TypeError), err:
489 """Verifies the syntax of an IP address.
491 This function checks if the ip address passes is valid or not based
492 on syntax (not ip range, class calculations or anything).
495 unit = "(0|[1-9]\d{0,2})"
496 return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
499 def IsValidShellParam(word):
500 """Verifies is the given word is safe from the shell's p.o.v.
502 This means that we can pass this to a command via the shell and be
503 sure that it doesn't alter the command line and is passed as such to
506 Note that we are overly restrictive here, in order to be on the safe
510 return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
513 def BuildShellCmd(template, *args):
514 """Build a safe shell command line from the given arguments.
516 This function will check all arguments in the args list so that they
517 are valid shell parameters (i.e. they don't contain shell
518 metacharaters). If everything is ok, it will return the result of
523 if not IsValidShellParam(word):
524 raise errors.ProgrammerError("Shell argument '%s' contains"
525 " invalid characters" % word)
526 return template % args
529 def FormatUnit(value):
530 """Formats an incoming number of MiB with the appropriate unit.
532 Value needs to be passed as a numeric type. Return value is always a string.
536 return "%dM" % round(value, 0)
538 elif value < (1024 * 1024):
539 return "%0.1fG" % round(float(value) / 1024, 1)
542 return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
545 def ParseUnit(input_string):
546 """Tries to extract number and scale from the given string.
548 Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
549 is specified, it defaults to MiB. Return value is always an int in MiB.
552 m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
554 raise errors.UnitParseError("Invalid format")
556 value = float(m.groups()[0])
560 lcunit = unit.lower()
564 if lcunit in ('m', 'mb', 'mib'):
565 # Value already in MiB
568 elif lcunit in ('g', 'gb', 'gib'):
571 elif lcunit in ('t', 'tb', 'tib'):
575 raise errors.UnitParseError("Unknown unit: %s" % unit)
577 # Make sure we round up
578 if int(value) < value:
581 # Round up to the next multiple of 4
584 value += 4 - value % 4
589 def AddAuthorizedKey(file_name, key):
590 """Adds an SSH public key to an authorized_keys file.
593 file_name: Path to authorized_keys file
594 key: String containing key
596 key_fields = key.split()
598 f = open(file_name, 'a+')
602 # Ignore whitespace changes
603 if line.split() == key_fields:
605 nl = line.endswith('\n')
609 f.write(key.rstrip('\r\n'))
616 def RemoveAuthorizedKey(file_name, key):
617 """Removes an SSH public key from an authorized_keys file.
620 file_name: Path to authorized_keys file
621 key: String containing key
623 key_fields = key.split()
625 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
627 out = os.fdopen(fd, 'w')
629 f = open(file_name, 'r')
632 # Ignore whitespace changes while comparing lines
633 if line.split() != key_fields:
637 os.rename(tmpname, file_name)
647 def SetEtcHostsEntry(file_name, ip, hostname, aliases):
648 """Sets the name of an IP address and hostname in /etc/hosts.
651 # Ensure aliases are unique
652 aliases = UniqueSequence([hostname] + aliases)[1:]
654 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
656 out = os.fdopen(fd, 'w')
658 f = open(file_name, 'r')
662 fields = line.split()
663 if fields and not fields[0].startswith('#') and ip == fields[0]:
667 out.write("%s\t%s" % (ip, hostname))
669 out.write(" %s" % ' '.join(aliases))
674 os.rename(tmpname, file_name)
684 def AddHostToEtcHosts(hostname):
685 """Wrapper around SetEtcHostsEntry.
688 hi = HostInfo(name=hostname)
689 SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
692 def RemoveEtcHostsEntry(file_name, hostname):
693 """Removes a hostname from /etc/hosts.
695 IP addresses without names are removed from the file.
697 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
699 out = os.fdopen(fd, 'w')
701 f = open(file_name, 'r')
704 fields = line.split()
705 if len(fields) > 1 and not fields[0].startswith('#'):
707 if hostname in names:
708 while hostname in names:
709 names.remove(hostname)
711 out.write("%s %s\n" % (fields[0], ' '.join(names)))
718 os.rename(tmpname, file_name)
728 def RemoveHostFromEtcHosts(hostname):
729 """Wrapper around RemoveEtcHostsEntry.
732 hi = HostInfo(name=hostname)
733 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
734 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
737 def CreateBackup(file_name):
738 """Creates a backup of a file.
740 Returns: the path to the newly created backup file.
743 if not os.path.isfile(file_name):
744 raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
747 prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
748 dir_name = os.path.dirname(file_name)
750 fsrc = open(file_name, 'rb')
752 (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
753 fdst = os.fdopen(fd, 'wb')
755 shutil.copyfileobj(fsrc, fdst)
764 def ShellQuote(value):
765 """Quotes shell argument according to POSIX.
768 if _re_shell_unquoted.match(value):
771 return "'%s'" % value.replace("'", "'\\''")
774 def ShellQuoteArgs(args):
775 """Quotes all given shell arguments and concatenates using spaces.
778 return ' '.join([ShellQuote(i) for i in args])
781 def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
782 """Simple ping implementation using TCP connect(2).
784 Try to do a TCP connect(2) from an optional source IP to the
785 specified target IP and the specified target port. If the optional
786 parameter live_port_needed is set to true, requires the remote end
787 to accept the connection. The timeout is specified in seconds and
788 defaults to 10 seconds. If the source optional argument is not
789 passed, the source address selection is left to the kernel,
790 otherwise we try to connect using the passed address (failures to
791 bind other than EADDRNOTAVAIL will be ignored).
794 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
798 if source is not None:
800 sock.bind((source, 0))
801 except socket.error, (errcode, errstring):
802 if errcode == errno.EADDRNOTAVAIL:
805 sock.settimeout(timeout)
808 sock.connect((target, port))
811 except socket.timeout:
813 except socket.error, (errcode, errstring):
814 success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
819 def ListVisibleFiles(path):
820 """Returns a list of all visible files in a directory.
823 files = [i for i in os.listdir(path) if not i.startswith(".")]
828 def GetHomeDir(user, default=None):
829 """Try to get the homedir of the given user.
831 The user can be passed either as a string (denoting the name) or as
832 an integer (denoting the user id). If the user is not found, the
833 'default' argument is returned, which defaults to None.
837 if isinstance(user, basestring):
838 result = pwd.getpwnam(user)
839 elif isinstance(user, (int, long)):
840 result = pwd.getpwuid(user)
842 raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
850 """Returns a random UUID.
853 f = open("/proc/sys/kernel/random/uuid", "r")
855 return f.read(128).rstrip("\n")
860 def WriteFile(file_name, fn=None, data=None,
861 mode=None, uid=-1, gid=-1,
862 atime=None, mtime=None, close=True,
863 dry_run=False, backup=False,
864 prewrite=None, postwrite=None):
865 """(Over)write a file atomically.
867 The file_name and either fn (a function taking one argument, the
868 file descriptor, and which should write the data to it) or data (the
869 contents of the file) must be passed. The other arguments are
870 optional and allow setting the file mode, owner and group, and the
871 mtime/atime of the file.
873 If the function doesn't raise an exception, it has succeeded and the
874 target file has the new contents. If the file has raised an
875 exception, an existing target file should be unmodified and the
876 temporary file should be removed.
879 file_name: New filename
880 fn: Content writing function, called with file descriptor as parameter
881 data: Content as string
886 mtime: Modification time
887 close: Whether to close file after writing it
888 prewrite: Function object called before writing content
889 postwrite: Function object called after writing content
892 None if "close" parameter evaluates to True, otherwise file descriptor.
895 if not os.path.isabs(file_name):
896 raise errors.ProgrammerError("Path passed to WriteFile is not"
897 " absolute: '%s'" % file_name)
899 if [fn, data].count(None) != 1:
900 raise errors.ProgrammerError("fn or data required")
902 if [atime, mtime].count(None) == 1:
903 raise errors.ProgrammerError("Both atime and mtime must be either"
906 if backup and not dry_run and os.path.isfile(file_name):
907 CreateBackup(file_name)
909 dir_name, base_name = os.path.split(file_name)
910 fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
911 # here we need to make sure we remove the temp file, if any error
914 if uid != -1 or gid != -1:
915 os.chown(new_name, uid, gid)
917 os.chmod(new_name, mode)
918 if callable(prewrite):
924 if callable(postwrite):
927 if atime is not None and mtime is not None:
928 os.utime(new_name, (atime, mtime))
930 os.rename(new_name, file_name)
942 def FirstFree(seq, base=0):
943 """Returns the first non-existing integer from seq.
945 The seq argument should be a sorted list of positive integers. The
946 first time the index of an element is smaller than the element
947 value, the index will be returned.
949 The base argument is used to start at a different offset,
950 i.e. [3, 4, 6] with offset=3 will return 5.
952 Example: [0, 1, 3] will return 2.
955 for idx, elem in enumerate(seq):
956 assert elem >= base, "Passed element is higher than base offset"
957 if elem > idx + base:
963 def all(seq, pred=bool):
964 "Returns True if pred(x) is True for every element in the iterable"
965 for elem in itertools.ifilterfalse(pred, seq):
970 def any(seq, pred=bool):
971 "Returns True if pred(x) is True for at least one element in the iterable"
972 for elem in itertools.ifilter(pred, seq):
977 def UniqueSequence(seq):
978 """Returns a list with unique elements.
980 Element order is preserved.
983 return [i for i in seq if i not in seen and not seen.add(i)]
987 """Predicate to check if a MAC address is valid.
989 Checks wether the supplied MAC address is formally correct, only
990 accepts colon separated format.
992 mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
993 return mac_check.match(mac) is not None
996 def TestDelay(duration):
997 """Sleep for a fixed amount of time.
1002 time.sleep(duration)
1006 def Daemonize(logfile, noclose_fds=None):
1007 """Daemonize the current process.
1009 This detaches the current process from the controlling terminal and
1010 runs it in the background as a daemon.
1015 # Default maximum for the number of available file descriptors.
1016 if 'SC_OPEN_MAX' in os.sysconf_names:
1018 MAXFD = os.sysconf('SC_OPEN_MAX')
1028 if (pid == 0): # The first child.
1031 pid = os.fork() # Fork a second child.
1032 if (pid == 0): # The second child.
1036 # exit() or _exit()? See below.
1037 os._exit(0) # Exit parent (the first child) of the second child.
1039 os._exit(0) # Exit parent of the first child.
1040 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1041 if (maxfd == resource.RLIM_INFINITY):
1044 # Iterate through and close all file descriptors.
1045 for fd in range(0, maxfd):
1046 if noclose_fds and fd in noclose_fds:
1050 except OSError: # ERROR, fd wasn't open to begin with (ignored)
1052 os.open(logfile, os.O_RDWR|os.O_CREAT|os.O_APPEND, 0600)
1053 # Duplicate standard input to standard output and standard error.
1054 os.dup2(0, 1) # standard output (1)
1055 os.dup2(0, 2) # standard error (2)
1059 def DaemonPidFileName(name):
1060 """Compute a ganeti pid file absolute path, given the daemon name.
1063 return os.path.join(constants.RUN_GANETI_DIR, "%s.pid" % name)
1066 def WritePidFile(name):
1067 """Write the current process pidfile.
1069 The file will be written to constants.RUN_GANETI_DIR/name.pid
1073 pidfilename = DaemonPidFileName(name)
1074 if IsProcessAlive(ReadPidFile(pidfilename)):
1075 raise errors.GenericError("%s contains a live process" % pidfilename)
1077 WriteFile(pidfilename, data="%d\n" % pid)
1080 def RemovePidFile(name):
1081 """Remove the current process pidfile.
1083 Any errors are ignored.
1087 pidfilename = DaemonPidFileName(name)
1088 # TODO: we could check here that the file contains our pid
1090 RemoveFile(pidfilename)
1095 def KillProcess(pid, signal_=signal.SIGTERM, timeout=30):
1096 """Kill a process given by its pid.
1099 @param pid: The PID to terminate.
1101 @param signal_: The signal to send, by default SIGTERM
1103 @param timeout: The timeout after which, if the process is still alive,
1104 a SIGKILL will be sent. If not positive, no such checking
1109 # kill with pid=0 == suicide
1110 raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1112 if not IsProcessAlive(pid):
1114 os.kill(pid, signal_)
1117 end = time.time() + timeout
1118 while time.time() < end and IsProcessAlive(pid):
1120 if IsProcessAlive(pid):
1121 os.kill(pid, signal.SIGKILL)
1124 def FindFile(name, search_path, test=os.path.exists):
1125 """Look for a filesystem object in a given path.
1127 This is an abstract method to search for filesystem object (files,
1128 dirs) under a given search path.
1131 - name: the name to look for
1132 - search_path: list of directory names
1133 - test: the test which the full path must satisfy
1134 (defaults to os.path.exists)
1137 - full path to the item if found
1141 for dir_name in search_path:
1142 item_name = os.path.sep.join([dir_name, name])
1148 def CheckVolumeGroupSize(vglist, vgname, minsize):
1149 """Checks if the volume group list is valid.
1151 A non-None return value means there's an error, and the return value
1152 is the error message.
1155 vgsize = vglist.get(vgname, None)
1157 return "volume group '%s' missing" % vgname
1158 elif vgsize < minsize:
1159 return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1160 (vgname, minsize, vgsize))
1164 def LockedMethod(fn):
1165 """Synchronized object access decorator.
1167 This decorator is intended to protect access to an object using the
1168 object's own lock which is hardcoded to '_lock'.
1171 def wrapper(self, *args, **kwargs):
1172 assert hasattr(self, '_lock')
1176 result = fn(self, *args, **kwargs)
1184 """Locks a file using POSIX locks.
1188 fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1189 except IOError, err:
1190 if err.errno == errno.EAGAIN:
1191 raise errors.LockError("File already locked")
1195 class SignalHandler(object):
1196 """Generic signal handler class.
1198 It automatically restores the original handler when deconstructed or when
1199 Reset() is called. You can either pass your own handler function in or query
1200 the "called" attribute to detect whether the signal was sent.
1203 def __init__(self, signum):
1204 """Constructs a new SignalHandler instance.
1206 @param signum: Single signal number or set of signal numbers
1209 if isinstance(signum, (int, long)):
1210 self.signum = set([signum])
1212 self.signum = set(signum)
1218 for signum in self.signum:
1220 prev_handler = signal.signal(signum, self._HandleSignal)
1222 self._previous[signum] = prev_handler
1224 # Restore previous handler
1225 signal.signal(signum, prev_handler)
1228 # Reset all handlers
1230 # Here we have a race condition: a handler may have already been called,
1231 # but there's not much we can do about it at this point.
1238 """Restore previous handler.
1241 for signum, prev_handler in self._previous.items():
1242 signal.signal(signum, prev_handler)
1243 # If successful, remove from dict
1244 del self._previous[signum]
1247 """Unsets "called" flag.
1249 This function can be used in case a signal may arrive several times.
1254 def _HandleSignal(self, signum, frame):
1255 """Actual signal handling function.
1258 # This is not nice and not absolutely atomic, but it appears to be the only
1259 # solution in Python -- there are no atomic types.