Do not install init script in PREFIX/bin.
[ganeti-local] / lib / utils.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Ganeti small utilities
23 """
24
25
26 import sys
27 import os
28 import sha
29 import time
30 import subprocess
31 import re
32 import socket
33 import tempfile
34 import shutil
35 import errno
36
37 from ganeti import logger
38 from ganeti import errors
39
40 _locksheld = []
41 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
42
43 class RunResult(object):
44   """Simple class for holding the result of running external programs.
45
46   Instance variables:
47     exit_code: the exit code of the program, or None (if the program
48                didn't exit())
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
56
57   """
58   __slots__ = ["exit_code", "signal", "stdout", "stderr",
59                "failed", "fail_reason", "cmd"]
60
61
62   def __init__(self, exit_code, signal, stdout, stderr, cmd):
63     self.cmd = cmd
64     self.exit_code = exit_code
65     self.signal = signal
66     self.stdout = stdout
67     self.stderr = stderr
68     self.failed = (signal is not None or exit_code != 0)
69
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
74     else:
75       self.fail_reason = "unable to determine termination reason"
76
77   def _GetOutput(self):
78     """Returns the combined stdout and stderr for easier usage.
79
80     """
81     return self.stdout + self.stderr
82
83   output = property(_GetOutput, None, None, "Return full output")
84
85
86 def _GetLockFile(subsystem):
87   """Compute the file name for a given lock name."""
88   return "/var/lock/ganeti_lock_%s" % subsystem
89
90
91 def Lock(name, max_retries=None, debug=False):
92   """Lock a given subsystem.
93
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.
96
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.
103
104   """
105   lockfile = _GetLockFile(name)
106
107   if name in _locksheld:
108     raise errors.LockError('Lock "%s" already held!' % (name,))
109
110   errcount = 0
111
112   retries = 0
113   while True:
114     try:
115       fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
116       break
117     except OSError, creat_err:
118       if creat_err.errno != errno.EEXIST:
119         raise errors.LockError("Can't create the lock file. Error '%s'." %
120                                str(creat_err))
121
122       try:
123         pf = open(lockfile, 'r')
124       except IOError, open_err:
125         errcount += 1
126         if errcount >= 5:
127           raise errors.LockError("Lock file exists but cannot be opened."
128                                  " Error: '%s'." % str(open_err))
129         time.sleep(1)
130         continue
131
132       try:
133         pid = int(pf.read())
134       except ValueError:
135         raise errors.LockError("Invalid pid string in %s" %
136                                (lockfile,))
137
138       if not IsProcessAlive(pid):
139         raise errors.LockError("Stale lockfile %s for pid %d?" %
140                                (lockfile, pid))
141
142       if max_retries and max_retries <= retries:
143         raise errors.LockError("Can't acquire lock during the specified"
144                                " time, aborting.")
145       if retries == 5 and (debug or sys.stdin.isatty()):
146         logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
147
148       time.sleep(1)
149       retries += 1
150       continue
151
152   os.write(fd, '%d\n' % (os.getpid(),))
153   os.close(fd)
154
155   _locksheld.append(name)
156
157
158 def Unlock(name):
159   """Unlock a given subsystem.
160
161   """
162   lockfile = _GetLockFile(name)
163
164   try:
165     fd = os.open(lockfile, os.O_RDONLY)
166   except OSError:
167     raise errors.LockError('Lock "%s" not held.' % (name,))
168
169   f = os.fdopen(fd, 'r')
170   pid_str = f.read()
171
172   try:
173     pid = int(pid_str)
174   except ValueError:
175     raise errors.LockError('Unable to determine PID of locking process.')
176
177   if pid != os.getpid():
178     raise errors.LockError('Lock not held by me (%d != %d)' %
179                            (os.getpid(), pid,))
180
181   os.unlink(lockfile)
182   _locksheld.remove(name)
183
184
185 def LockCleanup():
186   """Remove all locks.
187
188   """
189   for lock in _locksheld:
190     Unlock(lock)
191
192
193 def RunCmd(cmd):
194   """Execute a (shell) command.
195
196   The command should not read from its standard input, as it will be
197   closed.
198
199   Args:
200     cmd: command to run. (str)
201
202   Returns: `RunResult` instance
203
204   """
205   if isinstance(cmd, list):
206     cmd = [str(val) for val in cmd]
207     strcmd = " ".join(cmd)
208     shell = False
209   else:
210     strcmd = cmd
211     shell = True
212   env = os.environ.copy()
213   env["LC_ALL"] = "C"
214   child = subprocess.Popen(cmd, shell=shell,
215                            stderr=subprocess.PIPE,
216                            stdout=subprocess.PIPE,
217                            stdin=subprocess.PIPE,
218                            close_fds=True, env=env)
219
220   child.stdin.close()
221   out = child.stdout.read()
222   err = child.stderr.read()
223
224   status = child.wait()
225   if status >= 0:
226     exitcode = status
227     signal = None
228   else:
229     exitcode = None
230     signal = -status
231
232   return RunResult(exitcode, signal, out, err, strcmd)
233
234
235 def RunCmdUnlocked(cmd):
236   """Execute a shell command without the 'cmd' lock.
237
238   This variant of `RunCmd()` drops the 'cmd' lock before running the
239   command and re-aquires it afterwards, thus it can be used to call
240   other ganeti commands.
241
242   The argument and return values are the same as for the `RunCmd()`
243   function.
244
245   Args:
246     cmd - command to run. (str)
247
248   Returns:
249     `RunResult`
250
251   """
252   Unlock('cmd')
253   ret = RunCmd(cmd)
254   Lock('cmd')
255
256   return ret
257
258
259 def RemoveFile(filename):
260   """Remove a file ignoring some errors.
261
262   Remove a file, ignoring non-existing ones or directories. Other
263   errors are passed.
264
265   """
266   try:
267     os.unlink(filename)
268   except OSError, err:
269     if err.errno not in (errno.ENOENT, errno.EISDIR):
270       raise
271
272
273 def _FingerprintFile(filename):
274   """Compute the fingerprint of a file.
275
276   If the file does not exist, a None will be returned
277   instead.
278
279   Args:
280     filename - Filename (str)
281
282   """
283   if not (os.path.exists(filename) and os.path.isfile(filename)):
284     return None
285
286   f = open(filename)
287
288   fp = sha.sha()
289   while True:
290     data = f.read(4096)
291     if not data:
292       break
293
294     fp.update(data)
295
296   return fp.hexdigest()
297
298
299 def FingerprintFiles(files):
300   """Compute fingerprints for a list of files.
301
302   Args:
303     files - array of filenames.  ( [str, ...] )
304
305   Return value:
306     dictionary of filename: fingerprint for the files that exist
307
308   """
309   ret = {}
310
311   for filename in files:
312     cksum = _FingerprintFile(filename)
313     if cksum:
314       ret[filename] = cksum
315
316   return ret
317
318
319 def CheckDict(target, template, logname=None):
320   """Ensure a dictionary has a required set of keys.
321
322   For the given dictionaries `target` and `template`, ensure target
323   has all the keys from template. Missing keys are added with values
324   from template.
325
326   Args:
327     target   - the dictionary to check
328     template - template dictionary
329     logname  - a caller-chosen string to identify the debug log
330                entry; if None, no logging will be done
331
332   Returns value:
333     None
334
335   """
336   missing = []
337   for k in template:
338     if k not in target:
339       missing.append(k)
340       target[k] = template[k]
341
342   if missing and logname:
343     logger.Debug('%s missing keys %s' %
344                  (logname, ', '.join(missing)))
345
346
347 def IsProcessAlive(pid):
348   """Check if a given pid exists on the system.
349
350   Returns: true or false, depending on if the pid exists or not
351
352   Remarks: zombie processes treated as not alive
353
354   """
355   try:
356     f = open("/proc/%d/status" % pid)
357   except IOError, err:
358     if err.errno in (errno.ENOENT, errno.ENOTDIR):
359       return False
360
361   alive = True
362   try:
363     data = f.readlines()
364     if len(data) > 1:
365       state = data[1].split()
366       if len(state) > 1 and state[1] == "Z":
367         alive = False
368   finally:
369     f.close()
370
371   return alive
372
373
374 def MatchNameComponent(key, name_list):
375   """Try to match a name against a list.
376
377   This function will try to match a name like test1 against a list
378   like ['test1.example.com', 'test2.example.com', ...]. Against this
379   list, 'test1' as well as 'test1.example' will match, but not
380   'test1.ex'. A multiple match will be considered as no match at all
381   (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
382
383   Args:
384     key: the name to be searched
385     name_list: the list of strings against which to search the key
386
387   Returns:
388     None if there is no match *or* if there are multiple matches
389     otherwise the element from the list which matches
390
391   """
392   mo = re.compile("^%s(\..*)?$" % re.escape(key))
393   names_filtered = [name for name in name_list if mo.match(name) is not None]
394   if len(names_filtered) != 1:
395     return None
396   return names_filtered[0]
397
398
399 class HostInfo:
400   """Class implementing resolver and hostname functionality
401
402   """
403   def __init__(self, name=None):
404     """Initialize the host name object.
405
406     If the name argument is not passed, it will use this system's
407     name.
408
409     """
410     if name is None:
411       name = self.SysName()
412
413     self.query = name
414     self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
415     self.ip = self.ipaddrs[0]
416
417   @staticmethod
418   def SysName():
419     """Return the current system's name.
420
421     This is simply a wrapper over socket.gethostname()
422
423     """
424     return socket.gethostname()
425
426   @staticmethod
427   def LookupHostname(hostname):
428     """Look up hostname
429
430     Args:
431       hostname: hostname to look up
432
433     Returns:
434       a tuple (name, aliases, ipaddrs) as returned by socket.gethostbyname_ex
435       in case of errors in resolving, we raise a ResolverError
436
437     """
438     try:
439       result = socket.gethostbyname_ex(hostname)
440     except socket.gaierror, err:
441       # hostname not found in DNS
442       raise errors.ResolverError(hostname, err.args[0], err.args[1])
443
444     return result
445
446
447 def ListVolumeGroups():
448   """List volume groups and their size
449
450   Returns:
451      Dictionary with keys volume name and values the size of the volume
452
453   """
454   command = "vgs --noheadings --units m --nosuffix -o name,size"
455   result = RunCmd(command)
456   retval = {}
457   if result.failed:
458     return retval
459
460   for line in result.stdout.splitlines():
461     try:
462       name, size = line.split()
463       size = int(float(size))
464     except (IndexError, ValueError), err:
465       logger.Error("Invalid output from vgs (%s): %s" % (err, line))
466       continue
467
468     retval[name] = size
469
470   return retval
471
472
473 def BridgeExists(bridge):
474   """Check whether the given bridge exists in the system
475
476   Returns:
477      True if it does, false otherwise.
478
479   """
480   return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
481
482
483 def NiceSort(name_list):
484   """Sort a list of strings based on digit and non-digit groupings.
485
486   Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
487   sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
488
489   The sort algorithm breaks each name in groups of either only-digits
490   or no-digits. Only the first eight such groups are considered, and
491   after that we just use what's left of the string.
492
493   Return value
494     - a copy of the list sorted according to our algorithm
495
496   """
497   _SORTER_BASE = "(\D+|\d+)"
498   _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
499                                                   _SORTER_BASE, _SORTER_BASE,
500                                                   _SORTER_BASE, _SORTER_BASE,
501                                                   _SORTER_BASE, _SORTER_BASE)
502   _SORTER_RE = re.compile(_SORTER_FULL)
503   _SORTER_NODIGIT = re.compile("^\D*$")
504   def _TryInt(val):
505     """Attempts to convert a variable to integer."""
506     if val is None or _SORTER_NODIGIT.match(val):
507       return val
508     rval = int(val)
509     return rval
510
511   to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
512              for name in name_list]
513   to_sort.sort()
514   return [tup[1] for tup in to_sort]
515
516
517 def CheckDaemonAlive(pid_file, process_string):
518   """Check wether the specified daemon is alive.
519
520   Args:
521    - pid_file: file to read the daemon pid from, the file is
522                expected to contain only a single line containing
523                only the PID
524    - process_string: a substring that we expect to find in
525                      the command line of the daemon process
526
527   Returns:
528    - True if the daemon is judged to be alive (that is:
529       - the PID file exists, is readable and contains a number
530       - a process of the specified PID is running
531       - that process contains the specified string in its
532         command line
533       - the process is not in state Z (zombie))
534    - False otherwise
535
536   """
537   try:
538     pid_file = file(pid_file, 'r')
539     try:
540       pid = int(pid_file.readline())
541     finally:
542       pid_file.close()
543
544     cmdline_file_path = "/proc/%s/cmdline" % (pid)
545     cmdline_file = open(cmdline_file_path, 'r')
546     try:
547       cmdline = cmdline_file.readline()
548     finally:
549       cmdline_file.close()
550
551     if not process_string in cmdline:
552       return False
553
554     stat_file_path =  "/proc/%s/stat" % (pid)
555     stat_file = open(stat_file_path, 'r')
556     try:
557       process_state = stat_file.readline().split()[2]
558     finally:
559       stat_file.close()
560
561     if process_state == 'Z':
562       return False
563
564   except (IndexError, IOError, ValueError):
565     return False
566
567   return True
568
569
570 def TryConvert(fn, val):
571   """Try to convert a value ignoring errors.
572
573   This function tries to apply function `fn` to `val`. If no
574   ValueError or TypeError exceptions are raised, it will return the
575   result, else it will return the original value. Any other exceptions
576   are propagated to the caller.
577
578   """
579   try:
580     nv = fn(val)
581   except (ValueError, TypeError), err:
582     nv = val
583   return nv
584
585
586 def IsValidIP(ip):
587   """Verifies the syntax of an IP address.
588
589   This function checks if the ip address passes is valid or not based
590   on syntax (not ip range, class calculations or anything).
591
592   """
593   unit = "(0|[1-9]\d{0,2})"
594   return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
595
596
597 def IsValidShellParam(word):
598   """Verifies is the given word is safe from the shell's p.o.v.
599
600   This means that we can pass this to a command via the shell and be
601   sure that it doesn't alter the command line and is passed as such to
602   the actual command.
603
604   Note that we are overly restrictive here, in order to be on the safe
605   side.
606
607   """
608   return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
609
610
611 def BuildShellCmd(template, *args):
612   """Build a safe shell command line from the given arguments.
613
614   This function will check all arguments in the args list so that they
615   are valid shell parameters (i.e. they don't contain shell
616   metacharaters). If everything is ok, it will return the result of
617   template % args.
618
619   """
620   for word in args:
621     if not IsValidShellParam(word):
622       raise errors.ProgrammerError("Shell argument '%s' contains"
623                                    " invalid characters" % word)
624   return template % args
625
626
627 def FormatUnit(value):
628   """Formats an incoming number of MiB with the appropriate unit.
629
630   Value needs to be passed as a numeric type. Return value is always a string.
631
632   """
633   if value < 1024:
634     return "%dM" % round(value, 0)
635
636   elif value < (1024 * 1024):
637     return "%0.1fG" % round(float(value) / 1024, 1)
638
639   else:
640     return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
641
642
643 def ParseUnit(input_string):
644   """Tries to extract number and scale from the given string.
645
646   Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
647   is specified, it defaults to MiB. Return value is always an int in MiB.
648
649   """
650   m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
651   if not m:
652     raise errors.UnitParseError("Invalid format")
653
654   value = float(m.groups()[0])
655
656   unit = m.groups()[1]
657   if unit:
658     lcunit = unit.lower()
659   else:
660     lcunit = 'm'
661
662   if lcunit in ('m', 'mb', 'mib'):
663     # Value already in MiB
664     pass
665
666   elif lcunit in ('g', 'gb', 'gib'):
667     value *= 1024
668
669   elif lcunit in ('t', 'tb', 'tib'):
670     value *= 1024 * 1024
671
672   else:
673     raise errors.UnitParseError("Unknown unit: %s" % unit)
674
675   # Make sure we round up
676   if int(value) < value:
677     value += 1
678
679   # Round up to the next multiple of 4
680   value = int(value)
681   if value % 4:
682     value += 4 - value % 4
683
684   return value
685
686
687 def AddAuthorizedKey(file_name, key):
688   """Adds an SSH public key to an authorized_keys file.
689
690   Args:
691     file_name: Path to authorized_keys file
692     key: String containing key
693   """
694   key_fields = key.split()
695
696   f = open(file_name, 'a+')
697   try:
698     nl = True
699     for line in f:
700       # Ignore whitespace changes
701       if line.split() == key_fields:
702         break
703       nl = line.endswith('\n')
704     else:
705       if not nl:
706         f.write("\n")
707       f.write(key.rstrip('\r\n'))
708       f.write("\n")
709       f.flush()
710   finally:
711     f.close()
712
713
714 def RemoveAuthorizedKey(file_name, key):
715   """Removes an SSH public key from an authorized_keys file.
716
717   Args:
718     file_name: Path to authorized_keys file
719     key: String containing key
720   """
721   key_fields = key.split()
722
723   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
724   out = os.fdopen(fd, 'w')
725   try:
726     f = open(file_name, 'r')
727     try:
728       for line in f:
729         # Ignore whitespace changes while comparing lines
730         if line.split() != key_fields:
731           out.write(line)
732
733       out.flush()
734       os.rename(tmpname, file_name)
735     finally:
736       f.close()
737   finally:
738     out.close()
739
740
741 def CreateBackup(file_name):
742   """Creates a backup of a file.
743
744   Returns: the path to the newly created backup file.
745
746   """
747   if not os.path.isfile(file_name):
748     raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
749                                 file_name)
750
751   prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
752   dir = os.path.dirname(file_name)
753
754   fsrc = open(file_name, 'rb')
755   try:
756     (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir)
757     fdst = os.fdopen(fd, 'wb')
758     try:
759       shutil.copyfileobj(fsrc, fdst)
760     finally:
761       fdst.close()
762   finally:
763     fsrc.close()
764
765   return backup_name
766
767
768 def ShellQuote(value):
769   """Quotes shell argument according to POSIX.
770
771   """
772   if _re_shell_unquoted.match(value):
773     return value
774   else:
775     return "'%s'" % value.replace("'", "'\\''")
776
777
778 def ShellQuoteArgs(args):
779   """Quotes all given shell arguments and concatenates using spaces.
780
781   """
782   return ' '.join([ShellQuote(i) for i in args])
783
784
785 def _ParseIpOutput(output):
786   """Parsing code for GetLocalIPAddresses().
787
788   This function is split out, so we can unit test it.
789
790   """
791   re_ip = re.compile('^(\d+\.\d+\.\d+\.\d+)(?:/\d+)$')
792
793   ips = []
794   for line in output.splitlines(False):
795     fields = line.split()
796     if len(line) < 4:
797       continue
798     m = re_ip.match(fields[3])
799     if m:
800       ips.append(m.group(1))
801
802   return ips
803
804
805 def GetLocalIPAddresses():
806   """Gets a list of all local IP addresses.
807
808   Should this break one day, a small Python module written in C could
809   use the API call getifaddrs().
810
811   """
812   result = RunCmd(["ip", "-family", "inet", "-oneline", "addr", "show"])
813   if result.failed:
814     raise errors.OpExecError("Command '%s' failed, error: %s,"
815       " output: %s" % (result.cmd, result.fail_reason, result.output))
816
817   return _ParseIpOutput(result.output)
818
819
820 def TcpPing(source, target, port, timeout=10, live_port_needed=True):
821   """Simple ping implementation using TCP connect(2).
822
823   Try to do a TCP connect(2) from the specified source IP to the specified
824   target IP and the specified target port. If live_port_needed is set to true,
825   requires the remote end to accept the connection. The timeout is specified
826   in seconds and defaults to 10 seconds
827
828   """
829   sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
830
831   sucess = False
832
833   try:
834     sock.bind((source, 0))
835   except socket.error, (errcode, errstring):
836     if errcode == errno.EADDRNOTAVAIL:
837       success = False
838
839   sock.settimeout(timeout)
840
841   try:
842     sock.connect((target, port))
843     sock.close()
844     success = True
845   except socket.timeout:
846     success = False
847   except socket.error, (errcode, errstring):
848     success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
849
850   return success
851
852
853 def ListVisibleFiles(path):
854   """Returns a list of all visible files in a directory.
855
856   """
857   return [i for i in os.listdir(path) if not i.startswith(".")]