4 # Copyright (C) 2006, 2007 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Ganeti small utilities
44 from cStringIO import StringIO
46 from ganeti import errors
47 from ganeti import constants
51 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
57 class RunResult(object):
58 """Simple class for holding the result of running external programs.
61 exit_code: the exit code of the program, or None (if the program
63 signal: numeric signal that caused the program to finish, or None
64 (if the program wasn't terminated by a signal)
65 stdout: the standard output of the program
66 stderr: the standard error of the program
67 failed: a Boolean value which is True in case the program was
68 terminated by a signal or exited with a non-zero exit code
69 fail_reason: a string detailing the termination reason
72 __slots__ = ["exit_code", "signal", "stdout", "stderr",
73 "failed", "fail_reason", "cmd"]
76 def __init__(self, exit_code, signal, stdout, stderr, cmd):
78 self.exit_code = exit_code
82 self.failed = (signal is not None or exit_code != 0)
84 if self.signal is not None:
85 self.fail_reason = "terminated by signal %s" % self.signal
86 elif self.exit_code is not None:
87 self.fail_reason = "exited with exit code %s" % self.exit_code
89 self.fail_reason = "unable to determine termination reason"
92 logging.debug("Command '%s' failed (%s); output: %s",
93 self.cmd, self.fail_reason, self.output)
96 """Returns the combined stdout and stderr for easier usage.
99 return self.stdout + self.stderr
101 output = property(_GetOutput, None, None, "Return full output")
105 """Execute a (shell) command.
107 The command should not read from its standard input, as it will be
111 cmd: command to run. (str)
113 Returns: `RunResult` instance
117 raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
119 if isinstance(cmd, list):
120 cmd = [str(val) for val in cmd]
121 strcmd = " ".join(cmd)
126 logging.debug("RunCmd '%s'", strcmd)
127 env = os.environ.copy()
129 poller = select.poll()
130 child = subprocess.Popen(cmd, shell=shell,
131 stderr=subprocess.PIPE,
132 stdout=subprocess.PIPE,
133 stdin=subprocess.PIPE,
134 close_fds=True, env=env)
137 poller.register(child.stdout, select.POLLIN)
138 poller.register(child.stderr, select.POLLIN)
142 child.stdout.fileno(): (out, child.stdout),
143 child.stderr.fileno(): (err, child.stderr),
146 status = fcntl.fcntl(fd, fcntl.F_GETFL)
147 fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
150 for fd, event in poller.poll():
151 if event & select.POLLIN or event & select.POLLPRI:
152 data = fdmap[fd][1].read()
153 # no data from read signifies EOF (the same as POLLHUP)
155 poller.unregister(fd)
158 fdmap[fd][0].write(data)
159 if (event & select.POLLNVAL or event & select.POLLHUP or
160 event & select.POLLERR):
161 poller.unregister(fd)
167 status = child.wait()
175 return RunResult(exitcode, signal, out, err, strcmd)
178 def RemoveFile(filename):
179 """Remove a file ignoring some errors.
181 Remove a file, ignoring non-existing ones or directories. Other
188 if err.errno not in (errno.ENOENT, errno.EISDIR):
192 def _FingerprintFile(filename):
193 """Compute the fingerprint of a file.
195 If the file does not exist, a None will be returned
199 filename - Filename (str)
202 if not (os.path.exists(filename) and os.path.isfile(filename)):
215 return fp.hexdigest()
218 def FingerprintFiles(files):
219 """Compute fingerprints for a list of files.
222 files - array of filenames. ( [str, ...] )
225 dictionary of filename: fingerprint for the files that exist
230 for filename in files:
231 cksum = _FingerprintFile(filename)
233 ret[filename] = cksum
238 def CheckDict(target, template, logname=None):
239 """Ensure a dictionary has a required set of keys.
241 For the given dictionaries `target` and `template`, ensure target
242 has all the keys from template. Missing keys are added with values
246 target - the dictionary to check
247 template - template dictionary
248 logname - a caller-chosen string to identify the debug log
249 entry; if None, no logging will be done
259 target[k] = template[k]
261 if missing and logname:
262 logging.warning('%s missing keys %s', logname, ', '.join(missing))
265 def IsProcessAlive(pid):
266 """Check if a given pid exists on the system.
268 Returns: true or false, depending on if the pid exists or not
270 Remarks: zombie processes treated as not alive
274 f = open("/proc/%d/status" % pid)
276 if err.errno in (errno.ENOENT, errno.ENOTDIR):
283 state = data[1].split()
284 if len(state) > 1 and state[1] == "Z":
292 def MatchNameComponent(key, name_list):
293 """Try to match a name against a list.
295 This function will try to match a name like test1 against a list
296 like ['test1.example.com', 'test2.example.com', ...]. Against this
297 list, 'test1' as well as 'test1.example' will match, but not
298 'test1.ex'. A multiple match will be considered as no match at all
299 (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
302 key: the name to be searched
303 name_list: the list of strings against which to search the key
306 None if there is no match *or* if there are multiple matches
307 otherwise the element from the list which matches
310 mo = re.compile("^%s(\..*)?$" % re.escape(key))
311 names_filtered = [name for name in name_list if mo.match(name) is not None]
312 if len(names_filtered) != 1:
314 return names_filtered[0]
318 """Class implementing resolver and hostname functionality
321 def __init__(self, name=None):
322 """Initialize the host name object.
324 If the name argument is not passed, it will use this system's
329 name = self.SysName()
332 self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
333 self.ip = self.ipaddrs[0]
336 """Returns the hostname without domain.
339 return self.name.split('.')[0]
343 """Return the current system's name.
345 This is simply a wrapper over socket.gethostname()
348 return socket.gethostname()
351 def LookupHostname(hostname):
355 hostname: hostname to look up
358 a tuple (name, aliases, ipaddrs) as returned by socket.gethostbyname_ex
359 in case of errors in resolving, we raise a ResolverError
363 result = socket.gethostbyname_ex(hostname)
364 except socket.gaierror, err:
365 # hostname not found in DNS
366 raise errors.ResolverError(hostname, err.args[0], err.args[1])
371 def ListVolumeGroups():
372 """List volume groups and their size
375 Dictionary with keys volume name and values the size of the volume
378 command = "vgs --noheadings --units m --nosuffix -o name,size"
379 result = RunCmd(command)
384 for line in result.stdout.splitlines():
386 name, size = line.split()
387 size = int(float(size))
388 except (IndexError, ValueError), err:
389 logging.error("Invalid output from vgs (%s): %s", err, line)
397 def BridgeExists(bridge):
398 """Check whether the given bridge exists in the system
401 True if it does, false otherwise.
404 return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
407 def NiceSort(name_list):
408 """Sort a list of strings based on digit and non-digit groupings.
410 Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
411 sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
413 The sort algorithm breaks each name in groups of either only-digits
414 or no-digits. Only the first eight such groups are considered, and
415 after that we just use what's left of the string.
418 - a copy of the list sorted according to our algorithm
421 _SORTER_BASE = "(\D+|\d+)"
422 _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
423 _SORTER_BASE, _SORTER_BASE,
424 _SORTER_BASE, _SORTER_BASE,
425 _SORTER_BASE, _SORTER_BASE)
426 _SORTER_RE = re.compile(_SORTER_FULL)
427 _SORTER_NODIGIT = re.compile("^\D*$")
429 """Attempts to convert a variable to integer."""
430 if val is None or _SORTER_NODIGIT.match(val):
435 to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
436 for name in name_list]
438 return [tup[1] for tup in to_sort]
441 def TryConvert(fn, val):
442 """Try to convert a value ignoring errors.
444 This function tries to apply function `fn` to `val`. If no
445 ValueError or TypeError exceptions are raised, it will return the
446 result, else it will return the original value. Any other exceptions
447 are propagated to the caller.
452 except (ValueError, TypeError), err:
458 """Verifies the syntax of an IP address.
460 This function checks if the ip address passes is valid or not based
461 on syntax (not ip range, class calculations or anything).
464 unit = "(0|[1-9]\d{0,2})"
465 return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
468 def IsValidShellParam(word):
469 """Verifies is the given word is safe from the shell's p.o.v.
471 This means that we can pass this to a command via the shell and be
472 sure that it doesn't alter the command line and is passed as such to
475 Note that we are overly restrictive here, in order to be on the safe
479 return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
482 def BuildShellCmd(template, *args):
483 """Build a safe shell command line from the given arguments.
485 This function will check all arguments in the args list so that they
486 are valid shell parameters (i.e. they don't contain shell
487 metacharaters). If everything is ok, it will return the result of
492 if not IsValidShellParam(word):
493 raise errors.ProgrammerError("Shell argument '%s' contains"
494 " invalid characters" % word)
495 return template % args
498 def FormatUnit(value):
499 """Formats an incoming number of MiB with the appropriate unit.
501 Value needs to be passed as a numeric type. Return value is always a string.
505 return "%dM" % round(value, 0)
507 elif value < (1024 * 1024):
508 return "%0.1fG" % round(float(value) / 1024, 1)
511 return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
514 def ParseUnit(input_string):
515 """Tries to extract number and scale from the given string.
517 Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
518 is specified, it defaults to MiB. Return value is always an int in MiB.
521 m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
523 raise errors.UnitParseError("Invalid format")
525 value = float(m.groups()[0])
529 lcunit = unit.lower()
533 if lcunit in ('m', 'mb', 'mib'):
534 # Value already in MiB
537 elif lcunit in ('g', 'gb', 'gib'):
540 elif lcunit in ('t', 'tb', 'tib'):
544 raise errors.UnitParseError("Unknown unit: %s" % unit)
546 # Make sure we round up
547 if int(value) < value:
550 # Round up to the next multiple of 4
553 value += 4 - value % 4
558 def AddAuthorizedKey(file_name, key):
559 """Adds an SSH public key to an authorized_keys file.
562 file_name: Path to authorized_keys file
563 key: String containing key
565 key_fields = key.split()
567 f = open(file_name, 'a+')
571 # Ignore whitespace changes
572 if line.split() == key_fields:
574 nl = line.endswith('\n')
578 f.write(key.rstrip('\r\n'))
585 def RemoveAuthorizedKey(file_name, key):
586 """Removes an SSH public key from an authorized_keys file.
589 file_name: Path to authorized_keys file
590 key: String containing key
592 key_fields = key.split()
594 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
596 out = os.fdopen(fd, 'w')
598 f = open(file_name, 'r')
601 # Ignore whitespace changes while comparing lines
602 if line.split() != key_fields:
606 os.rename(tmpname, file_name)
616 def SetEtcHostsEntry(file_name, ip, hostname, aliases):
617 """Sets the name of an IP address and hostname in /etc/hosts.
620 # Ensure aliases are unique
621 aliases = UniqueSequence([hostname] + aliases)[1:]
623 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
625 out = os.fdopen(fd, 'w')
627 f = open(file_name, 'r')
631 fields = line.split()
632 if fields and not fields[0].startswith('#') and ip == fields[0]:
636 out.write("%s\t%s" % (ip, hostname))
638 out.write(" %s" % ' '.join(aliases))
643 os.rename(tmpname, file_name)
653 def AddHostToEtcHosts(hostname):
654 """Wrapper around SetEtcHostsEntry.
657 hi = HostInfo(name=hostname)
658 SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
661 def RemoveEtcHostsEntry(file_name, hostname):
662 """Removes a hostname from /etc/hosts.
664 IP addresses without names are removed from the file.
666 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
668 out = os.fdopen(fd, 'w')
670 f = open(file_name, 'r')
673 fields = line.split()
674 if len(fields) > 1 and not fields[0].startswith('#'):
676 if hostname in names:
677 while hostname in names:
678 names.remove(hostname)
680 out.write("%s %s\n" % (fields[0], ' '.join(names)))
687 os.rename(tmpname, file_name)
697 def RemoveHostFromEtcHosts(hostname):
698 """Wrapper around RemoveEtcHostsEntry.
701 hi = HostInfo(name=hostname)
702 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
703 RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
706 def CreateBackup(file_name):
707 """Creates a backup of a file.
709 Returns: the path to the newly created backup file.
712 if not os.path.isfile(file_name):
713 raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
716 prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
717 dir_name = os.path.dirname(file_name)
719 fsrc = open(file_name, 'rb')
721 (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
722 fdst = os.fdopen(fd, 'wb')
724 shutil.copyfileobj(fsrc, fdst)
733 def ShellQuote(value):
734 """Quotes shell argument according to POSIX.
737 if _re_shell_unquoted.match(value):
740 return "'%s'" % value.replace("'", "'\\''")
743 def ShellQuoteArgs(args):
744 """Quotes all given shell arguments and concatenates using spaces.
747 return ' '.join([ShellQuote(i) for i in args])
750 def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
751 """Simple ping implementation using TCP connect(2).
753 Try to do a TCP connect(2) from an optional source IP to the
754 specified target IP and the specified target port. If the optional
755 parameter live_port_needed is set to true, requires the remote end
756 to accept the connection. The timeout is specified in seconds and
757 defaults to 10 seconds. If the source optional argument is not
758 passed, the source address selection is left to the kernel,
759 otherwise we try to connect using the passed address (failures to
760 bind other than EADDRNOTAVAIL will be ignored).
763 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
767 if source is not None:
769 sock.bind((source, 0))
770 except socket.error, (errcode, errstring):
771 if errcode == errno.EADDRNOTAVAIL:
774 sock.settimeout(timeout)
777 sock.connect((target, port))
780 except socket.timeout:
782 except socket.error, (errcode, errstring):
783 success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
788 def ListVisibleFiles(path):
789 """Returns a list of all visible files in a directory.
792 files = [i for i in os.listdir(path) if not i.startswith(".")]
797 def GetHomeDir(user, default=None):
798 """Try to get the homedir of the given user.
800 The user can be passed either as a string (denoting the name) or as
801 an integer (denoting the user id). If the user is not found, the
802 'default' argument is returned, which defaults to None.
806 if isinstance(user, basestring):
807 result = pwd.getpwnam(user)
808 elif isinstance(user, (int, long)):
809 result = pwd.getpwuid(user)
811 raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
819 """Returns a random UUID.
822 f = open("/proc/sys/kernel/random/uuid", "r")
824 return f.read(128).rstrip("\n")
829 def WriteFile(file_name, fn=None, data=None,
830 mode=None, uid=-1, gid=-1,
831 atime=None, mtime=None, close=True,
832 dry_run=False, backup=False,
833 prewrite=None, postwrite=None):
834 """(Over)write a file atomically.
836 The file_name and either fn (a function taking one argument, the
837 file descriptor, and which should write the data to it) or data (the
838 contents of the file) must be passed. The other arguments are
839 optional and allow setting the file mode, owner and group, and the
840 mtime/atime of the file.
842 If the function doesn't raise an exception, it has succeeded and the
843 target file has the new contents. If the file has raised an
844 exception, an existing target file should be unmodified and the
845 temporary file should be removed.
848 file_name: New filename
849 fn: Content writing function, called with file descriptor as parameter
850 data: Content as string
855 mtime: Modification time
856 close: Whether to close file after writing it
857 prewrite: Function object called before writing content
858 postwrite: Function object called after writing content
861 None if "close" parameter evaluates to True, otherwise file descriptor.
864 if not os.path.isabs(file_name):
865 raise errors.ProgrammerError("Path passed to WriteFile is not"
866 " absolute: '%s'" % file_name)
868 if [fn, data].count(None) != 1:
869 raise errors.ProgrammerError("fn or data required")
871 if [atime, mtime].count(None) == 1:
872 raise errors.ProgrammerError("Both atime and mtime must be either"
875 if backup and not dry_run and os.path.isfile(file_name):
876 CreateBackup(file_name)
878 dir_name, base_name = os.path.split(file_name)
879 fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
880 # here we need to make sure we remove the temp file, if any error
883 if uid != -1 or gid != -1:
884 os.chown(new_name, uid, gid)
886 os.chmod(new_name, mode)
887 if callable(prewrite):
893 if callable(postwrite):
896 if atime is not None and mtime is not None:
897 os.utime(new_name, (atime, mtime))
899 os.rename(new_name, file_name)
911 def FirstFree(seq, base=0):
912 """Returns the first non-existing integer from seq.
914 The seq argument should be a sorted list of positive integers. The
915 first time the index of an element is smaller than the element
916 value, the index will be returned.
918 The base argument is used to start at a different offset,
919 i.e. [3, 4, 6] with offset=3 will return 5.
921 Example: [0, 1, 3] will return 2.
924 for idx, elem in enumerate(seq):
925 assert elem >= base, "Passed element is higher than base offset"
926 if elem > idx + base:
932 def all(seq, pred=bool):
933 "Returns True if pred(x) is True for every element in the iterable"
934 for elem in itertools.ifilterfalse(pred, seq):
939 def any(seq, pred=bool):
940 "Returns True if pred(x) is True for at least one element in the iterable"
941 for elem in itertools.ifilter(pred, seq):
946 def UniqueSequence(seq):
947 """Returns a list with unique elements.
949 Element order is preserved.
952 return [i for i in seq if i not in seen and not seen.add(i)]
956 """Predicate to check if a MAC address is valid.
958 Checks wether the supplied MAC address is formally correct, only
959 accepts colon separated format.
961 mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
962 return mac_check.match(mac) is not None
965 def TestDelay(duration):
966 """Sleep for a fixed amount of time.
975 def Daemonize(logfile, noclose_fds=None):
976 """Daemonize the current process.
978 This detaches the current process from the controlling terminal and
979 runs it in the background as a daemon.
984 # Default maximum for the number of available file descriptors.
985 if 'SC_OPEN_MAX' in os.sysconf_names:
987 MAXFD = os.sysconf('SC_OPEN_MAX')
997 if (pid == 0): # The first child.
1000 pid = os.fork() # Fork a second child.
1001 if (pid == 0): # The second child.
1005 # exit() or _exit()? See below.
1006 os._exit(0) # Exit parent (the first child) of the second child.
1008 os._exit(0) # Exit parent of the first child.
1009 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1010 if (maxfd == resource.RLIM_INFINITY):
1013 # Iterate through and close all file descriptors.
1014 for fd in range(0, maxfd):
1015 if noclose_fds and fd in noclose_fds:
1019 except OSError: # ERROR, fd wasn't open to begin with (ignored)
1021 os.open(logfile, os.O_RDWR|os.O_CREAT|os.O_APPEND, 0600)
1022 # Duplicate standard input to standard output and standard error.
1023 os.dup2(0, 1) # standard output (1)
1024 os.dup2(0, 2) # standard error (2)
1028 def FindFile(name, search_path, test=os.path.exists):
1029 """Look for a filesystem object in a given path.
1031 This is an abstract method to search for filesystem object (files,
1032 dirs) under a given search path.
1035 - name: the name to look for
1036 - search_path: list of directory names
1037 - test: the test which the full path must satisfy
1038 (defaults to os.path.exists)
1041 - full path to the item if found
1045 for dir_name in search_path:
1046 item_name = os.path.sep.join([dir_name, name])
1052 def CheckVolumeGroupSize(vglist, vgname, minsize):
1053 """Checks if the volume group list is valid.
1055 A non-None return value means there's an error, and the return value
1056 is the error message.
1059 vgsize = vglist.get(vgname, None)
1061 return "volume group '%s' missing" % vgname
1062 elif vgsize < minsize:
1063 return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1064 (vgname, minsize, vgsize))