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 utility module.
24 This module holds functions that can be used in both daemons (all) and
25 the command line scripts.
47 from cStringIO import StringIO
50 from hashlib import sha1
55 from ganeti import errors
56 from ganeti import constants
60 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
64 #: when set to True, L{RunCmd} is disabled
67 _RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
70 class RunResult(object):
71 """Holds the result of running external programs.
74 @ivar exit_code: the exit code of the program, or None (if the program
76 @type signal: int or None
77 @ivar signal: the signal that caused the program to finish, or None
78 (if the program wasn't terminated by a signal)
80 @ivar stdout: the standard output of the program
82 @ivar stderr: the standard error of the program
84 @ivar failed: True in case the program was
85 terminated by a signal or exited with a non-zero exit code
86 @ivar fail_reason: a string detailing the termination reason
89 __slots__ = ["exit_code", "signal", "stdout", "stderr",
90 "failed", "fail_reason", "cmd"]
93 def __init__(self, exit_code, signal_, stdout, stderr, cmd):
95 self.exit_code = exit_code
99 self.failed = (signal_ is not None or exit_code != 0)
101 if self.signal is not None:
102 self.fail_reason = "terminated by signal %s" % self.signal
103 elif self.exit_code is not None:
104 self.fail_reason = "exited with exit code %s" % self.exit_code
106 self.fail_reason = "unable to determine termination reason"
109 logging.debug("Command '%s' failed (%s); output: %s",
110 self.cmd, self.fail_reason, self.output)
112 def _GetOutput(self):
113 """Returns the combined stdout and stderr for easier usage.
116 return self.stdout + self.stderr
118 output = property(_GetOutput, None, None, "Return full output")
121 def RunCmd(cmd, env=None, output=None, cwd='/'):
122 """Execute a (shell) command.
124 The command should not read from its standard input, as it will be
127 @type cmd: string or list
128 @param cmd: Command to run
130 @param env: Additional environment
132 @param output: if desired, the output of the command can be
133 saved in a file instead of the RunResult instance; this
134 parameter denotes the file name (if not None)
136 @param cwd: if specified, will be used as the working
137 directory for the command; the default will be /
139 @return: RunResult instance
140 @raise errors.ProgrammerError: if we call this when forks are disabled
144 raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
146 if isinstance(cmd, list):
147 cmd = [str(val) for val in cmd]
148 strcmd = " ".join(cmd)
153 logging.debug("RunCmd '%s'", strcmd)
155 cmd_env = os.environ.copy()
156 cmd_env["LC_ALL"] = "C"
162 out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd)
164 status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
167 if err.errno == errno.ENOENT:
168 raise errors.OpExecError("Can't execute '%s': not found (%s)" %
180 return RunResult(exitcode, signal_, out, err, strcmd)
183 def _RunCmdPipe(cmd, env, via_shell, cwd):
184 """Run a command and return its output.
186 @type cmd: string or list
187 @param cmd: Command to run
189 @param env: The environment to use
190 @type via_shell: bool
191 @param via_shell: if we should run via the shell
193 @param cwd: the working directory for the program
195 @return: (out, err, status)
198 poller = select.poll()
199 child = subprocess.Popen(cmd, shell=via_shell,
200 stderr=subprocess.PIPE,
201 stdout=subprocess.PIPE,
202 stdin=subprocess.PIPE,
203 close_fds=True, env=env,
207 poller.register(child.stdout, select.POLLIN)
208 poller.register(child.stderr, select.POLLIN)
212 child.stdout.fileno(): (out, child.stdout),
213 child.stderr.fileno(): (err, child.stderr),
216 status = fcntl.fcntl(fd, fcntl.F_GETFL)
217 fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
221 pollresult = poller.poll()
222 except EnvironmentError, eerr:
223 if eerr.errno == errno.EINTR:
226 except select.error, serr:
227 if serr[0] == errno.EINTR:
231 for fd, event in pollresult:
232 if event & select.POLLIN or event & select.POLLPRI:
233 data = fdmap[fd][1].read()
234 # no data from read signifies EOF (the same as POLLHUP)
236 poller.unregister(fd)
239 fdmap[fd][0].write(data)
240 if (event & select.POLLNVAL or event & select.POLLHUP or
241 event & select.POLLERR):
242 poller.unregister(fd)
248 status = child.wait()
249 return out, err, status
252 def _RunCmdFile(cmd, env, via_shell, output, cwd):
253 """Run a command and save its output to a file.
255 @type cmd: string or list
256 @param cmd: Command to run
258 @param env: The environment to use
259 @type via_shell: bool
260 @param via_shell: if we should run via the shell
262 @param output: the filename in which to save the output
264 @param cwd: the working directory for the program
266 @return: the exit status
269 fh = open(output, "a")
271 child = subprocess.Popen(cmd, shell=via_shell,
272 stderr=subprocess.STDOUT,
274 stdin=subprocess.PIPE,
275 close_fds=True, env=env,
279 status = child.wait()
285 def RemoveFile(filename):
286 """Remove a file ignoring some errors.
288 Remove a file, ignoring non-existing ones or directories. Other
292 @param filename: the file to be removed
298 if err.errno not in (errno.ENOENT, errno.EISDIR):
302 def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
306 @param old: Original path
310 @param mkdir: Whether to create target directory if it doesn't exist
311 @type mkdir_mode: int
312 @param mkdir_mode: Mode for newly created directories
316 return os.rename(old, new)
318 # In at least one use case of this function, the job queue, directory
319 # creation is very rare. Checking for the directory before renaming is not
321 if mkdir and err.errno == errno.ENOENT:
322 # Create directory and try again
323 os.makedirs(os.path.dirname(new), mkdir_mode)
324 return os.rename(old, new)
328 def _FingerprintFile(filename):
329 """Compute the fingerprint of a file.
331 If the file does not exist, a None will be returned
335 @param filename: the filename to checksum
337 @return: the hex digest of the sha checksum of the contents
341 if not (os.path.exists(filename) and os.path.isfile(filename)):
354 return fp.hexdigest()
357 def FingerprintFiles(files):
358 """Compute fingerprints for a list of files.
361 @param files: the list of filename to fingerprint
363 @return: a dictionary filename: fingerprint, holding only
369 for filename in files:
370 cksum = _FingerprintFile(filename)
372 ret[filename] = cksum
377 def ForceDictType(target, key_types, allowed_values=None):
378 """Force the values of a dict to have certain types.
381 @param target: the dict to update
382 @type key_types: dict
383 @param key_types: dict mapping target dict keys to types
384 in constants.ENFORCEABLE_TYPES
385 @type allowed_values: list
386 @keyword allowed_values: list of specially allowed values
389 if allowed_values is None:
392 if not isinstance(target, dict):
393 msg = "Expected dictionary, got '%s'" % target
394 raise errors.TypeEnforcementError(msg)
397 if key not in key_types:
398 msg = "Unknown key '%s'" % key
399 raise errors.TypeEnforcementError(msg)
401 if target[key] in allowed_values:
404 ktype = key_types[key]
405 if ktype not in constants.ENFORCEABLE_TYPES:
406 msg = "'%s' has non-enforceable type %s" % (key, ktype)
407 raise errors.ProgrammerError(msg)
409 if ktype == constants.VTYPE_STRING:
410 if not isinstance(target[key], basestring):
411 if isinstance(target[key], bool) and not target[key]:
414 msg = "'%s' (value %s) is not a valid string" % (key, target[key])
415 raise errors.TypeEnforcementError(msg)
416 elif ktype == constants.VTYPE_BOOL:
417 if isinstance(target[key], basestring) and target[key]:
418 if target[key].lower() == constants.VALUE_FALSE:
420 elif target[key].lower() == constants.VALUE_TRUE:
423 msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
424 raise errors.TypeEnforcementError(msg)
429 elif ktype == constants.VTYPE_SIZE:
431 target[key] = ParseUnit(target[key])
432 except errors.UnitParseError, err:
433 msg = "'%s' (value %s) is not a valid size. error: %s" % \
434 (key, target[key], err)
435 raise errors.TypeEnforcementError(msg)
436 elif ktype == constants.VTYPE_INT:
438 target[key] = int(target[key])
439 except (ValueError, TypeError):
440 msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
441 raise errors.TypeEnforcementError(msg)
444 def IsProcessAlive(pid):
445 """Check if a given pid exists on the system.
447 @note: zombie status is not handled, so zombie processes
448 will be returned as alive
450 @param pid: the process ID to check
452 @return: True if the process exists
459 os.stat("/proc/%d/status" % pid)
461 except EnvironmentError, err:
462 if err.errno in (errno.ENOENT, errno.ENOTDIR):
467 def ReadPidFile(pidfile):
468 """Read a pid from a file.
470 @type pidfile: string
471 @param pidfile: path to the file containing the pid
473 @return: The process id, if the file exists and contains a valid PID,
478 raw_data = ReadFile(pidfile)
479 except EnvironmentError, err:
480 if err.errno != errno.ENOENT:
481 logging.exception("Can't read pid file")
486 except ValueError, err:
487 logging.info("Can't parse pid file contents", exc_info=True)
493 def MatchNameComponent(key, name_list, case_sensitive=True):
494 """Try to match a name against a list.
496 This function will try to match a name like test1 against a list
497 like C{['test1.example.com', 'test2.example.com', ...]}. Against
498 this list, I{'test1'} as well as I{'test1.example'} will match, but
499 not I{'test1.ex'}. A multiple match will be considered as no match
500 at all (e.g. I{'test1'} against C{['test1.example.com',
501 'test1.example.org']}), except when the key fully matches an entry
502 (e.g. I{'test1'} against C{['test1', 'test1.example.com']}).
505 @param key: the name to be searched
506 @type name_list: list
507 @param name_list: the list of strings against which to search the key
508 @type case_sensitive: boolean
509 @param case_sensitive: whether to provide a case-sensitive match
512 @return: None if there is no match I{or} if there are multiple matches,
513 otherwise the element from the list which matches
520 if not case_sensitive:
521 re_flags |= re.IGNORECASE
522 key = string.upper(key)
523 mo = re.compile("^%s(\..*)?$" % re.escape(key), re_flags)
526 for name in name_list:
527 if mo.match(name) is not None:
528 names_filtered.append(name)
529 if not case_sensitive and key == string.upper(name):
530 string_matches.append(name)
532 if len(string_matches) == 1:
533 return string_matches[0]
534 if len(names_filtered) == 1:
535 return names_filtered[0]
540 """Class implementing resolver and hostname functionality
543 def __init__(self, name=None):
544 """Initialize the host name object.
546 If the name argument is not passed, it will use this system's
551 name = self.SysName()
554 self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
555 self.ip = self.ipaddrs[0]
558 """Returns the hostname without domain.
561 return self.name.split('.')[0]
565 """Return the current system's name.
567 This is simply a wrapper over C{socket.gethostname()}.
570 return socket.gethostname()
573 def LookupHostname(hostname):
577 @param hostname: hostname to look up
580 @return: a tuple (name, aliases, ipaddrs) as returned by
581 C{socket.gethostbyname_ex}
582 @raise errors.ResolverError: in case of errors in resolving
586 result = socket.gethostbyname_ex(hostname)
587 except socket.gaierror, err:
588 # hostname not found in DNS
589 raise errors.ResolverError(hostname, err.args[0], err.args[1])
594 def ListVolumeGroups():
595 """List volume groups and their size
599 Dictionary with keys volume name and values
600 the size of the volume
603 command = "vgs --noheadings --units m --nosuffix -o name,size"
604 result = RunCmd(command)
609 for line in result.stdout.splitlines():
611 name, size = line.split()
612 size = int(float(size))
613 except (IndexError, ValueError), err:
614 logging.error("Invalid output from vgs (%s): %s", err, line)
622 def BridgeExists(bridge):
623 """Check whether the given bridge exists in the system
626 @param bridge: the bridge name to check
628 @return: True if it does
631 return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
634 def NiceSort(name_list):
635 """Sort a list of strings based on digit and non-digit groupings.
637 Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
638 will sort the list in the logical order C{['a1', 'a2', 'a10',
641 The sort algorithm breaks each name in groups of either only-digits
642 or no-digits. Only the first eight such groups are considered, and
643 after that we just use what's left of the string.
645 @type name_list: list
646 @param name_list: the names to be sorted
648 @return: a copy of the name list sorted with our algorithm
651 _SORTER_BASE = "(\D+|\d+)"
652 _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
653 _SORTER_BASE, _SORTER_BASE,
654 _SORTER_BASE, _SORTER_BASE,
655 _SORTER_BASE, _SORTER_BASE)
656 _SORTER_RE = re.compile(_SORTER_FULL)
657 _SORTER_NODIGIT = re.compile("^\D*$")
659 """Attempts to convert a variable to integer."""
660 if val is None or _SORTER_NODIGIT.match(val):
665 to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
666 for name in name_list]
668 return [tup[1] for tup in to_sort]
671 def TryConvert(fn, val):
672 """Try to convert a value ignoring errors.
674 This function tries to apply function I{fn} to I{val}. If no
675 C{ValueError} or C{TypeError} exceptions are raised, it will return
676 the result, else it will return the original value. Any other
677 exceptions are propagated to the caller.
680 @param fn: function to apply to the value
681 @param val: the value to be converted
682 @return: The converted value if the conversion was successful,
683 otherwise the original value.
688 except (ValueError, TypeError):
694 """Verifies the syntax of an IPv4 address.
696 This function checks if the IPv4 address passes is valid or not based
697 on syntax (not IP range, class calculations, etc.).
700 @param ip: the address to be checked
701 @rtype: a regular expression match object
702 @return: a regular expression match object, or None if the
706 unit = "(0|[1-9]\d{0,2})"
707 #TODO: convert and return only boolean
708 return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
711 def IsValidShellParam(word):
712 """Verifies is the given word is safe from the shell's p.o.v.
714 This means that we can pass this to a command via the shell and be
715 sure that it doesn't alter the command line and is passed as such to
718 Note that we are overly restrictive here, in order to be on the safe
722 @param word: the word to check
724 @return: True if the word is 'safe'
727 return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
730 def BuildShellCmd(template, *args):
731 """Build a safe shell command line from the given arguments.
733 This function will check all arguments in the args list so that they
734 are valid shell parameters (i.e. they don't contain shell
735 metacharacters). If everything is ok, it will return the result of
739 @param template: the string holding the template for the
742 @return: the expanded command line
746 if not IsValidShellParam(word):
747 raise errors.ProgrammerError("Shell argument '%s' contains"
748 " invalid characters" % word)
749 return template % args
752 def FormatUnit(value, units):
753 """Formats an incoming number of MiB with the appropriate unit.
756 @param value: integer representing the value in MiB (1048576)
758 @param units: the type of formatting we should do:
759 - 'h' for automatic scaling
764 @return: the formatted value (with suffix)
767 if units not in ('m', 'g', 't', 'h'):
768 raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
772 if units == 'm' or (units == 'h' and value < 1024):
775 return "%d%s" % (round(value, 0), suffix)
777 elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
780 return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
785 return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
788 def ParseUnit(input_string):
789 """Tries to extract number and scale from the given string.
791 Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
792 [UNIT]}. If no unit is specified, it defaults to MiB. Return value
793 is always an int in MiB.
796 m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', str(input_string))
798 raise errors.UnitParseError("Invalid format")
800 value = float(m.groups()[0])
804 lcunit = unit.lower()
808 if lcunit in ('m', 'mb', 'mib'):
809 # Value already in MiB
812 elif lcunit in ('g', 'gb', 'gib'):
815 elif lcunit in ('t', 'tb', 'tib'):
819 raise errors.UnitParseError("Unknown unit: %s" % unit)
821 # Make sure we round up
822 if int(value) < value:
825 # Round up to the next multiple of 4
828 value += 4 - value % 4
833 def AddAuthorizedKey(file_name, key):
834 """Adds an SSH public key to an authorized_keys file.
837 @param file_name: path to authorized_keys file
839 @param key: string containing key
842 key_fields = key.split()
844 f = open(file_name, 'a+')
848 # Ignore whitespace changes
849 if line.split() == key_fields:
851 nl = line.endswith('\n')
855 f.write(key.rstrip('\r\n'))
862 def RemoveAuthorizedKey(file_name, key):
863 """Removes an SSH public key from an authorized_keys file.
866 @param file_name: path to authorized_keys file
868 @param key: string containing key
871 key_fields = key.split()
873 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
875 out = os.fdopen(fd, 'w')
877 f = open(file_name, 'r')
880 # Ignore whitespace changes while comparing lines
881 if line.split() != key_fields:
885 os.rename(tmpname, file_name)
895 def SetEtcHostsEntry(file_name, ip, hostname, aliases):
896 """Sets the name of an IP address and hostname in /etc/hosts.
899 @param file_name: path to the file to modify (usually C{/etc/hosts})
901 @param ip: the IP address
903 @param hostname: the hostname to be added
905 @param aliases: the list of aliases to add for the hostname
908 # FIXME: use WriteFile + fn rather than duplicating its efforts
909 # Ensure aliases are unique
910 aliases = UniqueSequence([hostname] + aliases)[1:]
912 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
914 out = os.fdopen(fd, 'w')
916 f = open(file_name, 'r')
919 fields = line.split()
920 if fields and not fields[0].startswith('#') and ip == fields[0]:
924 out.write("%s\t%s" % (ip, hostname))
926 out.write(" %s" % ' '.join(aliases))
931 os.chmod(tmpname, 0644)
932 os.rename(tmpname, file_name)
942 def AddHostToEtcHosts(hostname):
943 """Wrapper around SetEtcHostsEntry.
946 @param hostname: a hostname that will be resolved and added to
947 L{constants.ETC_HOSTS}
950 hi = HostInfo(name=hostname)
951 SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
954 def RemoveEtcHostsEntry(file_name, hostname):
955 """Removes a hostname from /etc/hosts.
957 IP addresses without names are removed from the file.
960 @param file_name: path to the file to modify (usually C{/etc/hosts})
962 @param hostname: the hostname to be removed
965 # FIXME: use WriteFile + fn rather than duplicating its efforts
966 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
968 out = os.fdopen(fd, 'w')
970 f = open(file_name, 'r')
973 fields = line.split()
974 if len(fields) > 1 and not fields[0].startswith('#'):
976 if hostname in names:
977 while hostname in names:
978 names.remove(hostname)
980 out.write("%s %s\n" % (fields[0], ' '.join(names)))
987 os.chmod(tmpname, 0644)
988 os.rename(tmpname, file_name)
998 def RemoveHostFromEtcHosts(hostname):
999 """Wrapper around RemoveEtcHostsEntry.
1002 @param hostname: hostname that will be resolved and its
1003 full and shot name will be removed from
1004 L{constants.ETC_HOSTS}
1007 hi = HostInfo(name=hostname)
1008 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
1009 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
1012 def CreateBackup(file_name):
1013 """Creates a backup of a file.
1015 @type file_name: str
1016 @param file_name: file to be backed up
1018 @return: the path to the newly created backup
1019 @raise errors.ProgrammerError: for invalid file names
1022 if not os.path.isfile(file_name):
1023 raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
1026 prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
1027 dir_name = os.path.dirname(file_name)
1029 fsrc = open(file_name, 'rb')
1031 (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
1032 fdst = os.fdopen(fd, 'wb')
1034 shutil.copyfileobj(fsrc, fdst)
1043 def ShellQuote(value):
1044 """Quotes shell argument according to POSIX.
1047 @param value: the argument to be quoted
1049 @return: the quoted value
1052 if _re_shell_unquoted.match(value):
1055 return "'%s'" % value.replace("'", "'\\''")
1058 def ShellQuoteArgs(args):
1059 """Quotes a list of shell arguments.
1062 @param args: list of arguments to be quoted
1064 @return: the quoted arguments concatenated with spaces
1067 return ' '.join([ShellQuote(i) for i in args])
1070 def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
1071 """Simple ping implementation using TCP connect(2).
1073 Check if the given IP is reachable by doing attempting a TCP connect
1077 @param target: the IP or hostname to ping
1079 @param port: the port to connect to
1081 @param timeout: the timeout on the connection attempt
1082 @type live_port_needed: boolean
1083 @param live_port_needed: whether a closed port will cause the
1084 function to return failure, as if there was a timeout
1085 @type source: str or None
1086 @param source: if specified, will cause the connect to be made
1087 from this specific source address; failures to bind other
1088 than C{EADDRNOTAVAIL} will be ignored
1091 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1095 if source is not None:
1097 sock.bind((source, 0))
1098 except socket.error, (errcode, _):
1099 if errcode == errno.EADDRNOTAVAIL:
1102 sock.settimeout(timeout)
1105 sock.connect((target, port))
1108 except socket.timeout:
1110 except socket.error, (errcode, errstring):
1111 success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
1116 def OwnIpAddress(address):
1117 """Check if the current host has the the given IP address.
1119 Currently this is done by TCP-pinging the address from the loopback
1122 @type address: string
1123 @param address: the address to check
1125 @return: True if we own the address
1128 return TcpPing(address, constants.DEFAULT_NODED_PORT,
1129 source=constants.LOCALHOST_IP_ADDRESS)
1132 def ListVisibleFiles(path):
1133 """Returns a list of visible files in a directory.
1136 @param path: the directory to enumerate
1138 @return: the list of all files not starting with a dot
1141 files = [i for i in os.listdir(path) if not i.startswith(".")]
1146 def GetHomeDir(user, default=None):
1147 """Try to get the homedir of the given user.
1149 The user can be passed either as a string (denoting the name) or as
1150 an integer (denoting the user id). If the user is not found, the
1151 'default' argument is returned, which defaults to None.
1155 if isinstance(user, basestring):
1156 result = pwd.getpwnam(user)
1157 elif isinstance(user, (int, long)):
1158 result = pwd.getpwuid(user)
1160 raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
1164 return result.pw_dir
1168 """Returns a random UUID.
1170 @note: This is a Linux-specific method as it uses the /proc
1175 return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
1178 def GenerateSecret(numbytes=20):
1179 """Generates a random secret.
1181 This will generate a pseudo-random secret returning an hex string
1182 (so that it can be used where an ASCII string is needed).
1184 @param numbytes: the number of bytes which will be represented by the returned
1185 string (defaulting to 20, the length of a SHA1 hash)
1187 @return: an hex representation of the pseudo-random sequence
1190 return os.urandom(numbytes).encode('hex')
1193 def EnsureDirs(dirs):
1194 """Make required directories, if they don't exist.
1196 @param dirs: list of tuples (dir_name, dir_mode)
1197 @type dirs: list of (string, integer)
1200 for dir_name, dir_mode in dirs:
1202 os.mkdir(dir_name, dir_mode)
1203 except EnvironmentError, err:
1204 if err.errno != errno.EEXIST:
1205 raise errors.GenericError("Cannot create needed directory"
1206 " '%s': %s" % (dir_name, err))
1207 if not os.path.isdir(dir_name):
1208 raise errors.GenericError("%s is not a directory" % dir_name)
1211 def ReadFile(file_name, size=None):
1214 @type size: None or int
1215 @param size: Read at most size bytes
1217 @return: the (possibly partial) content of the file
1220 f = open(file_name, "r")
1230 def WriteFile(file_name, fn=None, data=None,
1231 mode=None, uid=-1, gid=-1,
1232 atime=None, mtime=None, close=True,
1233 dry_run=False, backup=False,
1234 prewrite=None, postwrite=None):
1235 """(Over)write a file atomically.
1237 The file_name and either fn (a function taking one argument, the
1238 file descriptor, and which should write the data to it) or data (the
1239 contents of the file) must be passed. The other arguments are
1240 optional and allow setting the file mode, owner and group, and the
1241 mtime/atime of the file.
1243 If the function doesn't raise an exception, it has succeeded and the
1244 target file has the new contents. If the function has raised an
1245 exception, an existing target file should be unmodified and the
1246 temporary file should be removed.
1248 @type file_name: str
1249 @param file_name: the target filename
1251 @param fn: content writing function, called with
1252 file descriptor as parameter
1254 @param data: contents of the file
1256 @param mode: file mode
1258 @param uid: the owner of the file
1260 @param gid: the group of the file
1262 @param atime: a custom access time to be set on the file
1264 @param mtime: a custom modification time to be set on the file
1265 @type close: boolean
1266 @param close: whether to close file after writing it
1267 @type prewrite: callable
1268 @param prewrite: function to be called before writing content
1269 @type postwrite: callable
1270 @param postwrite: function to be called after writing content
1273 @return: None if the 'close' parameter evaluates to True,
1274 otherwise the file descriptor
1276 @raise errors.ProgrammerError: if any of the arguments are not valid
1279 if not os.path.isabs(file_name):
1280 raise errors.ProgrammerError("Path passed to WriteFile is not"
1281 " absolute: '%s'" % file_name)
1283 if [fn, data].count(None) != 1:
1284 raise errors.ProgrammerError("fn or data required")
1286 if [atime, mtime].count(None) == 1:
1287 raise errors.ProgrammerError("Both atime and mtime must be either"
1290 if backup and not dry_run and os.path.isfile(file_name):
1291 CreateBackup(file_name)
1293 dir_name, base_name = os.path.split(file_name)
1294 fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1296 # here we need to make sure we remove the temp file, if any error
1297 # leaves it in place
1299 if uid != -1 or gid != -1:
1300 os.chown(new_name, uid, gid)
1302 os.chmod(new_name, mode)
1303 if callable(prewrite):
1305 if data is not None:
1309 if callable(postwrite):
1312 if atime is not None and mtime is not None:
1313 os.utime(new_name, (atime, mtime))
1315 os.rename(new_name, file_name)
1324 RemoveFile(new_name)
1329 def FirstFree(seq, base=0):
1330 """Returns the first non-existing integer from seq.
1332 The seq argument should be a sorted list of positive integers. The
1333 first time the index of an element is smaller than the element
1334 value, the index will be returned.
1336 The base argument is used to start at a different offset,
1337 i.e. C{[3, 4, 6]} with I{offset=3} will return 5.
1339 Example: C{[0, 1, 3]} will return I{2}.
1342 @param seq: the sequence to be analyzed.
1344 @param base: use this value as the base index of the sequence
1346 @return: the first non-used index in the sequence
1349 for idx, elem in enumerate(seq):
1350 assert elem >= base, "Passed element is higher than base offset"
1351 if elem > idx + base:
1357 def all(seq, pred=bool):
1358 "Returns True if pred(x) is True for every element in the iterable"
1359 for _ in itertools.ifilterfalse(pred, seq):
1364 def any(seq, pred=bool):
1365 "Returns True if pred(x) is True for at least one element in the iterable"
1366 for _ in itertools.ifilter(pred, seq):
1371 def UniqueSequence(seq):
1372 """Returns a list with unique elements.
1374 Element order is preserved.
1377 @param seq: the sequence with the source elements
1379 @return: list of unique elements from seq
1383 return [i for i in seq if i not in seen and not seen.add(i)]
1386 def IsValidMac(mac):
1387 """Predicate to check if a MAC address is valid.
1389 Checks whether the supplied MAC address is formally correct, only
1390 accepts colon separated format.
1393 @param mac: the MAC to be validated
1395 @return: True is the MAC seems valid
1398 mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
1399 return mac_check.match(mac) is not None
1402 def TestDelay(duration):
1403 """Sleep for a fixed amount of time.
1405 @type duration: float
1406 @param duration: the sleep duration
1408 @return: False for negative value, True otherwise
1412 return False, "Invalid sleep duration"
1413 time.sleep(duration)
1417 def _CloseFDNoErr(fd, retries=5):
1418 """Close a file descriptor ignoring errors.
1421 @param fd: the file descriptor
1423 @param retries: how many retries to make, in case we get any
1424 other error than EBADF
1429 except OSError, err:
1430 if err.errno != errno.EBADF:
1432 _CloseFDNoErr(fd, retries - 1)
1433 # else either it's closed already or we're out of retries, so we
1434 # ignore this and go on
1437 def CloseFDs(noclose_fds=None):
1438 """Close file descriptors.
1440 This closes all file descriptors above 2 (i.e. except
1443 @type noclose_fds: list or None
1444 @param noclose_fds: if given, it denotes a list of file descriptor
1445 that should not be closed
1448 # Default maximum for the number of available file descriptors.
1449 if 'SC_OPEN_MAX' in os.sysconf_names:
1451 MAXFD = os.sysconf('SC_OPEN_MAX')
1458 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1459 if (maxfd == resource.RLIM_INFINITY):
1462 # Iterate through and close all file descriptors (except the standard ones)
1463 for fd in range(3, maxfd):
1464 if noclose_fds and fd in noclose_fds:
1469 def Daemonize(logfile):
1470 """Daemonize the current process.
1472 This detaches the current process from the controlling terminal and
1473 runs it in the background as a daemon.
1476 @param logfile: the logfile to which we should redirect stdout/stderr
1478 @return: the value zero
1486 if (pid == 0): # The first child.
1489 pid = os.fork() # Fork a second child.
1490 if (pid == 0): # The second child.
1494 # exit() or _exit()? See below.
1495 os._exit(0) # Exit parent (the first child) of the second child.
1497 os._exit(0) # Exit parent of the first child.
1501 i = os.open("/dev/null", os.O_RDONLY) # stdin
1502 assert i == 0, "Can't close/reopen stdin"
1503 i = os.open(logfile, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0600) # stdout
1504 assert i == 1, "Can't close/reopen stdout"
1505 # Duplicate standard output to standard error.
1510 def DaemonPidFileName(name):
1511 """Compute a ganeti pid file absolute path
1514 @param name: the daemon name
1516 @return: the full path to the pidfile corresponding to the given
1520 return os.path.join(constants.RUN_GANETI_DIR, "%s.pid" % name)
1523 def WritePidFile(name):
1524 """Write the current process pidfile.
1526 The file will be written to L{constants.RUN_GANETI_DIR}I{/name.pid}
1529 @param name: the daemon name to use
1530 @raise errors.GenericError: if the pid file already exists and
1531 points to a live process
1535 pidfilename = DaemonPidFileName(name)
1536 if IsProcessAlive(ReadPidFile(pidfilename)):
1537 raise errors.GenericError("%s contains a live process" % pidfilename)
1539 WriteFile(pidfilename, data="%d\n" % pid)
1542 def RemovePidFile(name):
1543 """Remove the current process pidfile.
1545 Any errors are ignored.
1548 @param name: the daemon name used to derive the pidfile name
1551 pidfilename = DaemonPidFileName(name)
1552 # TODO: we could check here that the file contains our pid
1554 RemoveFile(pidfilename)
1559 def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1561 """Kill a process given by its pid.
1564 @param pid: The PID to terminate.
1566 @param signal_: The signal to send, by default SIGTERM
1568 @param timeout: The timeout after which, if the process is still alive,
1569 a SIGKILL will be sent. If not positive, no such checking
1571 @type waitpid: boolean
1572 @param waitpid: If true, we should waitpid on this process after
1573 sending signals, since it's our own child and otherwise it
1574 would remain as zombie
1577 def _helper(pid, signal_, wait):
1578 """Simple helper to encapsulate the kill/waitpid sequence"""
1579 os.kill(pid, signal_)
1582 os.waitpid(pid, os.WNOHANG)
1587 # kill with pid=0 == suicide
1588 raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1590 if not IsProcessAlive(pid):
1592 _helper(pid, signal_, waitpid)
1596 # Wait up to $timeout seconds
1597 end = time.time() + timeout
1599 while time.time() < end and IsProcessAlive(pid):
1601 (result_pid, _) = os.waitpid(pid, os.WNOHANG)
1607 # Make wait time longer for next try
1611 if IsProcessAlive(pid):
1612 # Kill process if it's still alive
1613 _helper(pid, signal.SIGKILL, waitpid)
1616 def FindFile(name, search_path, test=os.path.exists):
1617 """Look for a filesystem object in a given path.
1619 This is an abstract method to search for filesystem object (files,
1620 dirs) under a given search path.
1623 @param name: the name to look for
1624 @type search_path: str
1625 @param search_path: location to start at
1626 @type test: callable
1627 @param test: a function taking one argument that should return True
1628 if the a given object is valid; the default value is
1629 os.path.exists, causing only existing files to be returned
1631 @return: full path to the object if found, None otherwise
1634 for dir_name in search_path:
1635 item_name = os.path.sep.join([dir_name, name])
1641 def CheckVolumeGroupSize(vglist, vgname, minsize):
1642 """Checks if the volume group list is valid.
1644 The function will check if a given volume group is in the list of
1645 volume groups and has a minimum size.
1648 @param vglist: dictionary of volume group names and their size
1650 @param vgname: the volume group we should check
1652 @param minsize: the minimum size we accept
1654 @return: None for success, otherwise the error message
1657 vgsize = vglist.get(vgname, None)
1659 return "volume group '%s' missing" % vgname
1660 elif vgsize < minsize:
1661 return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1662 (vgname, minsize, vgsize))
1666 def SplitTime(value):
1667 """Splits time as floating point number into a tuple.
1669 @param value: Time in seconds
1670 @type value: int or float
1671 @return: Tuple containing (seconds, microseconds)
1674 (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
1676 assert 0 <= seconds, \
1677 "Seconds must be larger than or equal to 0, but are %s" % seconds
1678 assert 0 <= microseconds <= 999999, \
1679 "Microseconds must be 0-999999, but are %s" % microseconds
1681 return (int(seconds), int(microseconds))
1684 def MergeTime(timetuple):
1685 """Merges a tuple into time as a floating point number.
1687 @param timetuple: Time as tuple, (seconds, microseconds)
1688 @type timetuple: tuple
1689 @return: Time as a floating point number expressed in seconds
1692 (seconds, microseconds) = timetuple
1694 assert 0 <= seconds, \
1695 "Seconds must be larger than or equal to 0, but are %s" % seconds
1696 assert 0 <= microseconds <= 999999, \
1697 "Microseconds must be 0-999999, but are %s" % microseconds
1699 return float(seconds) + (float(microseconds) * 0.000001)
1702 def GetDaemonPort(daemon_name):
1703 """Get the daemon port for this cluster.
1705 Note that this routine does not read a ganeti-specific file, but
1706 instead uses C{socket.getservbyname} to allow pre-customization of
1707 this parameter outside of Ganeti.
1709 @type daemon_name: string
1710 @param daemon_name: daemon name (in constants.DAEMONS_PORTS)
1714 if daemon_name not in constants.DAEMONS_PORTS:
1715 raise errors.ProgrammerError("Unknown daemon: %s" % daemon_name)
1717 (proto, default_port) = constants.DAEMONS_PORTS[daemon_name]
1719 port = socket.getservbyname(daemon_name, proto)
1720 except socket.error:
1726 def SetupLogging(logfile, debug=False, stderr_logging=False, program="",
1727 multithreaded=False):
1728 """Configures the logging module.
1731 @param logfile: the filename to which we should log
1732 @type debug: boolean
1733 @param debug: whether to enable debug messages too or
1734 only those at C{INFO} and above level
1735 @type stderr_logging: boolean
1736 @param stderr_logging: whether we should also log to the standard error
1738 @param program: the name under which we should log messages
1739 @type multithreaded: boolean
1740 @param multithreaded: if True, will add the thread name to the log file
1741 @raise EnvironmentError: if we can't open the log file and
1742 stderr logging is disabled
1745 fmt = "%(asctime)s: " + program + " pid=%(process)d"
1747 fmt += "/%(threadName)s"
1749 fmt += " %(module)s:%(lineno)s"
1750 fmt += " %(levelname)s %(message)s"
1751 formatter = logging.Formatter(fmt)
1753 root_logger = logging.getLogger("")
1754 root_logger.setLevel(logging.NOTSET)
1756 # Remove all previously setup handlers
1757 for handler in root_logger.handlers:
1759 root_logger.removeHandler(handler)
1762 stderr_handler = logging.StreamHandler()
1763 stderr_handler.setFormatter(formatter)
1765 stderr_handler.setLevel(logging.NOTSET)
1767 stderr_handler.setLevel(logging.CRITICAL)
1768 root_logger.addHandler(stderr_handler)
1770 # this can fail, if the logging directories are not setup or we have
1771 # a permisssion problem; in this case, it's best to log but ignore
1772 # the error if stderr_logging is True, and if false we re-raise the
1773 # exception since otherwise we could run but without any logs at all
1775 logfile_handler = logging.FileHandler(logfile)
1776 logfile_handler.setFormatter(formatter)
1778 logfile_handler.setLevel(logging.DEBUG)
1780 logfile_handler.setLevel(logging.INFO)
1781 root_logger.addHandler(logfile_handler)
1782 except EnvironmentError:
1784 logging.exception("Failed to enable logging to file '%s'", logfile)
1786 # we need to re-raise the exception
1790 def IsNormAbsPath(path):
1791 """Check whether a path is absolute and also normalized
1793 This avoids things like /dir/../../other/path to be valid.
1796 return os.path.normpath(path) == path and os.path.isabs(path)
1799 def TailFile(fname, lines=20):
1800 """Return the last lines from a file.
1802 @note: this function will only read and parse the last 4KB of
1803 the file; if the lines are very long, it could be that less
1804 than the requested number of lines are returned
1806 @param fname: the file name
1808 @param lines: the (maximum) number of lines to return
1811 fd = open(fname, "r")
1815 pos = max(0, pos-4096)
1817 raw_data = fd.read()
1821 rows = raw_data.splitlines()
1822 return rows[-lines:]
1825 def SafeEncode(text):
1826 """Return a 'safe' version of a source string.
1828 This function mangles the input string and returns a version that
1829 should be safe to display/encode as ASCII. To this end, we first
1830 convert it to ASCII using the 'backslashreplace' encoding which
1831 should get rid of any non-ASCII chars, and then we process it
1832 through a loop copied from the string repr sources in the python; we
1833 don't use string_escape anymore since that escape single quotes and
1834 backslashes too, and that is too much; and that escaping is not
1835 stable, i.e. string_escape(string_escape(x)) != string_escape(x).
1837 @type text: str or unicode
1838 @param text: input data
1840 @return: a safe version of text
1843 if isinstance(text, unicode):
1844 # only if unicode; if str already, we handle it below
1845 text = text.encode('ascii', 'backslashreplace')
1855 elif c < 32 or c >= 127: # non-printable
1856 resu += "\\x%02x" % (c & 0xff)
1862 def BytesToMebibyte(value):
1863 """Converts bytes to mebibytes.
1866 @param value: Value in bytes
1868 @return: Value in mebibytes
1871 return int(round(value / (1024.0 * 1024.0), 0))
1874 def CalculateDirectorySize(path):
1875 """Calculates the size of a directory recursively.
1878 @param path: Path to directory
1880 @return: Size in mebibytes
1885 for (curpath, _, files) in os.walk(path):
1886 for filename in files:
1887 st = os.lstat(os.path.join(curpath, filename))
1890 return BytesToMebibyte(size)
1893 def GetFreeFilesystemSpace(path):
1894 """Returns the free space on a filesystem.
1897 @param path: Path on filesystem to be examined
1899 @return: Free space in mebibytes
1902 st = os.statvfs(path)
1904 return BytesToMebibyte(st.f_bavail * st.f_frsize)
1907 def LockedMethod(fn):
1908 """Synchronized object access decorator.
1910 This decorator is intended to protect access to an object using the
1911 object's own lock which is hardcoded to '_lock'.
1914 def _LockDebug(*args, **kwargs):
1916 logging.debug(*args, **kwargs)
1918 def wrapper(self, *args, **kwargs):
1919 assert hasattr(self, '_lock')
1921 _LockDebug("Waiting for %s", lock)
1924 _LockDebug("Acquired %s", lock)
1925 result = fn(self, *args, **kwargs)
1927 _LockDebug("Releasing %s", lock)
1929 _LockDebug("Released %s", lock)
1935 """Locks a file using POSIX locks.
1938 @param fd: the file descriptor we need to lock
1942 fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1943 except IOError, err:
1944 if err.errno == errno.EAGAIN:
1945 raise errors.LockError("File already locked")
1949 def FormatTime(val):
1950 """Formats a time value.
1952 @type val: float or None
1953 @param val: the timestamp as returned by time.time()
1954 @return: a string value or N/A if we don't have a valid timestamp
1957 if val is None or not isinstance(val, (int, float)):
1959 # these two codes works on Linux, but they are not guaranteed on all
1961 return time.strftime("%F %T", time.localtime(val))
1964 def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
1965 """Reads the watcher pause file.
1967 @type filename: string
1968 @param filename: Path to watcher pause file
1969 @type now: None, float or int
1970 @param now: Current time as Unix timestamp
1971 @type remove_after: int
1972 @param remove_after: Remove watcher pause file after specified amount of
1973 seconds past the pause end time
1980 value = ReadFile(filename)
1981 except IOError, err:
1982 if err.errno != errno.ENOENT:
1986 if value is not None:
1990 logging.warning(("Watcher pause file (%s) contains invalid value,"
1991 " removing it"), filename)
1992 RemoveFile(filename)
1995 if value is not None:
1996 # Remove file if it's outdated
1997 if now > (value + remove_after):
1998 RemoveFile(filename)
2007 class FileLock(object):
2008 """Utility class for file locks.
2011 def __init__(self, filename):
2012 """Constructor for FileLock.
2014 This will open the file denoted by the I{filename} argument.
2017 @param filename: path to the file to be locked
2020 self.filename = filename
2021 self.fd = open(self.filename, "w")
2027 """Close the file and release the lock.
2034 def _flock(self, flag, blocking, timeout, errmsg):
2035 """Wrapper for fcntl.flock.
2038 @param flag: operation flag
2039 @type blocking: bool
2040 @param blocking: whether the operation should be done in blocking mode.
2041 @type timeout: None or float
2042 @param timeout: for how long the operation should be retried (implies
2044 @type errmsg: string
2045 @param errmsg: error message in case operation fails.
2048 assert self.fd, "Lock was closed"
2049 assert timeout is None or timeout >= 0, \
2050 "If specified, timeout must be positive"
2052 if timeout is not None:
2053 flag |= fcntl.LOCK_NB
2054 timeout_end = time.time() + timeout
2056 # Blocking doesn't have effect with timeout
2058 flag |= fcntl.LOCK_NB
2064 fcntl.flock(self.fd, flag)
2066 except IOError, err:
2067 if err.errno in (errno.EAGAIN, ):
2068 if timeout_end is not None and time.time() < timeout_end:
2069 # Wait before trying again
2070 time.sleep(max(0.1, min(1.0, timeout)))
2072 raise errors.LockError(errmsg)
2074 logging.exception("fcntl.flock failed")
2077 def Exclusive(self, blocking=False, timeout=None):
2078 """Locks the file in exclusive mode.
2080 @type blocking: boolean
2081 @param blocking: whether to block and wait until we
2082 can lock the file or return immediately
2083 @type timeout: int or None
2084 @param timeout: if not None, the duration to wait for the lock
2088 self._flock(fcntl.LOCK_EX, blocking, timeout,
2089 "Failed to lock %s in exclusive mode" % self.filename)
2091 def Shared(self, blocking=False, timeout=None):
2092 """Locks the file in shared mode.
2094 @type blocking: boolean
2095 @param blocking: whether to block and wait until we
2096 can lock the file or return immediately
2097 @type timeout: int or None
2098 @param timeout: if not None, the duration to wait for the lock
2102 self._flock(fcntl.LOCK_SH, blocking, timeout,
2103 "Failed to lock %s in shared mode" % self.filename)
2105 def Unlock(self, blocking=True, timeout=None):
2106 """Unlocks the file.
2108 According to C{flock(2)}, unlocking can also be a nonblocking
2111 To make a non-blocking request, include LOCK_NB with any of the above
2114 @type blocking: boolean
2115 @param blocking: whether to block and wait until we
2116 can lock the file or return immediately
2117 @type timeout: int or None
2118 @param timeout: if not None, the duration to wait for the lock
2122 self._flock(fcntl.LOCK_UN, blocking, timeout,
2123 "Failed to unlock %s" % self.filename)
2126 def SignalHandled(signums):
2127 """Signal Handled decoration.
2129 This special decorator installs a signal handler and then calls the target
2130 function. The function must accept a 'signal_handlers' keyword argument,
2131 which will contain a dict indexed by signal number, with SignalHandler
2134 The decorator can be safely stacked with iself, to handle multiple signals
2135 with different handlers.
2138 @param signums: signals to intercept
2142 def sig_function(*args, **kwargs):
2143 assert 'signal_handlers' not in kwargs or \
2144 kwargs['signal_handlers'] is None or \
2145 isinstance(kwargs['signal_handlers'], dict), \
2146 "Wrong signal_handlers parameter in original function call"
2147 if 'signal_handlers' in kwargs and kwargs['signal_handlers'] is not None:
2148 signal_handlers = kwargs['signal_handlers']
2150 signal_handlers = {}
2151 kwargs['signal_handlers'] = signal_handlers
2152 sighandler = SignalHandler(signums)
2155 signal_handlers[sig] = sighandler
2156 return fn(*args, **kwargs)
2163 class SignalHandler(object):
2164 """Generic signal handler class.
2166 It automatically restores the original handler when deconstructed or
2167 when L{Reset} is called. You can either pass your own handler
2168 function in or query the L{called} attribute to detect whether the
2172 @ivar signum: the signals we handle
2173 @type called: boolean
2174 @ivar called: tracks whether any of the signals have been raised
2177 def __init__(self, signum):
2178 """Constructs a new SignalHandler instance.
2180 @type signum: int or list of ints
2181 @param signum: Single signal number or set of signal numbers
2184 self.signum = set(signum)
2189 for signum in self.signum:
2191 prev_handler = signal.signal(signum, self._HandleSignal)
2193 self._previous[signum] = prev_handler
2195 # Restore previous handler
2196 signal.signal(signum, prev_handler)
2199 # Reset all handlers
2201 # Here we have a race condition: a handler may have already been called,
2202 # but there's not much we can do about it at this point.
2209 """Restore previous handler.
2211 This will reset all the signals to their previous handlers.
2214 for signum, prev_handler in self._previous.items():
2215 signal.signal(signum, prev_handler)
2216 # If successful, remove from dict
2217 del self._previous[signum]
2220 """Unsets the L{called} flag.
2222 This function can be used in case a signal may arrive several times.
2227 def _HandleSignal(self, signum, frame):
2228 """Actual signal handling function.
2231 # This is not nice and not absolutely atomic, but it appears to be the only
2232 # solution in Python -- there are no atomic types.
2236 class FieldSet(object):
2237 """A simple field set.
2239 Among the features are:
2240 - checking if a string is among a list of static string or regex objects
2241 - checking if a whole list of string matches
2242 - returning the matching groups from a regex match
2244 Internally, all fields are held as regular expression objects.
2247 def __init__(self, *items):
2248 self.items = [re.compile("^%s$" % value) for value in items]
2250 def Extend(self, other_set):
2251 """Extend the field set with the items from another one"""
2252 self.items.extend(other_set.items)
2254 def Matches(self, field):
2255 """Checks if a field matches the current set
2258 @param field: the string to match
2259 @return: either False or a regular expression match object
2262 for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
2266 def NonMatching(self, items):
2267 """Returns the list of fields not matching the current set
2270 @param items: the list of fields to check
2272 @return: list of non-matching fields
2275 return [val for val in items if not self.Matches(val)]