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
67 class RunResult(object):
68 """Holds the result of running external programs.
71 @ivar exit_code: the exit code of the program, or None (if the program
73 @type signal: int or None
74 @ivar signal: the signal that caused the program to finish, or None
75 (if the program wasn't terminated by a signal)
77 @ivar stdout: the standard output of the program
79 @ivar stderr: the standard error of the program
81 @ivar failed: True in case the program was
82 terminated by a signal or exited with a non-zero exit code
83 @ivar fail_reason: a string detailing the termination reason
86 __slots__ = ["exit_code", "signal", "stdout", "stderr",
87 "failed", "fail_reason", "cmd"]
90 def __init__(self, exit_code, signal_, stdout, stderr, cmd):
92 self.exit_code = exit_code
96 self.failed = (signal_ is not None or exit_code != 0)
98 if self.signal is not None:
99 self.fail_reason = "terminated by signal %s" % self.signal
100 elif self.exit_code is not None:
101 self.fail_reason = "exited with exit code %s" % self.exit_code
103 self.fail_reason = "unable to determine termination reason"
106 logging.debug("Command '%s' failed (%s); output: %s",
107 self.cmd, self.fail_reason, self.output)
109 def _GetOutput(self):
110 """Returns the combined stdout and stderr for easier usage.
113 return self.stdout + self.stderr
115 output = property(_GetOutput, None, None, "Return full output")
118 def RunCmd(cmd, env=None, output=None, cwd='/'):
119 """Execute a (shell) command.
121 The command should not read from its standard input, as it will be
124 @type cmd: string or list
125 @param cmd: Command to run
127 @param env: Additional environment
129 @param output: if desired, the output of the command can be
130 saved in a file instead of the RunResult instance; this
131 parameter denotes the file name (if not None)
133 @param cwd: if specified, will be used as the working
134 directory for the command; the default will be /
136 @return: RunResult instance
137 @raise errors.ProgrammerError: if we call this when forks are disabled
141 raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
143 if isinstance(cmd, list):
144 cmd = [str(val) for val in cmd]
145 strcmd = " ".join(cmd)
150 logging.debug("RunCmd '%s'", strcmd)
152 cmd_env = os.environ.copy()
153 cmd_env["LC_ALL"] = "C"
159 out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd)
161 status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
164 if err.errno == errno.ENOENT:
165 raise errors.OpExecError("Can't execute '%s': not found (%s)" %
177 return RunResult(exitcode, signal_, out, err, strcmd)
180 def _RunCmdPipe(cmd, env, via_shell, cwd):
181 """Run a command and return its output.
183 @type cmd: string or list
184 @param cmd: Command to run
186 @param env: The environment to use
187 @type via_shell: bool
188 @param via_shell: if we should run via the shell
190 @param cwd: the working directory for the program
192 @return: (out, err, status)
195 poller = select.poll()
196 child = subprocess.Popen(cmd, shell=via_shell,
197 stderr=subprocess.PIPE,
198 stdout=subprocess.PIPE,
199 stdin=subprocess.PIPE,
200 close_fds=True, env=env,
204 poller.register(child.stdout, select.POLLIN)
205 poller.register(child.stderr, select.POLLIN)
209 child.stdout.fileno(): (out, child.stdout),
210 child.stderr.fileno(): (err, child.stderr),
213 status = fcntl.fcntl(fd, fcntl.F_GETFL)
214 fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
218 pollresult = poller.poll()
219 except EnvironmentError, eerr:
220 if eerr.errno == errno.EINTR:
223 except select.error, serr:
224 if serr[0] == errno.EINTR:
228 for fd, event in pollresult:
229 if event & select.POLLIN or event & select.POLLPRI:
230 data = fdmap[fd][1].read()
231 # no data from read signifies EOF (the same as POLLHUP)
233 poller.unregister(fd)
236 fdmap[fd][0].write(data)
237 if (event & select.POLLNVAL or event & select.POLLHUP or
238 event & select.POLLERR):
239 poller.unregister(fd)
245 status = child.wait()
246 return out, err, status
249 def _RunCmdFile(cmd, env, via_shell, output, cwd):
250 """Run a command and save its output to a file.
252 @type cmd: string or list
253 @param cmd: Command to run
255 @param env: The environment to use
256 @type via_shell: bool
257 @param via_shell: if we should run via the shell
259 @param output: the filename in which to save the output
261 @param cwd: the working directory for the program
263 @return: the exit status
266 fh = open(output, "a")
268 child = subprocess.Popen(cmd, shell=via_shell,
269 stderr=subprocess.STDOUT,
271 stdin=subprocess.PIPE,
272 close_fds=True, env=env,
276 status = child.wait()
282 def RemoveFile(filename):
283 """Remove a file ignoring some errors.
285 Remove a file, ignoring non-existing ones or directories. Other
289 @param filename: the file to be removed
295 if err.errno not in (errno.ENOENT, errno.EISDIR):
299 def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
303 @param old: Original path
307 @param mkdir: Whether to create target directory if it doesn't exist
308 @type mkdir_mode: int
309 @param mkdir_mode: Mode for newly created directories
313 return os.rename(old, new)
315 # In at least one use case of this function, the job queue, directory
316 # creation is very rare. Checking for the directory before renaming is not
318 if mkdir and err.errno == errno.ENOENT:
319 # Create directory and try again
320 os.makedirs(os.path.dirname(new), mkdir_mode)
321 return os.rename(old, new)
325 def _FingerprintFile(filename):
326 """Compute the fingerprint of a file.
328 If the file does not exist, a None will be returned
332 @param filename: the filename to checksum
334 @return: the hex digest of the sha checksum of the contents
338 if not (os.path.exists(filename) and os.path.isfile(filename)):
351 return fp.hexdigest()
354 def FingerprintFiles(files):
355 """Compute fingerprints for a list of files.
358 @param files: the list of filename to fingerprint
360 @return: a dictionary filename: fingerprint, holding only
366 for filename in files:
367 cksum = _FingerprintFile(filename)
369 ret[filename] = cksum
374 def ForceDictType(target, key_types, allowed_values=None):
375 """Force the values of a dict to have certain types.
378 @param target: the dict to update
379 @type key_types: dict
380 @param key_types: dict mapping target dict keys to types
381 in constants.ENFORCEABLE_TYPES
382 @type allowed_values: list
383 @keyword allowed_values: list of specially allowed values
386 if allowed_values is None:
389 if not isinstance(target, dict):
390 msg = "Expected dictionary, got '%s'" % target
391 raise errors.TypeEnforcementError(msg)
394 if key not in key_types:
395 msg = "Unknown key '%s'" % key
396 raise errors.TypeEnforcementError(msg)
398 if target[key] in allowed_values:
401 ktype = key_types[key]
402 if ktype not in constants.ENFORCEABLE_TYPES:
403 msg = "'%s' has non-enforceable type %s" % (key, ktype)
404 raise errors.ProgrammerError(msg)
406 if ktype == constants.VTYPE_STRING:
407 if not isinstance(target[key], basestring):
408 if isinstance(target[key], bool) and not target[key]:
411 msg = "'%s' (value %s) is not a valid string" % (key, target[key])
412 raise errors.TypeEnforcementError(msg)
413 elif ktype == constants.VTYPE_BOOL:
414 if isinstance(target[key], basestring) and target[key]:
415 if target[key].lower() == constants.VALUE_FALSE:
417 elif target[key].lower() == constants.VALUE_TRUE:
420 msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
421 raise errors.TypeEnforcementError(msg)
426 elif ktype == constants.VTYPE_SIZE:
428 target[key] = ParseUnit(target[key])
429 except errors.UnitParseError, err:
430 msg = "'%s' (value %s) is not a valid size. error: %s" % \
431 (key, target[key], err)
432 raise errors.TypeEnforcementError(msg)
433 elif ktype == constants.VTYPE_INT:
435 target[key] = int(target[key])
436 except (ValueError, TypeError):
437 msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
438 raise errors.TypeEnforcementError(msg)
441 def IsProcessAlive(pid):
442 """Check if a given pid exists on the system.
444 @note: zombie status is not handled, so zombie processes
445 will be returned as alive
447 @param pid: the process ID to check
449 @return: True if the process exists
456 os.stat("/proc/%d/status" % pid)
458 except EnvironmentError, err:
459 if err.errno in (errno.ENOENT, errno.ENOTDIR):
464 def ReadPidFile(pidfile):
465 """Read a pid from a file.
467 @type pidfile: string
468 @param pidfile: path to the file containing the pid
470 @return: The process id, if the file exists and contains a valid PID,
475 pf = open(pidfile, 'r')
476 except EnvironmentError, err:
477 if err.errno != errno.ENOENT:
478 logging.exception("Can't read pid file?!")
483 except ValueError, err:
484 logging.info("Can't parse pid file contents", exc_info=True)
490 def MatchNameComponent(key, name_list):
491 """Try to match a name against a list.
493 This function will try to match a name like test1 against a list
494 like C{['test1.example.com', 'test2.example.com', ...]}. Against
495 this list, I{'test1'} as well as I{'test1.example'} will match, but
496 not I{'test1.ex'}. A multiple match will be considered as no match
497 at all (e.g. I{'test1'} against C{['test1.example.com',
498 'test1.example.org']}).
501 @param key: the name to be searched
502 @type name_list: list
503 @param name_list: the list of strings against which to search the key
506 @return: None if there is no match I{or} if there are multiple matches,
507 otherwise the element from the list which matches
510 mo = re.compile("^%s(\..*)?$" % re.escape(key))
511 names_filtered = [name for name in name_list if mo.match(name) is not None]
512 if len(names_filtered) != 1:
514 return names_filtered[0]
518 """Class implementing resolver and hostname functionality
521 def __init__(self, name=None):
522 """Initialize the host name object.
524 If the name argument is not passed, it will use this system's
529 name = self.SysName()
532 self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
533 self.ip = self.ipaddrs[0]
536 """Returns the hostname without domain.
539 return self.name.split('.')[0]
543 """Return the current system's name.
545 This is simply a wrapper over C{socket.gethostname()}.
548 return socket.gethostname()
551 def LookupHostname(hostname):
555 @param hostname: hostname to look up
558 @return: a tuple (name, aliases, ipaddrs) as returned by
559 C{socket.gethostbyname_ex}
560 @raise errors.ResolverError: in case of errors in resolving
564 result = socket.gethostbyname_ex(hostname)
565 except socket.gaierror, err:
566 # hostname not found in DNS
567 raise errors.ResolverError(hostname, err.args[0], err.args[1])
572 def ListVolumeGroups():
573 """List volume groups and their size
577 Dictionary with keys volume name and values
578 the size of the volume
581 command = "vgs --noheadings --units m --nosuffix -o name,size"
582 result = RunCmd(command)
587 for line in result.stdout.splitlines():
589 name, size = line.split()
590 size = int(float(size))
591 except (IndexError, ValueError), err:
592 logging.error("Invalid output from vgs (%s): %s", err, line)
600 def BridgeExists(bridge):
601 """Check whether the given bridge exists in the system
604 @param bridge: the bridge name to check
606 @return: True if it does
609 return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
612 def NiceSort(name_list):
613 """Sort a list of strings based on digit and non-digit groupings.
615 Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
616 will sort the list in the logical order C{['a1', 'a2', 'a10',
619 The sort algorithm breaks each name in groups of either only-digits
620 or no-digits. Only the first eight such groups are considered, and
621 after that we just use what's left of the string.
623 @type name_list: list
624 @param name_list: the names to be sorted
626 @return: a copy of the name list sorted with our algorithm
629 _SORTER_BASE = "(\D+|\d+)"
630 _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
631 _SORTER_BASE, _SORTER_BASE,
632 _SORTER_BASE, _SORTER_BASE,
633 _SORTER_BASE, _SORTER_BASE)
634 _SORTER_RE = re.compile(_SORTER_FULL)
635 _SORTER_NODIGIT = re.compile("^\D*$")
637 """Attempts to convert a variable to integer."""
638 if val is None or _SORTER_NODIGIT.match(val):
643 to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
644 for name in name_list]
646 return [tup[1] for tup in to_sort]
649 def TryConvert(fn, val):
650 """Try to convert a value ignoring errors.
652 This function tries to apply function I{fn} to I{val}. If no
653 C{ValueError} or C{TypeError} exceptions are raised, it will return
654 the result, else it will return the original value. Any other
655 exceptions are propagated to the caller.
658 @param fn: function to apply to the value
659 @param val: the value to be converted
660 @return: The converted value if the conversion was successful,
661 otherwise the original value.
666 except (ValueError, TypeError):
672 """Verifies the syntax of an IPv4 address.
674 This function checks if the IPv4 address passes is valid or not based
675 on syntax (not IP range, class calculations, etc.).
678 @param ip: the address to be checked
679 @rtype: a regular expression match object
680 @return: a regular expression match object, or None if the
684 unit = "(0|[1-9]\d{0,2})"
685 #TODO: convert and return only boolean
686 return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
689 def IsValidShellParam(word):
690 """Verifies is the given word is safe from the shell's p.o.v.
692 This means that we can pass this to a command via the shell and be
693 sure that it doesn't alter the command line and is passed as such to
696 Note that we are overly restrictive here, in order to be on the safe
700 @param word: the word to check
702 @return: True if the word is 'safe'
705 return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
708 def BuildShellCmd(template, *args):
709 """Build a safe shell command line from the given arguments.
711 This function will check all arguments in the args list so that they
712 are valid shell parameters (i.e. they don't contain shell
713 metacharacters). If everything is ok, it will return the result of
717 @param template: the string holding the template for the
720 @return: the expanded command line
724 if not IsValidShellParam(word):
725 raise errors.ProgrammerError("Shell argument '%s' contains"
726 " invalid characters" % word)
727 return template % args
730 def FormatUnit(value, units):
731 """Formats an incoming number of MiB with the appropriate unit.
734 @param value: integer representing the value in MiB (1048576)
736 @param units: the type of formatting we should do:
737 - 'h' for automatic scaling
742 @return: the formatted value (with suffix)
745 if units not in ('m', 'g', 't', 'h'):
746 raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
750 if units == 'm' or (units == 'h' and value < 1024):
753 return "%d%s" % (round(value, 0), suffix)
755 elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
758 return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
763 return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
766 def ParseUnit(input_string):
767 """Tries to extract number and scale from the given string.
769 Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
770 [UNIT]}. If no unit is specified, it defaults to MiB. Return value
771 is always an int in MiB.
774 m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', str(input_string))
776 raise errors.UnitParseError("Invalid format")
778 value = float(m.groups()[0])
782 lcunit = unit.lower()
786 if lcunit in ('m', 'mb', 'mib'):
787 # Value already in MiB
790 elif lcunit in ('g', 'gb', 'gib'):
793 elif lcunit in ('t', 'tb', 'tib'):
797 raise errors.UnitParseError("Unknown unit: %s" % unit)
799 # Make sure we round up
800 if int(value) < value:
803 # Round up to the next multiple of 4
806 value += 4 - value % 4
811 def AddAuthorizedKey(file_name, key):
812 """Adds an SSH public key to an authorized_keys file.
815 @param file_name: path to authorized_keys file
817 @param key: string containing key
820 key_fields = key.split()
822 f = open(file_name, 'a+')
826 # Ignore whitespace changes
827 if line.split() == key_fields:
829 nl = line.endswith('\n')
833 f.write(key.rstrip('\r\n'))
840 def RemoveAuthorizedKey(file_name, key):
841 """Removes an SSH public key from an authorized_keys file.
844 @param file_name: path to authorized_keys file
846 @param key: string containing key
849 key_fields = key.split()
851 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
853 out = os.fdopen(fd, 'w')
855 f = open(file_name, 'r')
858 # Ignore whitespace changes while comparing lines
859 if line.split() != key_fields:
863 os.rename(tmpname, file_name)
873 def SetEtcHostsEntry(file_name, ip, hostname, aliases):
874 """Sets the name of an IP address and hostname in /etc/hosts.
877 @param file_name: path to the file to modify (usually C{/etc/hosts})
879 @param ip: the IP address
881 @param hostname: the hostname to be added
883 @param aliases: the list of aliases to add for the hostname
886 # FIXME: use WriteFile + fn rather than duplicating its efforts
887 # Ensure aliases are unique
888 aliases = UniqueSequence([hostname] + aliases)[1:]
890 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
892 out = os.fdopen(fd, 'w')
894 f = open(file_name, 'r')
897 fields = line.split()
898 if fields and not fields[0].startswith('#') and ip == fields[0]:
902 out.write("%s\t%s" % (ip, hostname))
904 out.write(" %s" % ' '.join(aliases))
909 os.chmod(tmpname, 0644)
910 os.rename(tmpname, file_name)
920 def AddHostToEtcHosts(hostname):
921 """Wrapper around SetEtcHostsEntry.
924 @param hostname: a hostname that will be resolved and added to
925 L{constants.ETC_HOSTS}
928 hi = HostInfo(name=hostname)
929 SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
932 def RemoveEtcHostsEntry(file_name, hostname):
933 """Removes a hostname from /etc/hosts.
935 IP addresses without names are removed from the file.
938 @param file_name: path to the file to modify (usually C{/etc/hosts})
940 @param hostname: the hostname to be removed
943 # FIXME: use WriteFile + fn rather than duplicating its efforts
944 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
946 out = os.fdopen(fd, 'w')
948 f = open(file_name, 'r')
951 fields = line.split()
952 if len(fields) > 1 and not fields[0].startswith('#'):
954 if hostname in names:
955 while hostname in names:
956 names.remove(hostname)
958 out.write("%s %s\n" % (fields[0], ' '.join(names)))
965 os.chmod(tmpname, 0644)
966 os.rename(tmpname, file_name)
976 def RemoveHostFromEtcHosts(hostname):
977 """Wrapper around RemoveEtcHostsEntry.
980 @param hostname: hostname that will be resolved and its
981 full and shot name will be removed from
982 L{constants.ETC_HOSTS}
985 hi = HostInfo(name=hostname)
986 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
987 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
990 def CreateBackup(file_name):
991 """Creates a backup of a file.
994 @param file_name: file to be backed up
996 @return: the path to the newly created backup
997 @raise errors.ProgrammerError: for invalid file names
1000 if not os.path.isfile(file_name):
1001 raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
1004 prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
1005 dir_name = os.path.dirname(file_name)
1007 fsrc = open(file_name, 'rb')
1009 (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
1010 fdst = os.fdopen(fd, 'wb')
1012 shutil.copyfileobj(fsrc, fdst)
1021 def ShellQuote(value):
1022 """Quotes shell argument according to POSIX.
1025 @param value: the argument to be quoted
1027 @return: the quoted value
1030 if _re_shell_unquoted.match(value):
1033 return "'%s'" % value.replace("'", "'\\''")
1036 def ShellQuoteArgs(args):
1037 """Quotes a list of shell arguments.
1040 @param args: list of arguments to be quoted
1042 @return: the quoted arguments concatenated with spaces
1045 return ' '.join([ShellQuote(i) for i in args])
1048 def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
1049 """Simple ping implementation using TCP connect(2).
1051 Check if the given IP is reachable by doing attempting a TCP connect
1055 @param target: the IP or hostname to ping
1057 @param port: the port to connect to
1059 @param timeout: the timeout on the connection attempt
1060 @type live_port_needed: boolean
1061 @param live_port_needed: whether a closed port will cause the
1062 function to return failure, as if there was a timeout
1063 @type source: str or None
1064 @param source: if specified, will cause the connect to be made
1065 from this specific source address; failures to bind other
1066 than C{EADDRNOTAVAIL} will be ignored
1069 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1073 if source is not None:
1075 sock.bind((source, 0))
1076 except socket.error, (errcode, _):
1077 if errcode == errno.EADDRNOTAVAIL:
1080 sock.settimeout(timeout)
1083 sock.connect((target, port))
1086 except socket.timeout:
1088 except socket.error, (errcode, errstring):
1089 success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
1094 def OwnIpAddress(address):
1095 """Check if the current host has the the given IP address.
1097 Currently this is done by TCP-pinging the address from the loopback
1100 @type address: string
1101 @param address: the address to check
1103 @return: True if we own the address
1106 return TcpPing(address, constants.DEFAULT_NODED_PORT,
1107 source=constants.LOCALHOST_IP_ADDRESS)
1110 def ListVisibleFiles(path):
1111 """Returns a list of visible files in a directory.
1114 @param path: the directory to enumerate
1116 @return: the list of all files not starting with a dot
1119 files = [i for i in os.listdir(path) if not i.startswith(".")]
1124 def GetHomeDir(user, default=None):
1125 """Try to get the homedir of the given user.
1127 The user can be passed either as a string (denoting the name) or as
1128 an integer (denoting the user id). If the user is not found, the
1129 'default' argument is returned, which defaults to None.
1133 if isinstance(user, basestring):
1134 result = pwd.getpwnam(user)
1135 elif isinstance(user, (int, long)):
1136 result = pwd.getpwuid(user)
1138 raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
1142 return result.pw_dir
1146 """Returns a random UUID.
1148 @note: This is a Linux-specific method as it uses the /proc
1153 f = open("/proc/sys/kernel/random/uuid", "r")
1155 return f.read(128).rstrip("\n")
1160 def GenerateSecret(numbytes=20):
1161 """Generates a random secret.
1163 This will generate a pseudo-random secret returning an hex string
1164 (so that it can be used where an ASCII string is needed).
1166 @param numbytes: the number of bytes which will be represented by the returned
1167 string (defaulting to 20, the length of a SHA1 hash)
1169 @return: an hex representation of the pseudo-random sequence
1172 return os.urandom(numbytes).encode('hex')
1175 def EnsureDirs(dirs):
1176 """Make required directories, if they don't exist.
1178 @param dirs: list of tuples (dir_name, dir_mode)
1179 @type dirs: list of (string, integer)
1182 for dir_name, dir_mode in dirs:
1184 os.mkdir(dir_name, dir_mode)
1185 except EnvironmentError, err:
1186 if err.errno != errno.EEXIST:
1187 raise errors.GenericError("Cannot create needed directory"
1188 " '%s': %s" % (dir_name, err))
1189 if not os.path.isdir(dir_name):
1190 raise errors.GenericError("%s is not a directory" % dir_name)
1193 def ReadFile(file_name, size=None):
1196 @type size: None or int
1197 @param size: Read at most size bytes
1199 @return: the (possibly partial) content of the file
1202 f = open(file_name, "r")
1212 def WriteFile(file_name, fn=None, data=None,
1213 mode=None, uid=-1, gid=-1,
1214 atime=None, mtime=None, close=True,
1215 dry_run=False, backup=False,
1216 prewrite=None, postwrite=None):
1217 """(Over)write a file atomically.
1219 The file_name and either fn (a function taking one argument, the
1220 file descriptor, and which should write the data to it) or data (the
1221 contents of the file) must be passed. The other arguments are
1222 optional and allow setting the file mode, owner and group, and the
1223 mtime/atime of the file.
1225 If the function doesn't raise an exception, it has succeeded and the
1226 target file has the new contents. If the function has raised an
1227 exception, an existing target file should be unmodified and the
1228 temporary file should be removed.
1230 @type file_name: str
1231 @param file_name: the target filename
1233 @param fn: content writing function, called with
1234 file descriptor as parameter
1236 @param data: contents of the file
1238 @param mode: file mode
1240 @param uid: the owner of the file
1242 @param gid: the group of the file
1244 @param atime: a custom access time to be set on the file
1246 @param mtime: a custom modification time to be set on the file
1247 @type close: boolean
1248 @param close: whether to close file after writing it
1249 @type prewrite: callable
1250 @param prewrite: function to be called before writing content
1251 @type postwrite: callable
1252 @param postwrite: function to be called after writing content
1255 @return: None if the 'close' parameter evaluates to True,
1256 otherwise the file descriptor
1258 @raise errors.ProgrammerError: if any of the arguments are not valid
1261 if not os.path.isabs(file_name):
1262 raise errors.ProgrammerError("Path passed to WriteFile is not"
1263 " absolute: '%s'" % file_name)
1265 if [fn, data].count(None) != 1:
1266 raise errors.ProgrammerError("fn or data required")
1268 if [atime, mtime].count(None) == 1:
1269 raise errors.ProgrammerError("Both atime and mtime must be either"
1272 if backup and not dry_run and os.path.isfile(file_name):
1273 CreateBackup(file_name)
1275 dir_name, base_name = os.path.split(file_name)
1276 fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1278 # here we need to make sure we remove the temp file, if any error
1279 # leaves it in place
1281 if uid != -1 or gid != -1:
1282 os.chown(new_name, uid, gid)
1284 os.chmod(new_name, mode)
1285 if callable(prewrite):
1287 if data is not None:
1291 if callable(postwrite):
1294 if atime is not None and mtime is not None:
1295 os.utime(new_name, (atime, mtime))
1297 os.rename(new_name, file_name)
1306 RemoveFile(new_name)
1311 def FirstFree(seq, base=0):
1312 """Returns the first non-existing integer from seq.
1314 The seq argument should be a sorted list of positive integers. The
1315 first time the index of an element is smaller than the element
1316 value, the index will be returned.
1318 The base argument is used to start at a different offset,
1319 i.e. C{[3, 4, 6]} with I{offset=3} will return 5.
1321 Example: C{[0, 1, 3]} will return I{2}.
1324 @param seq: the sequence to be analyzed.
1326 @param base: use this value as the base index of the sequence
1328 @return: the first non-used index in the sequence
1331 for idx, elem in enumerate(seq):
1332 assert elem >= base, "Passed element is higher than base offset"
1333 if elem > idx + base:
1339 def all(seq, pred=bool):
1340 "Returns True if pred(x) is True for every element in the iterable"
1341 for _ in itertools.ifilterfalse(pred, seq):
1346 def any(seq, pred=bool):
1347 "Returns True if pred(x) is True for at least one element in the iterable"
1348 for _ in itertools.ifilter(pred, seq):
1353 def UniqueSequence(seq):
1354 """Returns a list with unique elements.
1356 Element order is preserved.
1359 @param seq: the sequence with the source elements
1361 @return: list of unique elements from seq
1365 return [i for i in seq if i not in seen and not seen.add(i)]
1368 def IsValidMac(mac):
1369 """Predicate to check if a MAC address is valid.
1371 Checks whether the supplied MAC address is formally correct, only
1372 accepts colon separated format.
1375 @param mac: the MAC to be validated
1377 @return: True is the MAC seems valid
1380 mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
1381 return mac_check.match(mac) is not None
1384 def TestDelay(duration):
1385 """Sleep for a fixed amount of time.
1387 @type duration: float
1388 @param duration: the sleep duration
1390 @return: False for negative value, True otherwise
1394 return False, "Invalid sleep duration"
1395 time.sleep(duration)
1399 def _CloseFDNoErr(fd, retries=5):
1400 """Close a file descriptor ignoring errors.
1403 @param fd: the file descriptor
1405 @param retries: how many retries to make, in case we get any
1406 other error than EBADF
1411 except OSError, err:
1412 if err.errno != errno.EBADF:
1414 _CloseFDNoErr(fd, retries - 1)
1415 # else either it's closed already or we're out of retries, so we
1416 # ignore this and go on
1419 def CloseFDs(noclose_fds=None):
1420 """Close file descriptors.
1422 This closes all file descriptors above 2 (i.e. except
1425 @type noclose_fds: list or None
1426 @param noclose_fds: if given, it denotes a list of file descriptor
1427 that should not be closed
1430 # Default maximum for the number of available file descriptors.
1431 if 'SC_OPEN_MAX' in os.sysconf_names:
1433 MAXFD = os.sysconf('SC_OPEN_MAX')
1440 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1441 if (maxfd == resource.RLIM_INFINITY):
1444 # Iterate through and close all file descriptors (except the standard ones)
1445 for fd in range(3, maxfd):
1446 if noclose_fds and fd in noclose_fds:
1451 def Daemonize(logfile):
1452 """Daemonize the current process.
1454 This detaches the current process from the controlling terminal and
1455 runs it in the background as a daemon.
1458 @param logfile: the logfile to which we should redirect stdout/stderr
1460 @return: the value zero
1468 if (pid == 0): # The first child.
1471 pid = os.fork() # Fork a second child.
1472 if (pid == 0): # The second child.
1476 # exit() or _exit()? See below.
1477 os._exit(0) # Exit parent (the first child) of the second child.
1479 os._exit(0) # Exit parent of the first child.
1483 i = os.open("/dev/null", os.O_RDONLY) # stdin
1484 assert i == 0, "Can't close/reopen stdin"
1485 i = os.open(logfile, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0600) # stdout
1486 assert i == 1, "Can't close/reopen stdout"
1487 # Duplicate standard output to standard error.
1492 def DaemonPidFileName(name):
1493 """Compute a ganeti pid file absolute path
1496 @param name: the daemon name
1498 @return: the full path to the pidfile corresponding to the given
1502 return os.path.join(constants.RUN_GANETI_DIR, "%s.pid" % name)
1505 def WritePidFile(name):
1506 """Write the current process pidfile.
1508 The file will be written to L{constants.RUN_GANETI_DIR}I{/name.pid}
1511 @param name: the daemon name to use
1512 @raise errors.GenericError: if the pid file already exists and
1513 points to a live process
1517 pidfilename = DaemonPidFileName(name)
1518 if IsProcessAlive(ReadPidFile(pidfilename)):
1519 raise errors.GenericError("%s contains a live process" % pidfilename)
1521 WriteFile(pidfilename, data="%d\n" % pid)
1524 def RemovePidFile(name):
1525 """Remove the current process pidfile.
1527 Any errors are ignored.
1530 @param name: the daemon name used to derive the pidfile name
1533 pidfilename = DaemonPidFileName(name)
1534 # TODO: we could check here that the file contains our pid
1536 RemoveFile(pidfilename)
1541 def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1543 """Kill a process given by its pid.
1546 @param pid: The PID to terminate.
1548 @param signal_: The signal to send, by default SIGTERM
1550 @param timeout: The timeout after which, if the process is still alive,
1551 a SIGKILL will be sent. If not positive, no such checking
1553 @type waitpid: boolean
1554 @param waitpid: If true, we should waitpid on this process after
1555 sending signals, since it's our own child and otherwise it
1556 would remain as zombie
1559 def _helper(pid, signal_, wait):
1560 """Simple helper to encapsulate the kill/waitpid sequence"""
1561 os.kill(pid, signal_)
1564 os.waitpid(pid, os.WNOHANG)
1569 # kill with pid=0 == suicide
1570 raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1572 if not IsProcessAlive(pid):
1574 _helper(pid, signal_, waitpid)
1578 # Wait up to $timeout seconds
1579 end = time.time() + timeout
1581 while time.time() < end and IsProcessAlive(pid):
1583 (result_pid, _) = os.waitpid(pid, os.WNOHANG)
1589 # Make wait time longer for next try
1593 if IsProcessAlive(pid):
1594 # Kill process if it's still alive
1595 _helper(pid, signal.SIGKILL, waitpid)
1598 def FindFile(name, search_path, test=os.path.exists):
1599 """Look for a filesystem object in a given path.
1601 This is an abstract method to search for filesystem object (files,
1602 dirs) under a given search path.
1605 @param name: the name to look for
1606 @type search_path: str
1607 @param search_path: location to start at
1608 @type test: callable
1609 @param test: a function taking one argument that should return True
1610 if the a given object is valid; the default value is
1611 os.path.exists, causing only existing files to be returned
1613 @return: full path to the object if found, None otherwise
1616 for dir_name in search_path:
1617 item_name = os.path.sep.join([dir_name, name])
1623 def CheckVolumeGroupSize(vglist, vgname, minsize):
1624 """Checks if the volume group list is valid.
1626 The function will check if a given volume group is in the list of
1627 volume groups and has a minimum size.
1630 @param vglist: dictionary of volume group names and their size
1632 @param vgname: the volume group we should check
1634 @param minsize: the minimum size we accept
1636 @return: None for success, otherwise the error message
1639 vgsize = vglist.get(vgname, None)
1641 return "volume group '%s' missing" % vgname
1642 elif vgsize < minsize:
1643 return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1644 (vgname, minsize, vgsize))
1648 def SplitTime(value):
1649 """Splits time as floating point number into a tuple.
1651 @param value: Time in seconds
1652 @type value: int or float
1653 @return: Tuple containing (seconds, microseconds)
1656 (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
1658 assert 0 <= seconds, \
1659 "Seconds must be larger than or equal to 0, but are %s" % seconds
1660 assert 0 <= microseconds <= 999999, \
1661 "Microseconds must be 0-999999, but are %s" % microseconds
1663 return (int(seconds), int(microseconds))
1666 def MergeTime(timetuple):
1667 """Merges a tuple into time as a floating point number.
1669 @param timetuple: Time as tuple, (seconds, microseconds)
1670 @type timetuple: tuple
1671 @return: Time as a floating point number expressed in seconds
1674 (seconds, microseconds) = timetuple
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 float(seconds) + (float(microseconds) * 0.000001)
1684 def GetDaemonPort(daemon_name):
1685 """Get the daemon port for this cluster.
1687 Note that this routine does not read a ganeti-specific file, but
1688 instead uses C{socket.getservbyname} to allow pre-customization of
1689 this parameter outside of Ganeti.
1691 @type daemon_name: string
1692 @param daemon_name: daemon name (in constants.DAEMONS_PORTS)
1696 if daemon_name not in constants.DAEMONS_PORTS:
1697 raise errors.ProgrammerError("Unknown daemon: %s" % daemon_name)
1699 (proto, default_port) = constants.DAEMONS_PORTS[daemon_name]
1701 port = socket.getservbyname(daemon_name, proto)
1702 except socket.error:
1708 def SetupLogging(logfile, debug=False, stderr_logging=False, program="",
1709 multithreaded=False):
1710 """Configures the logging module.
1713 @param logfile: the filename to which we should log
1714 @type debug: boolean
1715 @param debug: whether to enable debug messages too or
1716 only those at C{INFO} and above level
1717 @type stderr_logging: boolean
1718 @param stderr_logging: whether we should also log to the standard error
1720 @param program: the name under which we should log messages
1721 @type multithreaded: boolean
1722 @param multithreaded: if True, will add the thread name to the log file
1723 @raise EnvironmentError: if we can't open the log file and
1724 stderr logging is disabled
1727 fmt = "%(asctime)s: " + program + " pid=%(process)d"
1729 fmt += "/%(threadName)s"
1731 fmt += " %(module)s:%(lineno)s"
1732 fmt += " %(levelname)s %(message)s"
1733 formatter = logging.Formatter(fmt)
1735 root_logger = logging.getLogger("")
1736 root_logger.setLevel(logging.NOTSET)
1738 # Remove all previously setup handlers
1739 for handler in root_logger.handlers:
1741 root_logger.removeHandler(handler)
1744 stderr_handler = logging.StreamHandler()
1745 stderr_handler.setFormatter(formatter)
1747 stderr_handler.setLevel(logging.NOTSET)
1749 stderr_handler.setLevel(logging.CRITICAL)
1750 root_logger.addHandler(stderr_handler)
1752 # this can fail, if the logging directories are not setup or we have
1753 # a permisssion problem; in this case, it's best to log but ignore
1754 # the error if stderr_logging is True, and if false we re-raise the
1755 # exception since otherwise we could run but without any logs at all
1757 logfile_handler = logging.FileHandler(logfile)
1758 logfile_handler.setFormatter(formatter)
1760 logfile_handler.setLevel(logging.DEBUG)
1762 logfile_handler.setLevel(logging.INFO)
1763 root_logger.addHandler(logfile_handler)
1764 except EnvironmentError:
1766 logging.exception("Failed to enable logging to file '%s'", logfile)
1768 # we need to re-raise the exception
1771 def IsNormAbsPath(path):
1772 """Check whether a path is absolute and also normalized
1774 This avoids things like /dir/../../other/path to be valid.
1777 return os.path.normpath(path) == path and os.path.isabs(path)
1779 def TailFile(fname, lines=20):
1780 """Return the last lines from a file.
1782 @note: this function will only read and parse the last 4KB of
1783 the file; if the lines are very long, it could be that less
1784 than the requested number of lines are returned
1786 @param fname: the file name
1788 @param lines: the (maximum) number of lines to return
1791 fd = open(fname, "r")
1795 pos = max(0, pos-4096)
1797 raw_data = fd.read()
1801 rows = raw_data.splitlines()
1802 return rows[-lines:]
1805 def SafeEncode(text):
1806 """Return a 'safe' version of a source string.
1808 This function mangles the input string and returns a version that
1809 should be safe to display/encode as ASCII. To this end, we first
1810 convert it to ASCII using the 'backslashreplace' encoding which
1811 should get rid of any non-ASCII chars, and then we process it
1812 through a loop copied from the string repr sources in the python; we
1813 don't use string_escape anymore since that escape single quotes and
1814 backslashes too, and that is too much; and that escaping is not
1815 stable, i.e. string_escape(string_escape(x)) != string_escape(x).
1817 @type text: str or unicode
1818 @param text: input data
1820 @return: a safe version of text
1823 if isinstance(text, unicode):
1824 # only if unicode; if str already, we handle it below
1825 text = text.encode('ascii', 'backslashreplace')
1835 elif c < 32 or c >= 127: # non-printable
1836 resu += "\\x%02x" % (c & 0xff)
1842 def CommaJoin(names):
1843 """Nicely join a set of identifiers.
1845 @param names: set, list or tuple
1846 @return: a string with the formatted results
1849 return ", ".join(["'%s'" % val for val in names])
1852 def BytesToMebibyte(value):
1853 """Converts bytes to mebibytes.
1856 @param value: Value in bytes
1858 @return: Value in mebibytes
1861 return int(round(value / (1024.0 * 1024.0), 0))
1864 def CalculateDirectorySize(path):
1865 """Calculates the size of a directory recursively.
1868 @param path: Path to directory
1870 @return: Size in mebibytes
1875 for (curpath, _, files) in os.walk(path):
1877 st = os.lstat(os.path.join(curpath, file))
1880 return BytesToMebibyte(size)
1883 def GetFreeFilesystemSpace(path):
1884 """Returns the free space on a filesystem.
1887 @param path: Path on filesystem to be examined
1889 @return: Free space in mebibytes
1892 st = os.statvfs(path)
1894 return BytesToMebibyte(st.f_bavail * st.f_frsize)
1897 def LockedMethod(fn):
1898 """Synchronized object access decorator.
1900 This decorator is intended to protect access to an object using the
1901 object's own lock which is hardcoded to '_lock'.
1904 def _LockDebug(*args, **kwargs):
1906 logging.debug(*args, **kwargs)
1908 def wrapper(self, *args, **kwargs):
1909 assert hasattr(self, '_lock')
1911 _LockDebug("Waiting for %s", lock)
1914 _LockDebug("Acquired %s", lock)
1915 result = fn(self, *args, **kwargs)
1917 _LockDebug("Releasing %s", lock)
1919 _LockDebug("Released %s", lock)
1925 """Locks a file using POSIX locks.
1928 @param fd: the file descriptor we need to lock
1932 fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1933 except IOError, err:
1934 if err.errno == errno.EAGAIN:
1935 raise errors.LockError("File already locked")
1939 def FormatTime(val):
1940 """Formats a time value.
1942 @type val: float or None
1943 @param val: the timestamp as returned by time.time()
1944 @return: a string value or N/A if we don't have a valid timestamp
1947 if val is None or not isinstance(val, (int, float)):
1949 # these two codes works on Linux, but they are not guaranteed on all
1951 return time.strftime("%F %T", time.localtime(val))
1954 def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
1955 """Reads the watcher pause file.
1957 @type filename: string
1958 @param filename: Path to watcher pause file
1959 @type now: None, float or int
1960 @param now: Current time as Unix timestamp
1961 @type remove_after: int
1962 @param remove_after: Remove watcher pause file after specified amount of
1963 seconds past the pause end time
1970 value = ReadFile(filename)
1971 except IOError, err:
1972 if err.errno != errno.ENOENT:
1976 if value is not None:
1980 logging.warning(("Watcher pause file (%s) contains invalid value,"
1981 " removing it"), filename)
1982 RemoveFile(filename)
1985 if value is not None:
1986 # Remove file if it's outdated
1987 if now > (value + remove_after):
1988 RemoveFile(filename)
1997 class FileLock(object):
1998 """Utility class for file locks.
2001 def __init__(self, filename):
2002 """Constructor for FileLock.
2004 This will open the file denoted by the I{filename} argument.
2007 @param filename: path to the file to be locked
2010 self.filename = filename
2011 self.fd = open(self.filename, "w")
2017 """Close the file and release the lock.
2024 def _flock(self, flag, blocking, timeout, errmsg):
2025 """Wrapper for fcntl.flock.
2028 @param flag: operation flag
2029 @type blocking: bool
2030 @param blocking: whether the operation should be done in blocking mode.
2031 @type timeout: None or float
2032 @param timeout: for how long the operation should be retried (implies
2034 @type errmsg: string
2035 @param errmsg: error message in case operation fails.
2038 assert self.fd, "Lock was closed"
2039 assert timeout is None or timeout >= 0, \
2040 "If specified, timeout must be positive"
2042 if timeout is not None:
2043 flag |= fcntl.LOCK_NB
2044 timeout_end = time.time() + timeout
2046 # Blocking doesn't have effect with timeout
2048 flag |= fcntl.LOCK_NB
2054 fcntl.flock(self.fd, flag)
2056 except IOError, err:
2057 if err.errno in (errno.EAGAIN, ):
2058 if timeout_end is not None and time.time() < timeout_end:
2059 # Wait before trying again
2060 time.sleep(max(0.1, min(1.0, timeout)))
2062 raise errors.LockError(errmsg)
2064 logging.exception("fcntl.flock failed")
2067 def Exclusive(self, blocking=False, timeout=None):
2068 """Locks the file in exclusive mode.
2070 @type blocking: boolean
2071 @param blocking: whether to block and wait until we
2072 can lock the file or return immediately
2073 @type timeout: int or None
2074 @param timeout: if not None, the duration to wait for the lock
2078 self._flock(fcntl.LOCK_EX, blocking, timeout,
2079 "Failed to lock %s in exclusive mode" % self.filename)
2081 def Shared(self, blocking=False, timeout=None):
2082 """Locks the file in shared mode.
2084 @type blocking: boolean
2085 @param blocking: whether to block and wait until we
2086 can lock the file or return immediately
2087 @type timeout: int or None
2088 @param timeout: if not None, the duration to wait for the lock
2092 self._flock(fcntl.LOCK_SH, blocking, timeout,
2093 "Failed to lock %s in shared mode" % self.filename)
2095 def Unlock(self, blocking=True, timeout=None):
2096 """Unlocks the file.
2098 According to C{flock(2)}, unlocking can also be a nonblocking
2101 To make a non-blocking request, include LOCK_NB with any of the above
2104 @type blocking: boolean
2105 @param blocking: whether to block and wait until we
2106 can lock the file or return immediately
2107 @type timeout: int or None
2108 @param timeout: if not None, the duration to wait for the lock
2112 self._flock(fcntl.LOCK_UN, blocking, timeout,
2113 "Failed to unlock %s" % self.filename)
2116 def SignalHandled(signums):
2117 """Signal Handled decoration.
2119 This special decorator installs a signal handler and then calls the target
2120 function. The function must accept a 'signal_handlers' keyword argument,
2121 which will contain a dict indexed by signal number, with SignalHandler
2124 The decorator can be safely stacked with iself, to handle multiple signals
2125 with different handlers.
2128 @param signums: signals to intercept
2132 def sig_function(*args, **kwargs):
2133 assert 'signal_handlers' not in kwargs or \
2134 kwargs['signal_handlers'] is None or \
2135 isinstance(kwargs['signal_handlers'], dict), \
2136 "Wrong signal_handlers parameter in original function call"
2137 if 'signal_handlers' in kwargs and kwargs['signal_handlers'] is not None:
2138 signal_handlers = kwargs['signal_handlers']
2140 signal_handlers = {}
2141 kwargs['signal_handlers'] = signal_handlers
2142 sighandler = SignalHandler(signums)
2145 signal_handlers[sig] = sighandler
2146 return fn(*args, **kwargs)
2153 class SignalHandler(object):
2154 """Generic signal handler class.
2156 It automatically restores the original handler when deconstructed or
2157 when L{Reset} is called. You can either pass your own handler
2158 function in or query the L{called} attribute to detect whether the
2162 @ivar signum: the signals we handle
2163 @type called: boolean
2164 @ivar called: tracks whether any of the signals have been raised
2167 def __init__(self, signum):
2168 """Constructs a new SignalHandler instance.
2170 @type signum: int or list of ints
2171 @param signum: Single signal number or set of signal numbers
2174 self.signum = set(signum)
2179 for signum in self.signum:
2181 prev_handler = signal.signal(signum, self._HandleSignal)
2183 self._previous[signum] = prev_handler
2185 # Restore previous handler
2186 signal.signal(signum, prev_handler)
2189 # Reset all handlers
2191 # Here we have a race condition: a handler may have already been called,
2192 # but there's not much we can do about it at this point.
2199 """Restore previous handler.
2201 This will reset all the signals to their previous handlers.
2204 for signum, prev_handler in self._previous.items():
2205 signal.signal(signum, prev_handler)
2206 # If successful, remove from dict
2207 del self._previous[signum]
2210 """Unsets the L{called} flag.
2212 This function can be used in case a signal may arrive several times.
2217 def _HandleSignal(self, signum, frame):
2218 """Actual signal handling function.
2221 # This is not nice and not absolutely atomic, but it appears to be the only
2222 # solution in Python -- there are no atomic types.
2226 class FieldSet(object):
2227 """A simple field set.
2229 Among the features are:
2230 - checking if a string is among a list of static string or regex objects
2231 - checking if a whole list of string matches
2232 - returning the matching groups from a regex match
2234 Internally, all fields are held as regular expression objects.
2237 def __init__(self, *items):
2238 self.items = [re.compile("^%s$" % value) for value in items]
2240 def Extend(self, other_set):
2241 """Extend the field set with the items from another one"""
2242 self.items.extend(other_set.items)
2244 def Matches(self, field):
2245 """Checks if a field matches the current set
2248 @param field: the string to match
2249 @return: either False or a regular expression match object
2252 for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
2256 def NonMatching(self, items):
2257 """Returns the list of fields not matching the current set
2260 @param items: the list of fields to check
2262 @return: list of non-matching fields
2265 return [val for val in items if not self.Matches(val)]