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
35 from errno import ENOENT, ENOTDIR, EISDIR, EEXIST
37 from ganeti import logger
38 from ganeti import errors
41 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
43 class RunResult(object):
44 """Simple class for holding the result of running external programs.
47 exit_code: the exit code of the program, or None (if the program
49 signal: numeric signal that caused the program to finish, or None
50 (if the program wasn't terminated by a signal)
51 stdout: the standard output of the program
52 stderr: the standard error of the program
53 failed: a Boolean value which is True in case the program was
54 terminated by a signal or exited with a non-zero exit code
55 fail_reason: a string detailing the termination reason
58 __slots__ = ["exit_code", "signal", "stdout", "stderr",
59 "failed", "fail_reason", "cmd"]
62 def __init__(self, exit_code, signal, stdout, stderr, cmd):
64 self.exit_code = exit_code
68 self.failed = (signal is not None or exit_code != 0)
70 if self.signal is not None:
71 self.fail_reason = "terminated by signal %s" % self.signal
72 elif self.exit_code is not None:
73 self.fail_reason = "exited with exit code %s" % self.exit_code
75 self.fail_reason = "unable to determine termination reason"
78 """Returns the combined stdout and stderr for easier usage.
81 return self.stdout + self.stderr
83 output = property(_GetOutput, None, None, "Return full output")
86 def _GetLockFile(subsystem):
87 """Compute the file name for a given lock name."""
88 return "/var/lock/ganeti_lock_%s" % subsystem
91 def Lock(name, max_retries=None, debug=False):
92 """Lock a given subsystem.
94 In case the lock is already held by an alive process, the function
95 will sleep indefintely and poll with a one second interval.
97 When the optional integer argument 'max_retries' is passed with a
98 non-zero value, the function will sleep only for this number of
99 times, and then it will will raise a LockError if the lock can't be
100 acquired. Passing in a negative number will cause only one try to
101 get the lock. Passing a positive number will make the function retry
102 for approximately that number of seconds.
105 lockfile = _GetLockFile(name)
107 if name in _locksheld:
108 raise errors.LockError('Lock "%s" already held!' % (name,))
115 fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
117 except OSError, creat_err:
118 if creat_err.errno != EEXIST:
119 raise errors.LockError("Can't create the lock file. Error '%s'." %
123 pf = open(lockfile, 'r')
124 except IOError, open_err:
127 raise errors.LockError("Lock file exists but cannot be opened."
128 " Error: '%s'." % str(open_err))
135 raise errors.LockError("Invalid pid string in %s" %
138 if not IsProcessAlive(pid):
139 raise errors.LockError("Stale lockfile %s for pid %d?" %
142 if max_retries and max_retries <= retries:
143 raise errors.LockError("Can't acquire lock during the specified"
145 if retries == 5 and (debug or sys.stdin.isatty()):
146 logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
152 os.write(fd, '%d\n' % (os.getpid(),))
155 _locksheld.append(name)
159 """Unlock a given subsystem.
162 lockfile = _GetLockFile(name)
165 fd = os.open(lockfile, os.O_RDONLY)
167 raise errors.LockError('Lock "%s" not held.' % (name,))
169 f = os.fdopen(fd, 'r')
175 raise errors.LockError('Unable to determine PID of locking process.')
177 if pid != os.getpid():
178 raise errors.LockError('Lock not held by me (%d != %d)' %
182 _locksheld.remove(name)
189 for lock in _locksheld:
194 """Execute a (shell) command.
196 The command should not read from its standard input, as it will be
200 cmd: command to run. (str)
202 Returns: `RunResult` instance
205 if isinstance(cmd, list):
206 cmd = [str(val) for val in cmd]
207 strcmd = " ".join(cmd)
212 child = subprocess.Popen(cmd, shell=shell,
213 stderr=subprocess.PIPE,
214 stdout=subprocess.PIPE,
215 stdin=subprocess.PIPE,
219 out = child.stdout.read()
220 err = child.stderr.read()
222 status = child.wait()
230 return RunResult(exitcode, signal, out, err, strcmd)
233 def RunCmdUnlocked(cmd):
234 """Execute a shell command without the 'cmd' lock.
236 This variant of `RunCmd()` drops the 'cmd' lock before running the
237 command and re-aquires it afterwards, thus it can be used to call
238 other ganeti commands.
240 The argument and return values are the same as for the `RunCmd()`
244 cmd - command to run. (str)
257 def RemoveFile(filename):
258 """Remove a file ignoring some errors.
260 Remove a file, ignoring non-existing ones or directories. Other
267 if err.errno not in (ENOENT, EISDIR):
271 def _FingerprintFile(filename):
272 """Compute the fingerprint of a file.
274 If the file does not exist, a None will be returned
278 filename - Filename (str)
281 if not (os.path.exists(filename) and os.path.isfile(filename)):
294 return fp.hexdigest()
297 def FingerprintFiles(files):
298 """Compute fingerprints for a list of files.
301 files - array of filenames. ( [str, ...] )
304 dictionary of filename: fingerprint for the files that exist
309 for filename in files:
310 cksum = _FingerprintFile(filename)
312 ret[filename] = cksum
317 def CheckDict(target, template, logname=None):
318 """Ensure a dictionary has a required set of keys.
320 For the given dictionaries `target` and `template`, ensure target
321 has all the keys from template. Missing keys are added with values
325 target - the dictionary to check
326 template - template dictionary
327 logname - a caller-chosen string to identify the debug log
328 entry; if None, no logging will be done
338 target[k] = template[k]
340 if missing and logname:
341 logger.Debug('%s missing keys %s' %
342 (logname, ', '.join(missing)))
345 def IsProcessAlive(pid):
346 """Check if a given pid exists on the system.
348 Returns: true or false, depending on if the pid exists or not
350 Remarks: zombie processes treated as not alive
354 f = open("/proc/%d/status" % pid)
356 if err.errno in (ENOENT, ENOTDIR):
363 state = data[1].split()
364 if len(state) > 1 and state[1] == "Z":
372 def MatchNameComponent(key, name_list):
373 """Try to match a name against a list.
375 This function will try to match a name like test1 against a list
376 like ['test1.example.com', 'test2.example.com', ...]. Against this
377 list, 'test1' as well as 'test1.example' will match, but not
378 'test1.ex'. A multiple match will be considered as no match at all
379 (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
382 key: the name to be searched
383 name_list: the list of strings against which to search the key
386 None if there is no match *or* if there are multiple matches
387 otherwise the element from the list which matches
390 mo = re.compile("^%s(\..*)?$" % re.escape(key))
391 names_filtered = [name for name in name_list if mo.match(name) is not None]
392 if len(names_filtered) != 1:
394 return names_filtered[0]
397 def LookupHostname(hostname):
401 hostname: hostname to look up, can be also be a non FQDN
404 Dictionary with keys:
406 - hostname_full: hostname fully qualified
407 - hostname: hostname fully qualified (historic artifact)
411 (fqdn, dummy, ipaddrs) = socket.gethostbyname_ex(hostname)
413 except socket.gaierror:
414 # hostname not found in DNS
419 "hostname_full": fqdn,
423 return returnhostname
426 def ListVolumeGroups():
427 """List volume groups and their size
430 Dictionary with keys volume name and values the size of the volume
433 command = "vgs --noheadings --units m --nosuffix -o name,size"
434 result = RunCmd(command)
439 for line in result.stdout.splitlines():
441 name, size = line.split()
442 size = int(float(size))
443 except (IndexError, ValueError), err:
444 logger.Error("Invalid output from vgs (%s): %s" % (err, line))
452 def BridgeExists(bridge):
453 """Check whether the given bridge exists in the system
456 True if it does, false otherwise.
459 return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
462 def NiceSort(name_list):
463 """Sort a list of strings based on digit and non-digit groupings.
465 Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
466 sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
468 The sort algorithm breaks each name in groups of either only-digits
469 or no-digits. Only the first eight such groups are considered, and
470 after that we just use what's left of the string.
473 - a copy of the list sorted according to our algorithm
476 _SORTER_BASE = "(\D+|\d+)"
477 _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
478 _SORTER_BASE, _SORTER_BASE,
479 _SORTER_BASE, _SORTER_BASE,
480 _SORTER_BASE, _SORTER_BASE)
481 _SORTER_RE = re.compile(_SORTER_FULL)
482 _SORTER_NODIGIT = re.compile("^\D*$")
484 """Attempts to convert a variable to integer."""
485 if val is None or _SORTER_NODIGIT.match(val):
490 to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
491 for name in name_list]
493 return [tup[1] for tup in to_sort]
496 def CheckDaemonAlive(pid_file, process_string):
497 """Check wether the specified daemon is alive.
500 - pid_file: file to read the daemon pid from, the file is
501 expected to contain only a single line containing
503 - process_string: a substring that we expect to find in
504 the command line of the daemon process
507 - True if the daemon is judged to be alive (that is:
508 - the PID file exists, is readable and contains a number
509 - a process of the specified PID is running
510 - that process contains the specified string in its
512 - the process is not in state Z (zombie))
517 pid_file = file(pid_file, 'r')
519 pid = int(pid_file.readline())
523 cmdline_file_path = "/proc/%s/cmdline" % (pid)
524 cmdline_file = open(cmdline_file_path, 'r')
526 cmdline = cmdline_file.readline()
530 if not process_string in cmdline:
533 stat_file_path = "/proc/%s/stat" % (pid)
534 stat_file = open(stat_file_path, 'r')
536 process_state = stat_file.readline().split()[2]
540 if process_state == 'Z':
543 except (IndexError, IOError, ValueError):
549 def TryConvert(fn, val):
550 """Try to convert a value ignoring errors.
552 This function tries to apply function `fn` to `val`. If no
553 ValueError or TypeError exceptions are raised, it will return the
554 result, else it will return the original value. Any other exceptions
555 are propagated to the caller.
560 except (ValueError, TypeError), err:
566 """Verifies the syntax of an IP address.
568 This function checks if the ip address passes is valid or not based
569 on syntax (not ip range, class calculations or anything).
572 unit = "(0|[1-9]\d{0,2})"
573 return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
576 def IsValidShellParam(word):
577 """Verifies is the given word is safe from the shell's p.o.v.
579 This means that we can pass this to a command via the shell and be
580 sure that it doesn't alter the command line and is passed as such to
583 Note that we are overly restrictive here, in order to be on the safe
587 return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
590 def BuildShellCmd(template, *args):
591 """Build a safe shell command line from the given arguments.
593 This function will check all arguments in the args list so that they
594 are valid shell parameters (i.e. they don't contain shell
595 metacharaters). If everything is ok, it will return the result of
600 if not IsValidShellParam(word):
601 raise errors.ProgrammerError("Shell argument '%s' contains"
602 " invalid characters" % word)
603 return template % args
606 def FormatUnit(value):
607 """Formats an incoming number of MiB with the appropriate unit.
609 Value needs to be passed as a numeric type. Return value is always a string.
613 return "%dM" % round(value, 0)
615 elif value < (1024 * 1024):
616 return "%0.1fG" % round(float(value) / 1024, 1)
619 return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
622 def ParseUnit(input_string):
623 """Tries to extract number and scale from the given string.
625 Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
626 is specified, it defaults to MiB. Return value is always an int in MiB.
629 m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
631 raise errors.UnitParseError("Invalid format")
633 value = float(m.groups()[0])
637 lcunit = unit.lower()
641 if lcunit in ('m', 'mb', 'mib'):
642 # Value already in MiB
645 elif lcunit in ('g', 'gb', 'gib'):
648 elif lcunit in ('t', 'tb', 'tib'):
652 raise errors.UnitParseError("Unknown unit: %s" % unit)
654 # Make sure we round up
655 if int(value) < value:
658 # Round up to the next multiple of 4
661 value += 4 - value % 4
666 def AddAuthorizedKey(file_name, key):
667 """Adds an SSH public key to an authorized_keys file.
670 file_name: Path to authorized_keys file
671 key: String containing key
673 key_fields = key.split()
675 f = open(file_name, 'a+')
679 # Ignore whitespace changes
680 if line.split() == key_fields:
682 nl = line.endswith('\n')
686 f.write(key.rstrip('\r\n'))
693 def RemoveAuthorizedKey(file_name, key):
694 """Removes an SSH public key from an authorized_keys file.
697 file_name: Path to authorized_keys file
698 key: String containing key
700 key_fields = key.split()
702 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
703 out = os.fdopen(fd, 'w')
705 f = open(file_name, 'r')
708 # Ignore whitespace changes while comparing lines
709 if line.split() != key_fields:
713 os.rename(tmpname, file_name)
720 def CreateBackup(file_name):
721 """Creates a backup of a file.
723 Returns: the path to the newly created backup file.
726 if not os.path.isfile(file_name):
727 raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
730 # Warning: the following code contains a race condition when we create more
731 # than one backup of the same file in a second.
732 backup_name = file_name + '.backup-%d' % int(time.time())
733 shutil.copyfile(file_name, backup_name)
737 def ShellQuote(value):
738 """Quotes shell argument according to POSIX.
741 if _re_shell_unquoted.match(value):
744 return "'%s'" % value.replace("'", "'\\''")
747 def ShellQuoteArgs(args):
748 """Quotes all given shell arguments and concatenates using spaces.
751 return ' '.join([ShellQuote(i) for i in args])
754 def _ParseIpOutput(output):
755 """Parsing code for GetLocalIPAddresses().
757 This function is split out, so we can unit test it.
760 re_ip = re.compile('^(\d+\.\d+\.\d+\.\d+)(?:/\d+)$')
763 for line in output.splitlines(False):
764 fields = line.split()
767 m = re_ip.match(fields[3])
769 ips.append(m.group(1))
774 def GetLocalIPAddresses():
775 """Gets a list of all local IP addresses.
777 Should this break one day, a small Python module written in C could
778 use the API call getifaddrs().
781 result = RunCmd(["ip", "-family", "inet", "-oneline", "addr", "show"])
783 raise errors.OpExecError("Command '%s' failed, error: %s,"
784 " output: %s" % (result.cmd, result.fail_reason, result.output))
786 return _ParseIpOutput(result.output)