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
40 from ganeti import logger
41 from ganeti import errors
45 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
47 class RunResult(object):
48 """Simple class for holding the result of running external programs.
51 exit_code: the exit code of the program, or None (if the program
53 signal: numeric signal that caused the program to finish, or None
54 (if the program wasn't terminated by a signal)
55 stdout: the standard output of the program
56 stderr: the standard error of the program
57 failed: a Boolean value which is True in case the program was
58 terminated by a signal or exited with a non-zero exit code
59 fail_reason: a string detailing the termination reason
62 __slots__ = ["exit_code", "signal", "stdout", "stderr",
63 "failed", "fail_reason", "cmd"]
66 def __init__(self, exit_code, signal, stdout, stderr, cmd):
68 self.exit_code = exit_code
72 self.failed = (signal is not None or exit_code != 0)
74 if self.signal is not None:
75 self.fail_reason = "terminated by signal %s" % self.signal
76 elif self.exit_code is not None:
77 self.fail_reason = "exited with exit code %s" % self.exit_code
79 self.fail_reason = "unable to determine termination reason"
82 """Returns the combined stdout and stderr for easier usage.
85 return self.stdout + self.stderr
87 output = property(_GetOutput, None, None, "Return full output")
90 def _GetLockFile(subsystem):
91 """Compute the file name for a given lock name."""
92 return "/var/lock/ganeti_lock_%s" % subsystem
95 def Lock(name, max_retries=None, debug=False):
96 """Lock a given subsystem.
98 In case the lock is already held by an alive process, the function
99 will sleep indefintely and poll with a one second interval.
101 When the optional integer argument 'max_retries' is passed with a
102 non-zero value, the function will sleep only for this number of
103 times, and then it will will raise a LockError if the lock can't be
104 acquired. Passing in a negative number will cause only one try to
105 get the lock. Passing a positive number will make the function retry
106 for approximately that number of seconds.
109 lockfile = _GetLockFile(name)
111 if name in _locksheld:
112 raise errors.LockError('Lock "%s" already held!' % (name,))
119 fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
121 except OSError, creat_err:
122 if creat_err.errno != errno.EEXIST:
123 raise errors.LockError("Can't create the lock file. Error '%s'." %
127 pf = open(lockfile, 'r')
128 except IOError, open_err:
131 raise errors.LockError("Lock file exists but cannot be opened."
132 " Error: '%s'." % str(open_err))
139 raise errors.LockError("Invalid pid string in %s" %
142 if not IsProcessAlive(pid):
143 raise errors.LockError("Stale lockfile %s for pid %d?" %
146 if max_retries and max_retries <= retries:
147 raise errors.LockError("Can't acquire lock during the specified"
149 if retries == 5 and (debug or sys.stdin.isatty()):
150 logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
156 os.write(fd, '%d\n' % (os.getpid(),))
159 _locksheld.append(name)
163 """Unlock a given subsystem.
166 lockfile = _GetLockFile(name)
169 fd = os.open(lockfile, os.O_RDONLY)
171 raise errors.LockError('Lock "%s" not held.' % (name,))
173 f = os.fdopen(fd, 'r')
179 raise errors.LockError('Unable to determine PID of locking process.')
181 if pid != os.getpid():
182 raise errors.LockError('Lock not held by me (%d != %d)' %
186 _locksheld.remove(name)
193 for lock in _locksheld:
198 """Execute a (shell) command.
200 The command should not read from its standard input, as it will be
204 cmd: command to run. (str)
206 Returns: `RunResult` instance
209 if isinstance(cmd, list):
210 cmd = [str(val) for val in cmd]
211 strcmd = " ".join(cmd)
216 env = os.environ.copy()
218 child = subprocess.Popen(cmd, shell=shell,
219 stderr=subprocess.PIPE,
220 stdout=subprocess.PIPE,
221 stdin=subprocess.PIPE,
222 close_fds=True, env=env)
225 out = child.stdout.read()
226 err = child.stderr.read()
228 status = child.wait()
236 return RunResult(exitcode, signal, out, err, strcmd)
239 def RunCmdUnlocked(cmd):
240 """Execute a shell command without the 'cmd' lock.
242 This variant of `RunCmd()` drops the 'cmd' lock before running the
243 command and re-aquires it afterwards, thus it can be used to call
244 other ganeti commands.
246 The argument and return values are the same as for the `RunCmd()`
250 cmd - command to run. (str)
263 def RemoveFile(filename):
264 """Remove a file ignoring some errors.
266 Remove a file, ignoring non-existing ones or directories. Other
273 if err.errno not in (errno.ENOENT, errno.EISDIR):
277 def _FingerprintFile(filename):
278 """Compute the fingerprint of a file.
280 If the file does not exist, a None will be returned
284 filename - Filename (str)
287 if not (os.path.exists(filename) and os.path.isfile(filename)):
300 return fp.hexdigest()
303 def FingerprintFiles(files):
304 """Compute fingerprints for a list of files.
307 files - array of filenames. ( [str, ...] )
310 dictionary of filename: fingerprint for the files that exist
315 for filename in files:
316 cksum = _FingerprintFile(filename)
318 ret[filename] = cksum
323 def CheckDict(target, template, logname=None):
324 """Ensure a dictionary has a required set of keys.
326 For the given dictionaries `target` and `template`, ensure target
327 has all the keys from template. Missing keys are added with values
331 target - the dictionary to check
332 template - template dictionary
333 logname - a caller-chosen string to identify the debug log
334 entry; if None, no logging will be done
344 target[k] = template[k]
346 if missing and logname:
347 logger.Debug('%s missing keys %s' %
348 (logname, ', '.join(missing)))
351 def IsProcessAlive(pid):
352 """Check if a given pid exists on the system.
354 Returns: true or false, depending on if the pid exists or not
356 Remarks: zombie processes treated as not alive
360 f = open("/proc/%d/status" % pid)
362 if err.errno in (errno.ENOENT, errno.ENOTDIR):
369 state = data[1].split()
370 if len(state) > 1 and state[1] == "Z":
378 def MatchNameComponent(key, name_list):
379 """Try to match a name against a list.
381 This function will try to match a name like test1 against a list
382 like ['test1.example.com', 'test2.example.com', ...]. Against this
383 list, 'test1' as well as 'test1.example' will match, but not
384 'test1.ex'. A multiple match will be considered as no match at all
385 (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
388 key: the name to be searched
389 name_list: the list of strings against which to search the key
392 None if there is no match *or* if there are multiple matches
393 otherwise the element from the list which matches
396 mo = re.compile("^%s(\..*)?$" % re.escape(key))
397 names_filtered = [name for name in name_list if mo.match(name) is not None]
398 if len(names_filtered) != 1:
400 return names_filtered[0]
404 """Class implementing resolver and hostname functionality
407 def __init__(self, name=None):
408 """Initialize the host name object.
410 If the name argument is not passed, it will use this system's
415 name = self.SysName()
418 self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
419 self.ip = self.ipaddrs[0]
422 """Returns the hostname without domain.
425 return self.name.split('.')[0]
429 """Return the current system's name.
431 This is simply a wrapper over socket.gethostname()
434 return socket.gethostname()
437 def LookupHostname(hostname):
441 hostname: hostname to look up
444 a tuple (name, aliases, ipaddrs) as returned by socket.gethostbyname_ex
445 in case of errors in resolving, we raise a ResolverError
449 result = socket.gethostbyname_ex(hostname)
450 except socket.gaierror, err:
451 # hostname not found in DNS
452 raise errors.ResolverError(hostname, err.args[0], err.args[1])
457 def ListVolumeGroups():
458 """List volume groups and their size
461 Dictionary with keys volume name and values the size of the volume
464 command = "vgs --noheadings --units m --nosuffix -o name,size"
465 result = RunCmd(command)
470 for line in result.stdout.splitlines():
472 name, size = line.split()
473 size = int(float(size))
474 except (IndexError, ValueError), err:
475 logger.Error("Invalid output from vgs (%s): %s" % (err, line))
483 def BridgeExists(bridge):
484 """Check whether the given bridge exists in the system
487 True if it does, false otherwise.
490 return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
493 def NiceSort(name_list):
494 """Sort a list of strings based on digit and non-digit groupings.
496 Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
497 sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
499 The sort algorithm breaks each name in groups of either only-digits
500 or no-digits. Only the first eight such groups are considered, and
501 after that we just use what's left of the string.
504 - a copy of the list sorted according to our algorithm
507 _SORTER_BASE = "(\D+|\d+)"
508 _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
509 _SORTER_BASE, _SORTER_BASE,
510 _SORTER_BASE, _SORTER_BASE,
511 _SORTER_BASE, _SORTER_BASE)
512 _SORTER_RE = re.compile(_SORTER_FULL)
513 _SORTER_NODIGIT = re.compile("^\D*$")
515 """Attempts to convert a variable to integer."""
516 if val is None or _SORTER_NODIGIT.match(val):
521 to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
522 for name in name_list]
524 return [tup[1] for tup in to_sort]
527 def CheckDaemonAlive(pid_file, process_string):
528 """Check wether the specified daemon is alive.
531 - pid_file: file to read the daemon pid from, the file is
532 expected to contain only a single line containing
534 - process_string: a substring that we expect to find in
535 the command line of the daemon process
538 - True if the daemon is judged to be alive (that is:
539 - the PID file exists, is readable and contains a number
540 - a process of the specified PID is running
541 - that process contains the specified string in its
543 - the process is not in state Z (zombie))
548 pid_file = file(pid_file, 'r')
550 pid = int(pid_file.readline())
554 cmdline_file_path = "/proc/%s/cmdline" % (pid)
555 cmdline_file = open(cmdline_file_path, 'r')
557 cmdline = cmdline_file.readline()
561 if not process_string in cmdline:
564 stat_file_path = "/proc/%s/stat" % (pid)
565 stat_file = open(stat_file_path, 'r')
567 process_state = stat_file.readline().split()[2]
571 if process_state == 'Z':
574 except (IndexError, IOError, ValueError):
580 def TryConvert(fn, val):
581 """Try to convert a value ignoring errors.
583 This function tries to apply function `fn` to `val`. If no
584 ValueError or TypeError exceptions are raised, it will return the
585 result, else it will return the original value. Any other exceptions
586 are propagated to the caller.
591 except (ValueError, TypeError), err:
597 """Verifies the syntax of an IP address.
599 This function checks if the ip address passes is valid or not based
600 on syntax (not ip range, class calculations or anything).
603 unit = "(0|[1-9]\d{0,2})"
604 return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
607 def IsValidShellParam(word):
608 """Verifies is the given word is safe from the shell's p.o.v.
610 This means that we can pass this to a command via the shell and be
611 sure that it doesn't alter the command line and is passed as such to
614 Note that we are overly restrictive here, in order to be on the safe
618 return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
621 def BuildShellCmd(template, *args):
622 """Build a safe shell command line from the given arguments.
624 This function will check all arguments in the args list so that they
625 are valid shell parameters (i.e. they don't contain shell
626 metacharaters). If everything is ok, it will return the result of
631 if not IsValidShellParam(word):
632 raise errors.ProgrammerError("Shell argument '%s' contains"
633 " invalid characters" % word)
634 return template % args
637 def FormatUnit(value):
638 """Formats an incoming number of MiB with the appropriate unit.
640 Value needs to be passed as a numeric type. Return value is always a string.
644 return "%dM" % round(value, 0)
646 elif value < (1024 * 1024):
647 return "%0.1fG" % round(float(value) / 1024, 1)
650 return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
653 def ParseUnit(input_string):
654 """Tries to extract number and scale from the given string.
656 Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
657 is specified, it defaults to MiB. Return value is always an int in MiB.
660 m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
662 raise errors.UnitParseError("Invalid format")
664 value = float(m.groups()[0])
668 lcunit = unit.lower()
672 if lcunit in ('m', 'mb', 'mib'):
673 # Value already in MiB
676 elif lcunit in ('g', 'gb', 'gib'):
679 elif lcunit in ('t', 'tb', 'tib'):
683 raise errors.UnitParseError("Unknown unit: %s" % unit)
685 # Make sure we round up
686 if int(value) < value:
689 # Round up to the next multiple of 4
692 value += 4 - value % 4
697 def AddAuthorizedKey(file_name, key):
698 """Adds an SSH public key to an authorized_keys file.
701 file_name: Path to authorized_keys file
702 key: String containing key
704 key_fields = key.split()
706 f = open(file_name, 'a+')
710 # Ignore whitespace changes
711 if line.split() == key_fields:
713 nl = line.endswith('\n')
717 f.write(key.rstrip('\r\n'))
724 def RemoveAuthorizedKey(file_name, key):
725 """Removes an SSH public key from an authorized_keys file.
728 file_name: Path to authorized_keys file
729 key: String containing key
731 key_fields = key.split()
733 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
735 out = os.fdopen(fd, 'w')
737 f = open(file_name, 'r')
740 # Ignore whitespace changes while comparing lines
741 if line.split() != key_fields:
745 os.rename(tmpname, file_name)
755 def SetEtcHostsEntry(file_name, ip, hostname, aliases):
756 """Sets the name of an IP address and hostname in /etc/hosts.
759 # Ensure aliases are unique
760 aliases = UniqueSequence([hostname] + aliases)[1:]
762 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
764 out = os.fdopen(fd, 'w')
766 f = open(file_name, 'r')
770 fields = line.split()
771 if fields and not fields[0].startswith('#') and ip == fields[0]:
775 out.write("%s\t%s" % (ip, hostname))
777 out.write(" %s" % ' '.join(aliases))
782 os.rename(tmpname, file_name)
792 def RemoveEtcHostsEntry(file_name, hostname):
793 """Removes a hostname from /etc/hosts.
795 IP addresses without names are removed from the file.
797 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
799 out = os.fdopen(fd, 'w')
801 f = open(file_name, 'r')
804 fields = line.split()
805 if len(fields) > 1 and not fields[0].startswith('#'):
807 if hostname in names:
808 while hostname in names:
809 names.remove(hostname)
811 out.write("%s %s\n" % (fields[0], ' '.join(names)))
818 os.rename(tmpname, file_name)
828 def CreateBackup(file_name):
829 """Creates a backup of a file.
831 Returns: the path to the newly created backup file.
834 if not os.path.isfile(file_name):
835 raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
838 prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
839 dir_name = os.path.dirname(file_name)
841 fsrc = open(file_name, 'rb')
843 (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
844 fdst = os.fdopen(fd, 'wb')
846 shutil.copyfileobj(fsrc, fdst)
855 def ShellQuote(value):
856 """Quotes shell argument according to POSIX.
859 if _re_shell_unquoted.match(value):
862 return "'%s'" % value.replace("'", "'\\''")
865 def ShellQuoteArgs(args):
866 """Quotes all given shell arguments and concatenates using spaces.
869 return ' '.join([ShellQuote(i) for i in args])
873 def TcpPing(source, target, port, timeout=10, live_port_needed=False):
874 """Simple ping implementation using TCP connect(2).
876 Try to do a TCP connect(2) from the specified source IP to the specified
877 target IP and the specified target port. If live_port_needed is set to true,
878 requires the remote end to accept the connection. The timeout is specified
879 in seconds and defaults to 10 seconds
882 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
887 sock.bind((source, 0))
888 except socket.error, (errcode, errstring):
889 if errcode == errno.EADDRNOTAVAIL:
892 sock.settimeout(timeout)
895 sock.connect((target, port))
898 except socket.timeout:
900 except socket.error, (errcode, errstring):
901 success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
906 def ListVisibleFiles(path):
907 """Returns a list of all visible files in a directory.
910 files = [i for i in os.listdir(path) if not i.startswith(".")]
915 def GetHomeDir(user, default=None):
916 """Try to get the homedir of the given user.
918 The user can be passed either as a string (denoting the name) or as
919 an integer (denoting the user id). If the user is not found, the
920 'default' argument is returned, which defaults to None.
924 if isinstance(user, basestring):
925 result = pwd.getpwnam(user)
926 elif isinstance(user, (int, long)):
927 result = pwd.getpwuid(user)
929 raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
937 """Returns a random UUID.
940 f = open("/proc/sys/kernel/random/uuid", "r")
942 return f.read(128).rstrip("\n")
947 def WriteFile(file_name, fn=None, data=None,
948 mode=None, uid=-1, gid=-1,
949 atime=None, mtime=None):
950 """(Over)write a file atomically.
952 The file_name and either fn (a function taking one argument, the
953 file descriptor, and which should write the data to it) or data (the
954 contents of the file) must be passed. The other arguments are
955 optional and allow setting the file mode, owner and group, and the
956 mtime/atime of the file.
958 If the function doesn't raise an exception, it has succeeded and the
959 target file has the new contents. If the file has raised an
960 exception, an existing target file should be unmodified and the
961 temporary file should be removed.
964 if not os.path.isabs(file_name):
965 raise errors.ProgrammerError("Path passed to WriteFile is not"
966 " absolute: '%s'" % file_name)
968 if [fn, data].count(None) != 1:
969 raise errors.ProgrammerError("fn or data required")
971 if [atime, mtime].count(None) == 1:
972 raise errors.ProgrammerError("Both atime and mtime must be either"
976 dir_name, base_name = os.path.split(file_name)
977 fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
978 # here we need to make sure we remove the temp file, if any error
981 if uid != -1 or gid != -1:
982 os.chown(new_name, uid, gid)
984 os.chmod(new_name, mode)
990 if atime is not None and mtime is not None:
991 os.utime(new_name, (atime, mtime))
992 os.rename(new_name, file_name)
998 def all(seq, pred=bool):
999 "Returns True if pred(x) is True for every element in the iterable"
1000 for elem in itertools.ifilterfalse(pred, seq):
1005 def any(seq, pred=bool):
1006 "Returns True if pred(x) is True for at least one element in the iterable"
1007 for elem in itertools.ifilter(pred, seq):
1012 def UniqueSequence(seq):
1013 """Returns a list with unique elements.
1015 Element order is preserved.
1018 return [i for i in seq if i not in seen and not seen.add(i)]