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.
46 from cStringIO import StringIO
49 from hashlib import sha1
54 from ganeti import errors
55 from ganeti import constants
59 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
63 #: when set to True, L{RunCmd} is disabled
66 _RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
69 class RunResult(object):
70 """Holds the result of running external programs.
73 @ivar exit_code: the exit code of the program, or None (if the program
75 @type signal: int or None
76 @ivar signal: the signal that caused the program to finish, or None
77 (if the program wasn't terminated by a signal)
79 @ivar stdout: the standard output of the program
81 @ivar stderr: the standard error of the program
83 @ivar failed: True in case the program was
84 terminated by a signal or exited with a non-zero exit code
85 @ivar fail_reason: a string detailing the termination reason
88 __slots__ = ["exit_code", "signal", "stdout", "stderr",
89 "failed", "fail_reason", "cmd"]
92 def __init__(self, exit_code, signal_, stdout, stderr, cmd):
94 self.exit_code = exit_code
98 self.failed = (signal_ is not None or exit_code != 0)
100 if self.signal is not None:
101 self.fail_reason = "terminated by signal %s" % self.signal
102 elif self.exit_code is not None:
103 self.fail_reason = "exited with exit code %s" % self.exit_code
105 self.fail_reason = "unable to determine termination reason"
108 logging.debug("Command '%s' failed (%s); output: %s",
109 self.cmd, self.fail_reason, self.output)
111 def _GetOutput(self):
112 """Returns the combined stdout and stderr for easier usage.
115 return self.stdout + self.stderr
117 output = property(_GetOutput, None, None, "Return full output")
120 def RunCmd(cmd, env=None, output=None, cwd='/'):
121 """Execute a (shell) command.
123 The command should not read from its standard input, as it will be
126 @type cmd: string or list
127 @param cmd: Command to run
129 @param env: Additional environment
131 @param output: if desired, the output of the command can be
132 saved in a file instead of the RunResult instance; this
133 parameter denotes the file name (if not None)
135 @param cwd: if specified, will be used as the working
136 directory for the command; the default will be /
138 @return: RunResult instance
139 @raise errors.ProgrammerError: if we call this when forks are disabled
143 raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
145 if isinstance(cmd, list):
146 cmd = [str(val) for val in cmd]
147 strcmd = " ".join(cmd)
152 logging.debug("RunCmd '%s'", strcmd)
154 cmd_env = os.environ.copy()
155 cmd_env["LC_ALL"] = "C"
161 out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd)
163 status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
166 if err.errno == errno.ENOENT:
167 raise errors.OpExecError("Can't execute '%s': not found (%s)" %
179 return RunResult(exitcode, signal_, out, err, strcmd)
182 def _RunCmdPipe(cmd, env, via_shell, cwd):
183 """Run a command and return its output.
185 @type cmd: string or list
186 @param cmd: Command to run
188 @param env: The environment to use
189 @type via_shell: bool
190 @param via_shell: if we should run via the shell
192 @param cwd: the working directory for the program
194 @return: (out, err, status)
197 poller = select.poll()
198 child = subprocess.Popen(cmd, shell=via_shell,
199 stderr=subprocess.PIPE,
200 stdout=subprocess.PIPE,
201 stdin=subprocess.PIPE,
202 close_fds=True, env=env,
206 poller.register(child.stdout, select.POLLIN)
207 poller.register(child.stderr, select.POLLIN)
211 child.stdout.fileno(): (out, child.stdout),
212 child.stderr.fileno(): (err, child.stderr),
215 status = fcntl.fcntl(fd, fcntl.F_GETFL)
216 fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
220 pollresult = poller.poll()
221 except EnvironmentError, eerr:
222 if eerr.errno == errno.EINTR:
225 except select.error, serr:
226 if serr[0] == errno.EINTR:
230 for fd, event in pollresult:
231 if event & select.POLLIN or event & select.POLLPRI:
232 data = fdmap[fd][1].read()
233 # no data from read signifies EOF (the same as POLLHUP)
235 poller.unregister(fd)
238 fdmap[fd][0].write(data)
239 if (event & select.POLLNVAL or event & select.POLLHUP or
240 event & select.POLLERR):
241 poller.unregister(fd)
247 status = child.wait()
248 return out, err, status
251 def _RunCmdFile(cmd, env, via_shell, output, cwd):
252 """Run a command and save its output to a file.
254 @type cmd: string or list
255 @param cmd: Command to run
257 @param env: The environment to use
258 @type via_shell: bool
259 @param via_shell: if we should run via the shell
261 @param output: the filename in which to save the output
263 @param cwd: the working directory for the program
265 @return: the exit status
268 fh = open(output, "a")
270 child = subprocess.Popen(cmd, shell=via_shell,
271 stderr=subprocess.STDOUT,
273 stdin=subprocess.PIPE,
274 close_fds=True, env=env,
278 status = child.wait()
284 def RemoveFile(filename):
285 """Remove a file ignoring some errors.
287 Remove a file, ignoring non-existing ones or directories. Other
291 @param filename: the file to be removed
297 if err.errno not in (errno.ENOENT, errno.EISDIR):
301 def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
305 @param old: Original path
309 @param mkdir: Whether to create target directory if it doesn't exist
310 @type mkdir_mode: int
311 @param mkdir_mode: Mode for newly created directories
315 return os.rename(old, new)
317 # In at least one use case of this function, the job queue, directory
318 # creation is very rare. Checking for the directory before renaming is not
320 if mkdir and err.errno == errno.ENOENT:
321 # Create directory and try again
322 os.makedirs(os.path.dirname(new), mkdir_mode)
323 return os.rename(old, new)
327 def _FingerprintFile(filename):
328 """Compute the fingerprint of a file.
330 If the file does not exist, a None will be returned
334 @param filename: the filename to checksum
336 @return: the hex digest of the sha checksum of the contents
340 if not (os.path.exists(filename) and os.path.isfile(filename)):
353 return fp.hexdigest()
356 def FingerprintFiles(files):
357 """Compute fingerprints for a list of files.
360 @param files: the list of filename to fingerprint
362 @return: a dictionary filename: fingerprint, holding only
368 for filename in files:
369 cksum = _FingerprintFile(filename)
371 ret[filename] = cksum
376 def ForceDictType(target, key_types, allowed_values=None):
377 """Force the values of a dict to have certain types.
380 @param target: the dict to update
381 @type key_types: dict
382 @param key_types: dict mapping target dict keys to types
383 in constants.ENFORCEABLE_TYPES
384 @type allowed_values: list
385 @keyword allowed_values: list of specially allowed values
388 if allowed_values is None:
391 if not isinstance(target, dict):
392 msg = "Expected dictionary, got '%s'" % target
393 raise errors.TypeEnforcementError(msg)
396 if key not in key_types:
397 msg = "Unknown key '%s'" % key
398 raise errors.TypeEnforcementError(msg)
400 if target[key] in allowed_values:
403 ktype = key_types[key]
404 if ktype not in constants.ENFORCEABLE_TYPES:
405 msg = "'%s' has non-enforceable type %s" % (key, ktype)
406 raise errors.ProgrammerError(msg)
408 if ktype == constants.VTYPE_STRING:
409 if not isinstance(target[key], basestring):
410 if isinstance(target[key], bool) and not target[key]:
413 msg = "'%s' (value %s) is not a valid string" % (key, target[key])
414 raise errors.TypeEnforcementError(msg)
415 elif ktype == constants.VTYPE_BOOL:
416 if isinstance(target[key], basestring) and target[key]:
417 if target[key].lower() == constants.VALUE_FALSE:
419 elif target[key].lower() == constants.VALUE_TRUE:
422 msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
423 raise errors.TypeEnforcementError(msg)
428 elif ktype == constants.VTYPE_SIZE:
430 target[key] = ParseUnit(target[key])
431 except errors.UnitParseError, err:
432 msg = "'%s' (value %s) is not a valid size. error: %s" % \
433 (key, target[key], err)
434 raise errors.TypeEnforcementError(msg)
435 elif ktype == constants.VTYPE_INT:
437 target[key] = int(target[key])
438 except (ValueError, TypeError):
439 msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
440 raise errors.TypeEnforcementError(msg)
443 def IsProcessAlive(pid):
444 """Check if a given pid exists on the system.
446 @note: zombie status is not handled, so zombie processes
447 will be returned as alive
449 @param pid: the process ID to check
451 @return: True if the process exists
458 os.stat("/proc/%d/status" % pid)
460 except EnvironmentError, err:
461 if err.errno in (errno.ENOENT, errno.ENOTDIR):
466 def ReadPidFile(pidfile):
467 """Read a pid from a file.
469 @type pidfile: string
470 @param pidfile: path to the file containing the pid
472 @return: The process id, if the file exists and contains a valid PID,
477 raw_data = ReadFile(pidfile)
478 except EnvironmentError, err:
479 if err.errno != errno.ENOENT:
480 logging.exception("Can't read pid file")
485 except ValueError, err:
486 logging.info("Can't parse pid file contents", exc_info=True)
492 def MatchNameComponent(key, name_list):
493 """Try to match a name against a list.
495 This function will try to match a name like test1 against a list
496 like C{['test1.example.com', 'test2.example.com', ...]}. Against
497 this list, I{'test1'} as well as I{'test1.example'} will match, but
498 not I{'test1.ex'}. A multiple match will be considered as no match
499 at all (e.g. I{'test1'} against C{['test1.example.com',
500 'test1.example.org']}), except when the key fully matches an entry
501 (e.g. I{'test1'} against C{['test1', 'test1.example.com']}).
504 @param key: the name to be searched
505 @type name_list: list
506 @param name_list: the list of strings against which to search the key
509 @return: None if there is no match I{or} if there are multiple matches,
510 otherwise the element from the list which matches
515 mo = re.compile("^%s(\..*)?$" % re.escape(key))
516 names_filtered = [name for name in name_list if mo.match(name) is not None]
517 if len(names_filtered) != 1:
519 return names_filtered[0]
523 """Class implementing resolver and hostname functionality
526 def __init__(self, name=None):
527 """Initialize the host name object.
529 If the name argument is not passed, it will use this system's
534 name = self.SysName()
537 self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
538 self.ip = self.ipaddrs[0]
541 """Returns the hostname without domain.
544 return self.name.split('.')[0]
548 """Return the current system's name.
550 This is simply a wrapper over C{socket.gethostname()}.
553 return socket.gethostname()
556 def LookupHostname(hostname):
560 @param hostname: hostname to look up
563 @return: a tuple (name, aliases, ipaddrs) as returned by
564 C{socket.gethostbyname_ex}
565 @raise errors.ResolverError: in case of errors in resolving
569 result = socket.gethostbyname_ex(hostname)
570 except socket.gaierror, err:
571 # hostname not found in DNS
572 raise errors.ResolverError(hostname, err.args[0], err.args[1])
577 def ListVolumeGroups():
578 """List volume groups and their size
582 Dictionary with keys volume name and values
583 the size of the volume
586 command = "vgs --noheadings --units m --nosuffix -o name,size"
587 result = RunCmd(command)
592 for line in result.stdout.splitlines():
594 name, size = line.split()
595 size = int(float(size))
596 except (IndexError, ValueError), err:
597 logging.error("Invalid output from vgs (%s): %s", err, line)
605 def BridgeExists(bridge):
606 """Check whether the given bridge exists in the system
609 @param bridge: the bridge name to check
611 @return: True if it does
614 return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
617 def NiceSort(name_list):
618 """Sort a list of strings based on digit and non-digit groupings.
620 Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
621 will sort the list in the logical order C{['a1', 'a2', 'a10',
624 The sort algorithm breaks each name in groups of either only-digits
625 or no-digits. Only the first eight such groups are considered, and
626 after that we just use what's left of the string.
628 @type name_list: list
629 @param name_list: the names to be sorted
631 @return: a copy of the name list sorted with our algorithm
634 _SORTER_BASE = "(\D+|\d+)"
635 _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
636 _SORTER_BASE, _SORTER_BASE,
637 _SORTER_BASE, _SORTER_BASE,
638 _SORTER_BASE, _SORTER_BASE)
639 _SORTER_RE = re.compile(_SORTER_FULL)
640 _SORTER_NODIGIT = re.compile("^\D*$")
642 """Attempts to convert a variable to integer."""
643 if val is None or _SORTER_NODIGIT.match(val):
648 to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
649 for name in name_list]
651 return [tup[1] for tup in to_sort]
654 def TryConvert(fn, val):
655 """Try to convert a value ignoring errors.
657 This function tries to apply function I{fn} to I{val}. If no
658 C{ValueError} or C{TypeError} exceptions are raised, it will return
659 the result, else it will return the original value. Any other
660 exceptions are propagated to the caller.
663 @param fn: function to apply to the value
664 @param val: the value to be converted
665 @return: The converted value if the conversion was successful,
666 otherwise the original value.
671 except (ValueError, TypeError):
677 """Verifies the syntax of an IPv4 address.
679 This function checks if the IPv4 address passes is valid or not based
680 on syntax (not IP range, class calculations, etc.).
683 @param ip: the address to be checked
684 @rtype: a regular expression match object
685 @return: a regular expression match object, or None if the
689 unit = "(0|[1-9]\d{0,2})"
690 #TODO: convert and return only boolean
691 return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
694 def IsValidShellParam(word):
695 """Verifies is the given word is safe from the shell's p.o.v.
697 This means that we can pass this to a command via the shell and be
698 sure that it doesn't alter the command line and is passed as such to
701 Note that we are overly restrictive here, in order to be on the safe
705 @param word: the word to check
707 @return: True if the word is 'safe'
710 return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
713 def BuildShellCmd(template, *args):
714 """Build a safe shell command line from the given arguments.
716 This function will check all arguments in the args list so that they
717 are valid shell parameters (i.e. they don't contain shell
718 metacharacters). If everything is ok, it will return the result of
722 @param template: the string holding the template for the
725 @return: the expanded command line
729 if not IsValidShellParam(word):
730 raise errors.ProgrammerError("Shell argument '%s' contains"
731 " invalid characters" % word)
732 return template % args
735 def FormatUnit(value, units):
736 """Formats an incoming number of MiB with the appropriate unit.
739 @param value: integer representing the value in MiB (1048576)
741 @param units: the type of formatting we should do:
742 - 'h' for automatic scaling
747 @return: the formatted value (with suffix)
750 if units not in ('m', 'g', 't', 'h'):
751 raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
755 if units == 'm' or (units == 'h' and value < 1024):
758 return "%d%s" % (round(value, 0), suffix)
760 elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
763 return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
768 return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
771 def ParseUnit(input_string):
772 """Tries to extract number and scale from the given string.
774 Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
775 [UNIT]}. If no unit is specified, it defaults to MiB. Return value
776 is always an int in MiB.
779 m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', str(input_string))
781 raise errors.UnitParseError("Invalid format")
783 value = float(m.groups()[0])
787 lcunit = unit.lower()
791 if lcunit in ('m', 'mb', 'mib'):
792 # Value already in MiB
795 elif lcunit in ('g', 'gb', 'gib'):
798 elif lcunit in ('t', 'tb', 'tib'):
802 raise errors.UnitParseError("Unknown unit: %s" % unit)
804 # Make sure we round up
805 if int(value) < value:
808 # Round up to the next multiple of 4
811 value += 4 - value % 4
816 def AddAuthorizedKey(file_name, key):
817 """Adds an SSH public key to an authorized_keys file.
820 @param file_name: path to authorized_keys file
822 @param key: string containing key
825 key_fields = key.split()
827 f = open(file_name, 'a+')
831 # Ignore whitespace changes
832 if line.split() == key_fields:
834 nl = line.endswith('\n')
838 f.write(key.rstrip('\r\n'))
845 def RemoveAuthorizedKey(file_name, key):
846 """Removes an SSH public key from an authorized_keys file.
849 @param file_name: path to authorized_keys file
851 @param key: string containing key
854 key_fields = key.split()
856 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
858 out = os.fdopen(fd, 'w')
860 f = open(file_name, 'r')
863 # Ignore whitespace changes while comparing lines
864 if line.split() != key_fields:
868 os.rename(tmpname, file_name)
878 def SetEtcHostsEntry(file_name, ip, hostname, aliases):
879 """Sets the name of an IP address and hostname in /etc/hosts.
882 @param file_name: path to the file to modify (usually C{/etc/hosts})
884 @param ip: the IP address
886 @param hostname: the hostname to be added
888 @param aliases: the list of aliases to add for the hostname
891 # FIXME: use WriteFile + fn rather than duplicating its efforts
892 # Ensure aliases are unique
893 aliases = UniqueSequence([hostname] + aliases)[1:]
895 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
897 out = os.fdopen(fd, 'w')
899 f = open(file_name, 'r')
902 fields = line.split()
903 if fields and not fields[0].startswith('#') and ip == fields[0]:
907 out.write("%s\t%s" % (ip, hostname))
909 out.write(" %s" % ' '.join(aliases))
914 os.chmod(tmpname, 0644)
915 os.rename(tmpname, file_name)
925 def AddHostToEtcHosts(hostname):
926 """Wrapper around SetEtcHostsEntry.
929 @param hostname: a hostname that will be resolved and added to
930 L{constants.ETC_HOSTS}
933 hi = HostInfo(name=hostname)
934 SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
937 def RemoveEtcHostsEntry(file_name, hostname):
938 """Removes a hostname from /etc/hosts.
940 IP addresses without names are removed from the file.
943 @param file_name: path to the file to modify (usually C{/etc/hosts})
945 @param hostname: the hostname to be removed
948 # FIXME: use WriteFile + fn rather than duplicating its efforts
949 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
951 out = os.fdopen(fd, 'w')
953 f = open(file_name, 'r')
956 fields = line.split()
957 if len(fields) > 1 and not fields[0].startswith('#'):
959 if hostname in names:
960 while hostname in names:
961 names.remove(hostname)
963 out.write("%s %s\n" % (fields[0], ' '.join(names)))
970 os.chmod(tmpname, 0644)
971 os.rename(tmpname, file_name)
981 def RemoveHostFromEtcHosts(hostname):
982 """Wrapper around RemoveEtcHostsEntry.
985 @param hostname: hostname that will be resolved and its
986 full and shot name will be removed from
987 L{constants.ETC_HOSTS}
990 hi = HostInfo(name=hostname)
991 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
992 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
995 def CreateBackup(file_name):
996 """Creates a backup of a file.
999 @param file_name: file to be backed up
1001 @return: the path to the newly created backup
1002 @raise errors.ProgrammerError: for invalid file names
1005 if not os.path.isfile(file_name):
1006 raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
1009 prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
1010 dir_name = os.path.dirname(file_name)
1012 fsrc = open(file_name, 'rb')
1014 (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
1015 fdst = os.fdopen(fd, 'wb')
1017 shutil.copyfileobj(fsrc, fdst)
1026 def ShellQuote(value):
1027 """Quotes shell argument according to POSIX.
1030 @param value: the argument to be quoted
1032 @return: the quoted value
1035 if _re_shell_unquoted.match(value):
1038 return "'%s'" % value.replace("'", "'\\''")
1041 def ShellQuoteArgs(args):
1042 """Quotes a list of shell arguments.
1045 @param args: list of arguments to be quoted
1047 @return: the quoted arguments concatenated with spaces
1050 return ' '.join([ShellQuote(i) for i in args])
1053 def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
1054 """Simple ping implementation using TCP connect(2).
1056 Check if the given IP is reachable by doing attempting a TCP connect
1060 @param target: the IP or hostname to ping
1062 @param port: the port to connect to
1064 @param timeout: the timeout on the connection attempt
1065 @type live_port_needed: boolean
1066 @param live_port_needed: whether a closed port will cause the
1067 function to return failure, as if there was a timeout
1068 @type source: str or None
1069 @param source: if specified, will cause the connect to be made
1070 from this specific source address; failures to bind other
1071 than C{EADDRNOTAVAIL} will be ignored
1074 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1078 if source is not None:
1080 sock.bind((source, 0))
1081 except socket.error, (errcode, _):
1082 if errcode == errno.EADDRNOTAVAIL:
1085 sock.settimeout(timeout)
1088 sock.connect((target, port))
1091 except socket.timeout:
1093 except socket.error, (errcode, errstring):
1094 success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
1099 def OwnIpAddress(address):
1100 """Check if the current host has the the given IP address.
1102 Currently this is done by TCP-pinging the address from the loopback
1105 @type address: string
1106 @param address: the address to check
1108 @return: True if we own the address
1111 return TcpPing(address, constants.DEFAULT_NODED_PORT,
1112 source=constants.LOCALHOST_IP_ADDRESS)
1115 def ListVisibleFiles(path):
1116 """Returns a list of visible files in a directory.
1119 @param path: the directory to enumerate
1121 @return: the list of all files not starting with a dot
1124 files = [i for i in os.listdir(path) if not i.startswith(".")]
1129 def GetHomeDir(user, default=None):
1130 """Try to get the homedir of the given user.
1132 The user can be passed either as a string (denoting the name) or as
1133 an integer (denoting the user id). If the user is not found, the
1134 'default' argument is returned, which defaults to None.
1138 if isinstance(user, basestring):
1139 result = pwd.getpwnam(user)
1140 elif isinstance(user, (int, long)):
1141 result = pwd.getpwuid(user)
1143 raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
1147 return result.pw_dir
1151 """Returns a random UUID.
1153 @note: This is a Linux-specific method as it uses the /proc
1158 return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
1161 def GenerateSecret(numbytes=20):
1162 """Generates a random secret.
1164 This will generate a pseudo-random secret returning an hex string
1165 (so that it can be used where an ASCII string is needed).
1167 @param numbytes: the number of bytes which will be represented by the returned
1168 string (defaulting to 20, the length of a SHA1 hash)
1170 @return: an hex representation of the pseudo-random sequence
1173 return os.urandom(numbytes).encode('hex')
1176 def EnsureDirs(dirs):
1177 """Make required directories, if they don't exist.
1179 @param dirs: list of tuples (dir_name, dir_mode)
1180 @type dirs: list of (string, integer)
1183 for dir_name, dir_mode in dirs:
1185 os.mkdir(dir_name, dir_mode)
1186 except EnvironmentError, err:
1187 if err.errno != errno.EEXIST:
1188 raise errors.GenericError("Cannot create needed directory"
1189 " '%s': %s" % (dir_name, err))
1190 if not os.path.isdir(dir_name):
1191 raise errors.GenericError("%s is not a directory" % dir_name)
1194 def ReadFile(file_name, size=None):
1197 @type size: None or int
1198 @param size: Read at most size bytes
1200 @return: the (possibly partial) content of the file
1203 f = open(file_name, "r")
1213 def WriteFile(file_name, fn=None, data=None,
1214 mode=None, uid=-1, gid=-1,
1215 atime=None, mtime=None, close=True,
1216 dry_run=False, backup=False,
1217 prewrite=None, postwrite=None):
1218 """(Over)write a file atomically.
1220 The file_name and either fn (a function taking one argument, the
1221 file descriptor, and which should write the data to it) or data (the
1222 contents of the file) must be passed. The other arguments are
1223 optional and allow setting the file mode, owner and group, and the
1224 mtime/atime of the file.
1226 If the function doesn't raise an exception, it has succeeded and the
1227 target file has the new contents. If the function has raised an
1228 exception, an existing target file should be unmodified and the
1229 temporary file should be removed.
1231 @type file_name: str
1232 @param file_name: the target filename
1234 @param fn: content writing function, called with
1235 file descriptor as parameter
1237 @param data: contents of the file
1239 @param mode: file mode
1241 @param uid: the owner of the file
1243 @param gid: the group of the file
1245 @param atime: a custom access time to be set on the file
1247 @param mtime: a custom modification time to be set on the file
1248 @type close: boolean
1249 @param close: whether to close file after writing it
1250 @type prewrite: callable
1251 @param prewrite: function to be called before writing content
1252 @type postwrite: callable
1253 @param postwrite: function to be called after writing content
1256 @return: None if the 'close' parameter evaluates to True,
1257 otherwise the file descriptor
1259 @raise errors.ProgrammerError: if any of the arguments are not valid
1262 if not os.path.isabs(file_name):
1263 raise errors.ProgrammerError("Path passed to WriteFile is not"
1264 " absolute: '%s'" % file_name)
1266 if [fn, data].count(None) != 1:
1267 raise errors.ProgrammerError("fn or data required")
1269 if [atime, mtime].count(None) == 1:
1270 raise errors.ProgrammerError("Both atime and mtime must be either"
1273 if backup and not dry_run and os.path.isfile(file_name):
1274 CreateBackup(file_name)
1276 dir_name, base_name = os.path.split(file_name)
1277 fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1279 # here we need to make sure we remove the temp file, if any error
1280 # leaves it in place
1282 if uid != -1 or gid != -1:
1283 os.chown(new_name, uid, gid)
1285 os.chmod(new_name, mode)
1286 if callable(prewrite):
1288 if data is not None:
1292 if callable(postwrite):
1295 if atime is not None and mtime is not None:
1296 os.utime(new_name, (atime, mtime))
1298 os.rename(new_name, file_name)
1307 RemoveFile(new_name)
1312 def FirstFree(seq, base=0):
1313 """Returns the first non-existing integer from seq.
1315 The seq argument should be a sorted list of positive integers. The
1316 first time the index of an element is smaller than the element
1317 value, the index will be returned.
1319 The base argument is used to start at a different offset,
1320 i.e. C{[3, 4, 6]} with I{offset=3} will return 5.
1322 Example: C{[0, 1, 3]} will return I{2}.
1325 @param seq: the sequence to be analyzed.
1327 @param base: use this value as the base index of the sequence
1329 @return: the first non-used index in the sequence
1332 for idx, elem in enumerate(seq):
1333 assert elem >= base, "Passed element is higher than base offset"
1334 if elem > idx + base:
1340 def all(seq, pred=bool):
1341 "Returns True if pred(x) is True for every element in the iterable"
1342 for _ in itertools.ifilterfalse(pred, seq):
1347 def any(seq, pred=bool):
1348 "Returns True if pred(x) is True for at least one element in the iterable"
1349 for _ in itertools.ifilter(pred, seq):
1354 def UniqueSequence(seq):
1355 """Returns a list with unique elements.
1357 Element order is preserved.
1360 @param seq: the sequence with the source elements
1362 @return: list of unique elements from seq
1366 return [i for i in seq if i not in seen and not seen.add(i)]
1369 def IsValidMac(mac):
1370 """Predicate to check if a MAC address is valid.
1372 Checks whether the supplied MAC address is formally correct, only
1373 accepts colon separated format.
1376 @param mac: the MAC to be validated
1378 @return: True is the MAC seems valid
1381 mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
1382 return mac_check.match(mac) is not None
1385 def TestDelay(duration):
1386 """Sleep for a fixed amount of time.
1388 @type duration: float
1389 @param duration: the sleep duration
1391 @return: False for negative value, True otherwise
1395 return False, "Invalid sleep duration"
1396 time.sleep(duration)
1400 def _CloseFDNoErr(fd, retries=5):
1401 """Close a file descriptor ignoring errors.
1404 @param fd: the file descriptor
1406 @param retries: how many retries to make, in case we get any
1407 other error than EBADF
1412 except OSError, err:
1413 if err.errno != errno.EBADF:
1415 _CloseFDNoErr(fd, retries - 1)
1416 # else either it's closed already or we're out of retries, so we
1417 # ignore this and go on
1420 def CloseFDs(noclose_fds=None):
1421 """Close file descriptors.
1423 This closes all file descriptors above 2 (i.e. except
1426 @type noclose_fds: list or None
1427 @param noclose_fds: if given, it denotes a list of file descriptor
1428 that should not be closed
1431 # Default maximum for the number of available file descriptors.
1432 if 'SC_OPEN_MAX' in os.sysconf_names:
1434 MAXFD = os.sysconf('SC_OPEN_MAX')
1441 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1442 if (maxfd == resource.RLIM_INFINITY):
1445 # Iterate through and close all file descriptors (except the standard ones)
1446 for fd in range(3, maxfd):
1447 if noclose_fds and fd in noclose_fds:
1452 def Daemonize(logfile):
1453 """Daemonize the current process.
1455 This detaches the current process from the controlling terminal and
1456 runs it in the background as a daemon.
1459 @param logfile: the logfile to which we should redirect stdout/stderr
1461 @return: the value zero
1469 if (pid == 0): # The first child.
1472 pid = os.fork() # Fork a second child.
1473 if (pid == 0): # The second child.
1477 # exit() or _exit()? See below.
1478 os._exit(0) # Exit parent (the first child) of the second child.
1480 os._exit(0) # Exit parent of the first child.
1484 i = os.open("/dev/null", os.O_RDONLY) # stdin
1485 assert i == 0, "Can't close/reopen stdin"
1486 i = os.open(logfile, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0600) # stdout
1487 assert i == 1, "Can't close/reopen stdout"
1488 # Duplicate standard output to standard error.
1493 def DaemonPidFileName(name):
1494 """Compute a ganeti pid file absolute path
1497 @param name: the daemon name
1499 @return: the full path to the pidfile corresponding to the given
1503 return os.path.join(constants.RUN_GANETI_DIR, "%s.pid" % name)
1506 def WritePidFile(name):
1507 """Write the current process pidfile.
1509 The file will be written to L{constants.RUN_GANETI_DIR}I{/name.pid}
1512 @param name: the daemon name to use
1513 @raise errors.GenericError: if the pid file already exists and
1514 points to a live process
1518 pidfilename = DaemonPidFileName(name)
1519 if IsProcessAlive(ReadPidFile(pidfilename)):
1520 raise errors.GenericError("%s contains a live process" % pidfilename)
1522 WriteFile(pidfilename, data="%d\n" % pid)
1525 def RemovePidFile(name):
1526 """Remove the current process pidfile.
1528 Any errors are ignored.
1531 @param name: the daemon name used to derive the pidfile name
1534 pidfilename = DaemonPidFileName(name)
1535 # TODO: we could check here that the file contains our pid
1537 RemoveFile(pidfilename)
1542 def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1544 """Kill a process given by its pid.
1547 @param pid: The PID to terminate.
1549 @param signal_: The signal to send, by default SIGTERM
1551 @param timeout: The timeout after which, if the process is still alive,
1552 a SIGKILL will be sent. If not positive, no such checking
1554 @type waitpid: boolean
1555 @param waitpid: If true, we should waitpid on this process after
1556 sending signals, since it's our own child and otherwise it
1557 would remain as zombie
1560 def _helper(pid, signal_, wait):
1561 """Simple helper to encapsulate the kill/waitpid sequence"""
1562 os.kill(pid, signal_)
1565 os.waitpid(pid, os.WNOHANG)
1570 # kill with pid=0 == suicide
1571 raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1573 if not IsProcessAlive(pid):
1575 _helper(pid, signal_, waitpid)
1579 # Wait up to $timeout seconds
1580 end = time.time() + timeout
1582 while time.time() < end and IsProcessAlive(pid):
1584 (result_pid, _) = os.waitpid(pid, os.WNOHANG)
1590 # Make wait time longer for next try
1594 if IsProcessAlive(pid):
1595 # Kill process if it's still alive
1596 _helper(pid, signal.SIGKILL, waitpid)
1599 def FindFile(name, search_path, test=os.path.exists):
1600 """Look for a filesystem object in a given path.
1602 This is an abstract method to search for filesystem object (files,
1603 dirs) under a given search path.
1606 @param name: the name to look for
1607 @type search_path: str
1608 @param search_path: location to start at
1609 @type test: callable
1610 @param test: a function taking one argument that should return True
1611 if the a given object is valid; the default value is
1612 os.path.exists, causing only existing files to be returned
1614 @return: full path to the object if found, None otherwise
1617 for dir_name in search_path:
1618 item_name = os.path.sep.join([dir_name, name])
1624 def CheckVolumeGroupSize(vglist, vgname, minsize):
1625 """Checks if the volume group list is valid.
1627 The function will check if a given volume group is in the list of
1628 volume groups and has a minimum size.
1631 @param vglist: dictionary of volume group names and their size
1633 @param vgname: the volume group we should check
1635 @param minsize: the minimum size we accept
1637 @return: None for success, otherwise the error message
1640 vgsize = vglist.get(vgname, None)
1642 return "volume group '%s' missing" % vgname
1643 elif vgsize < minsize:
1644 return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1645 (vgname, minsize, vgsize))
1649 def SplitTime(value):
1650 """Splits time as floating point number into a tuple.
1652 @param value: Time in seconds
1653 @type value: int or float
1654 @return: Tuple containing (seconds, microseconds)
1657 (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
1659 assert 0 <= seconds, \
1660 "Seconds must be larger than or equal to 0, but are %s" % seconds
1661 assert 0 <= microseconds <= 999999, \
1662 "Microseconds must be 0-999999, but are %s" % microseconds
1664 return (int(seconds), int(microseconds))
1667 def MergeTime(timetuple):
1668 """Merges a tuple into time as a floating point number.
1670 @param timetuple: Time as tuple, (seconds, microseconds)
1671 @type timetuple: tuple
1672 @return: Time as a floating point number expressed in seconds
1675 (seconds, microseconds) = timetuple
1677 assert 0 <= seconds, \
1678 "Seconds must be larger than or equal to 0, but are %s" % seconds
1679 assert 0 <= microseconds <= 999999, \
1680 "Microseconds must be 0-999999, but are %s" % microseconds
1682 return float(seconds) + (float(microseconds) * 0.000001)
1685 def GetDaemonPort(daemon_name):
1686 """Get the daemon port for this cluster.
1688 Note that this routine does not read a ganeti-specific file, but
1689 instead uses C{socket.getservbyname} to allow pre-customization of
1690 this parameter outside of Ganeti.
1692 @type daemon_name: string
1693 @param daemon_name: daemon name (in constants.DAEMONS_PORTS)
1697 if daemon_name not in constants.DAEMONS_PORTS:
1698 raise errors.ProgrammerError("Unknown daemon: %s" % daemon_name)
1700 (proto, default_port) = constants.DAEMONS_PORTS[daemon_name]
1702 port = socket.getservbyname(daemon_name, proto)
1703 except socket.error:
1709 def SetupLogging(logfile, debug=False, stderr_logging=False, program="",
1710 multithreaded=False):
1711 """Configures the logging module.
1714 @param logfile: the filename to which we should log
1715 @type debug: boolean
1716 @param debug: whether to enable debug messages too or
1717 only those at C{INFO} and above level
1718 @type stderr_logging: boolean
1719 @param stderr_logging: whether we should also log to the standard error
1721 @param program: the name under which we should log messages
1722 @type multithreaded: boolean
1723 @param multithreaded: if True, will add the thread name to the log file
1724 @raise EnvironmentError: if we can't open the log file and
1725 stderr logging is disabled
1728 fmt = "%(asctime)s: " + program + " pid=%(process)d"
1730 fmt += "/%(threadName)s"
1732 fmt += " %(module)s:%(lineno)s"
1733 fmt += " %(levelname)s %(message)s"
1734 formatter = logging.Formatter(fmt)
1736 root_logger = logging.getLogger("")
1737 root_logger.setLevel(logging.NOTSET)
1739 # Remove all previously setup handlers
1740 for handler in root_logger.handlers:
1742 root_logger.removeHandler(handler)
1745 stderr_handler = logging.StreamHandler()
1746 stderr_handler.setFormatter(formatter)
1748 stderr_handler.setLevel(logging.NOTSET)
1750 stderr_handler.setLevel(logging.CRITICAL)
1751 root_logger.addHandler(stderr_handler)
1753 # this can fail, if the logging directories are not setup or we have
1754 # a permisssion problem; in this case, it's best to log but ignore
1755 # the error if stderr_logging is True, and if false we re-raise the
1756 # exception since otherwise we could run but without any logs at all
1758 logfile_handler = logging.FileHandler(logfile)
1759 logfile_handler.setFormatter(formatter)
1761 logfile_handler.setLevel(logging.DEBUG)
1763 logfile_handler.setLevel(logging.INFO)
1764 root_logger.addHandler(logfile_handler)
1765 except EnvironmentError:
1767 logging.exception("Failed to enable logging to file '%s'", logfile)
1769 # we need to re-raise the exception
1773 def IsNormAbsPath(path):
1774 """Check whether a path is absolute and also normalized
1776 This avoids things like /dir/../../other/path to be valid.
1779 return os.path.normpath(path) == path and os.path.isabs(path)
1782 def TailFile(fname, lines=20):
1783 """Return the last lines from a file.
1785 @note: this function will only read and parse the last 4KB of
1786 the file; if the lines are very long, it could be that less
1787 than the requested number of lines are returned
1789 @param fname: the file name
1791 @param lines: the (maximum) number of lines to return
1794 fd = open(fname, "r")
1798 pos = max(0, pos-4096)
1800 raw_data = fd.read()
1804 rows = raw_data.splitlines()
1805 return rows[-lines:]
1808 def SafeEncode(text):
1809 """Return a 'safe' version of a source string.
1811 This function mangles the input string and returns a version that
1812 should be safe to display/encode as ASCII. To this end, we first
1813 convert it to ASCII using the 'backslashreplace' encoding which
1814 should get rid of any non-ASCII chars, and then we process it
1815 through a loop copied from the string repr sources in the python; we
1816 don't use string_escape anymore since that escape single quotes and
1817 backslashes too, and that is too much; and that escaping is not
1818 stable, i.e. string_escape(string_escape(x)) != string_escape(x).
1820 @type text: str or unicode
1821 @param text: input data
1823 @return: a safe version of text
1826 if isinstance(text, unicode):
1827 # only if unicode; if str already, we handle it below
1828 text = text.encode('ascii', 'backslashreplace')
1838 elif c < 32 or c >= 127: # non-printable
1839 resu += "\\x%02x" % (c & 0xff)
1845 def CommaJoin(names):
1846 """Nicely join a set of identifiers.
1848 @param names: set, list or tuple
1849 @return: a string with the formatted results
1852 return ", ".join(["'%s'" % val for val in names])
1855 def BytesToMebibyte(value):
1856 """Converts bytes to mebibytes.
1859 @param value: Value in bytes
1861 @return: Value in mebibytes
1864 return int(round(value / (1024.0 * 1024.0), 0))
1867 def CalculateDirectorySize(path):
1868 """Calculates the size of a directory recursively.
1871 @param path: Path to directory
1873 @return: Size in mebibytes
1878 for (curpath, _, files) in os.walk(path):
1879 for filename in files:
1880 st = os.lstat(os.path.join(curpath, filename))
1883 return BytesToMebibyte(size)
1886 def GetFreeFilesystemSpace(path):
1887 """Returns the free space on a filesystem.
1890 @param path: Path on filesystem to be examined
1892 @return: Free space in mebibytes
1895 st = os.statvfs(path)
1897 return BytesToMebibyte(st.f_bavail * st.f_frsize)
1900 def LockedMethod(fn):
1901 """Synchronized object access decorator.
1903 This decorator is intended to protect access to an object using the
1904 object's own lock which is hardcoded to '_lock'.
1907 def _LockDebug(*args, **kwargs):
1909 logging.debug(*args, **kwargs)
1911 def wrapper(self, *args, **kwargs):
1912 assert hasattr(self, '_lock')
1914 _LockDebug("Waiting for %s", lock)
1917 _LockDebug("Acquired %s", lock)
1918 result = fn(self, *args, **kwargs)
1920 _LockDebug("Releasing %s", lock)
1922 _LockDebug("Released %s", lock)
1928 """Locks a file using POSIX locks.
1931 @param fd: the file descriptor we need to lock
1935 fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1936 except IOError, err:
1937 if err.errno == errno.EAGAIN:
1938 raise errors.LockError("File already locked")
1942 def FormatTime(val):
1943 """Formats a time value.
1945 @type val: float or None
1946 @param val: the timestamp as returned by time.time()
1947 @return: a string value or N/A if we don't have a valid timestamp
1950 if val is None or not isinstance(val, (int, float)):
1952 # these two codes works on Linux, but they are not guaranteed on all
1954 return time.strftime("%F %T", time.localtime(val))
1957 def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
1958 """Reads the watcher pause file.
1960 @type filename: string
1961 @param filename: Path to watcher pause file
1962 @type now: None, float or int
1963 @param now: Current time as Unix timestamp
1964 @type remove_after: int
1965 @param remove_after: Remove watcher pause file after specified amount of
1966 seconds past the pause end time
1973 value = ReadFile(filename)
1974 except IOError, err:
1975 if err.errno != errno.ENOENT:
1979 if value is not None:
1983 logging.warning(("Watcher pause file (%s) contains invalid value,"
1984 " removing it"), filename)
1985 RemoveFile(filename)
1988 if value is not None:
1989 # Remove file if it's outdated
1990 if now > (value + remove_after):
1991 RemoveFile(filename)
2000 class FileLock(object):
2001 """Utility class for file locks.
2004 def __init__(self, filename):
2005 """Constructor for FileLock.
2007 This will open the file denoted by the I{filename} argument.
2010 @param filename: path to the file to be locked
2013 self.filename = filename
2014 self.fd = open(self.filename, "w")
2020 """Close the file and release the lock.
2027 def _flock(self, flag, blocking, timeout, errmsg):
2028 """Wrapper for fcntl.flock.
2031 @param flag: operation flag
2032 @type blocking: bool
2033 @param blocking: whether the operation should be done in blocking mode.
2034 @type timeout: None or float
2035 @param timeout: for how long the operation should be retried (implies
2037 @type errmsg: string
2038 @param errmsg: error message in case operation fails.
2041 assert self.fd, "Lock was closed"
2042 assert timeout is None or timeout >= 0, \
2043 "If specified, timeout must be positive"
2045 if timeout is not None:
2046 flag |= fcntl.LOCK_NB
2047 timeout_end = time.time() + timeout
2049 # Blocking doesn't have effect with timeout
2051 flag |= fcntl.LOCK_NB
2057 fcntl.flock(self.fd, flag)
2059 except IOError, err:
2060 if err.errno in (errno.EAGAIN, ):
2061 if timeout_end is not None and time.time() < timeout_end:
2062 # Wait before trying again
2063 time.sleep(max(0.1, min(1.0, timeout)))
2065 raise errors.LockError(errmsg)
2067 logging.exception("fcntl.flock failed")
2070 def Exclusive(self, blocking=False, timeout=None):
2071 """Locks the file in exclusive mode.
2073 @type blocking: boolean
2074 @param blocking: whether to block and wait until we
2075 can lock the file or return immediately
2076 @type timeout: int or None
2077 @param timeout: if not None, the duration to wait for the lock
2081 self._flock(fcntl.LOCK_EX, blocking, timeout,
2082 "Failed to lock %s in exclusive mode" % self.filename)
2084 def Shared(self, blocking=False, timeout=None):
2085 """Locks the file in shared mode.
2087 @type blocking: boolean
2088 @param blocking: whether to block and wait until we
2089 can lock the file or return immediately
2090 @type timeout: int or None
2091 @param timeout: if not None, the duration to wait for the lock
2095 self._flock(fcntl.LOCK_SH, blocking, timeout,
2096 "Failed to lock %s in shared mode" % self.filename)
2098 def Unlock(self, blocking=True, timeout=None):
2099 """Unlocks the file.
2101 According to C{flock(2)}, unlocking can also be a nonblocking
2104 To make a non-blocking request, include LOCK_NB with any of the above
2107 @type blocking: boolean
2108 @param blocking: whether to block and wait until we
2109 can lock the file or return immediately
2110 @type timeout: int or None
2111 @param timeout: if not None, the duration to wait for the lock
2115 self._flock(fcntl.LOCK_UN, blocking, timeout,
2116 "Failed to unlock %s" % self.filename)
2119 def SignalHandled(signums):
2120 """Signal Handled decoration.
2122 This special decorator installs a signal handler and then calls the target
2123 function. The function must accept a 'signal_handlers' keyword argument,
2124 which will contain a dict indexed by signal number, with SignalHandler
2127 The decorator can be safely stacked with iself, to handle multiple signals
2128 with different handlers.
2131 @param signums: signals to intercept
2135 def sig_function(*args, **kwargs):
2136 assert 'signal_handlers' not in kwargs or \
2137 kwargs['signal_handlers'] is None or \
2138 isinstance(kwargs['signal_handlers'], dict), \
2139 "Wrong signal_handlers parameter in original function call"
2140 if 'signal_handlers' in kwargs and kwargs['signal_handlers'] is not None:
2141 signal_handlers = kwargs['signal_handlers']
2143 signal_handlers = {}
2144 kwargs['signal_handlers'] = signal_handlers
2145 sighandler = SignalHandler(signums)
2148 signal_handlers[sig] = sighandler
2149 return fn(*args, **kwargs)
2156 class SignalHandler(object):
2157 """Generic signal handler class.
2159 It automatically restores the original handler when deconstructed or
2160 when L{Reset} is called. You can either pass your own handler
2161 function in or query the L{called} attribute to detect whether the
2165 @ivar signum: the signals we handle
2166 @type called: boolean
2167 @ivar called: tracks whether any of the signals have been raised
2170 def __init__(self, signum):
2171 """Constructs a new SignalHandler instance.
2173 @type signum: int or list of ints
2174 @param signum: Single signal number or set of signal numbers
2177 self.signum = set(signum)
2182 for signum in self.signum:
2184 prev_handler = signal.signal(signum, self._HandleSignal)
2186 self._previous[signum] = prev_handler
2188 # Restore previous handler
2189 signal.signal(signum, prev_handler)
2192 # Reset all handlers
2194 # Here we have a race condition: a handler may have already been called,
2195 # but there's not much we can do about it at this point.
2202 """Restore previous handler.
2204 This will reset all the signals to their previous handlers.
2207 for signum, prev_handler in self._previous.items():
2208 signal.signal(signum, prev_handler)
2209 # If successful, remove from dict
2210 del self._previous[signum]
2213 """Unsets the L{called} flag.
2215 This function can be used in case a signal may arrive several times.
2220 def _HandleSignal(self, signum, frame):
2221 """Actual signal handling function.
2224 # This is not nice and not absolutely atomic, but it appears to be the only
2225 # solution in Python -- there are no atomic types.
2229 class FieldSet(object):
2230 """A simple field set.
2232 Among the features are:
2233 - checking if a string is among a list of static string or regex objects
2234 - checking if a whole list of string matches
2235 - returning the matching groups from a regex match
2237 Internally, all fields are held as regular expression objects.
2240 def __init__(self, *items):
2241 self.items = [re.compile("^%s$" % value) for value in items]
2243 def Extend(self, other_set):
2244 """Extend the field set with the items from another one"""
2245 self.items.extend(other_set.items)
2247 def Matches(self, field):
2248 """Checks if a field matches the current set
2251 @param field: the string to match
2252 @return: either False or a regular expression match object
2255 for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
2259 def NonMatching(self, items):
2260 """Returns the list of fields not matching the current set
2263 @param items: the list of fields to check
2265 @return: list of non-matching fields
2268 return [val for val in items if not self.Matches(val)]