Add autocleanup of stale locks
[ganeti-local] / lib / utils.py
1 #
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
27 import sys
28 import os
29 import sha
30 import time
31 import subprocess
32 import re
33 import socket
34 import tempfile
35 import shutil
36 import errno
37 import pwd
38 import itertools
39 import select
40 import fcntl
41
42 from cStringIO import StringIO
43
44 from ganeti import logger
45 from ganeti import errors
46 from ganeti import constants
47
48
49 _locksheld = []
50 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
51
52 debug = False
53
54 class RunResult(object):
55   """Simple class for holding the result of running external programs.
56
57   Instance variables:
58     exit_code: the exit code of the program, or None (if the program
59                didn't exit())
60     signal: numeric signal that caused the program to finish, or None
61             (if the program wasn't terminated by a signal)
62     stdout: the standard output of the program
63     stderr: the standard error of the program
64     failed: a Boolean value which is True in case the program was
65             terminated by a signal or exited with a non-zero exit code
66     fail_reason: a string detailing the termination reason
67
68   """
69   __slots__ = ["exit_code", "signal", "stdout", "stderr",
70                "failed", "fail_reason", "cmd"]
71
72
73   def __init__(self, exit_code, signal, stdout, stderr, cmd):
74     self.cmd = cmd
75     self.exit_code = exit_code
76     self.signal = signal
77     self.stdout = stdout
78     self.stderr = stderr
79     self.failed = (signal is not None or exit_code != 0)
80
81     if self.signal is not None:
82       self.fail_reason = "terminated by signal %s" % self.signal
83     elif self.exit_code is not None:
84       self.fail_reason = "exited with exit code %s" % self.exit_code
85     else:
86       self.fail_reason = "unable to determine termination reason"
87
88     if debug and self.failed:
89       logger.Debug("Command '%s' failed (%s); output: %s" %
90                    (self.cmd, self.fail_reason, self.output))
91
92   def _GetOutput(self):
93     """Returns the combined stdout and stderr for easier usage.
94
95     """
96     return self.stdout + self.stderr
97
98   output = property(_GetOutput, None, None, "Return full output")
99
100
101 def _GetLockFile(subsystem):
102   """Compute the file name for a given lock name."""
103   return "%s/ganeti_lock_%s" % (constants.LOCK_DIR, subsystem)
104
105
106 def Lock(name, max_retries=None, debug=False, autoclean=True):
107   """Lock a given subsystem.
108
109   In case the lock is already held by an alive process, the function
110   will sleep indefintely and poll with a one second interval.
111
112   When the optional integer argument 'max_retries' is passed with a
113   non-zero value, the function will sleep only for this number of
114   times, and then it will will raise a LockError if the lock can't be
115   acquired. Passing in a negative number will cause only one try to
116   get the lock. Passing a positive number will make the function retry
117   for approximately that number of seconds.
118
119   """
120   lockfile = _GetLockFile(name)
121
122   if name in _locksheld:
123     raise errors.LockError('Lock "%s" already held!' % (name,))
124
125   errcount = 0
126   cleanupcount = 0
127
128   retries = 0
129   while True:
130     try:
131       fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
132       break
133     except OSError, creat_err:
134       if creat_err.errno != errno.EEXIST:
135         raise errors.LockError("Can't create the lock file. Error '%s'." %
136                                str(creat_err))
137
138       try:
139         pf = open(lockfile, 'r')
140       except IOError, open_err:
141         errcount += 1
142         if errcount >= 5:
143           raise errors.LockError("Lock file exists but cannot be opened."
144                                  " Error: '%s'." % str(open_err))
145         time.sleep(1)
146         continue
147
148       try:
149         pid = int(pf.read())
150       except ValueError:
151         raise errors.LockError("Invalid pid string in %s" %
152                                (lockfile,))
153
154       if not IsProcessAlive(pid):
155         if autoclean:
156           cleanupcount += 1
157           if cleanupcount >= 5:
158             raise errors.LockError, ("Too many stale lock cleanups! Check"
159                                      " what process is dying.")
160           logger.Error('Stale lockfile %s for pid %d?' % (lockfile, pid))
161           RemoveFile(lockfile)
162           continue
163         else:
164           raise errors.LockError("Stale lockfile %s for pid %d?" %
165                                  (lockfile, pid))
166
167       if max_retries and max_retries <= retries:
168         raise errors.LockError("Can't acquire lock during the specified"
169                                " time, aborting.")
170       if retries == 5 and (debug or sys.stdin.isatty()):
171         logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
172
173       time.sleep(1)
174       retries += 1
175       continue
176
177   os.write(fd, '%d\n' % (os.getpid(),))
178   os.close(fd)
179
180   _locksheld.append(name)
181
182
183 def Unlock(name):
184   """Unlock a given subsystem.
185
186   """
187   lockfile = _GetLockFile(name)
188
189   try:
190     fd = os.open(lockfile, os.O_RDONLY)
191   except OSError:
192     raise errors.LockError('Lock "%s" not held.' % (name,))
193
194   f = os.fdopen(fd, 'r')
195   pid_str = f.read()
196
197   try:
198     pid = int(pid_str)
199   except ValueError:
200     raise errors.LockError('Unable to determine PID of locking process.')
201
202   if pid != os.getpid():
203     raise errors.LockError('Lock not held by me (%d != %d)' %
204                            (os.getpid(), pid,))
205
206   os.unlink(lockfile)
207   _locksheld.remove(name)
208
209
210 def LockCleanup():
211   """Remove all locks.
212
213   """
214   for lock in _locksheld:
215     Unlock(lock)
216
217
218 def RunCmd(cmd):
219   """Execute a (shell) command.
220
221   The command should not read from its standard input, as it will be
222   closed.
223
224   Args:
225     cmd: command to run. (str)
226
227   Returns: `RunResult` instance
228
229   """
230   if isinstance(cmd, list):
231     cmd = [str(val) for val in cmd]
232     strcmd = " ".join(cmd)
233     shell = False
234   else:
235     strcmd = cmd
236     shell = True
237   env = os.environ.copy()
238   env["LC_ALL"] = "C"
239   poller = select.poll()
240   child = subprocess.Popen(cmd, shell=shell,
241                            stderr=subprocess.PIPE,
242                            stdout=subprocess.PIPE,
243                            stdin=subprocess.PIPE,
244                            close_fds=True, env=env)
245
246   child.stdin.close()
247   poller.register(child.stdout, select.POLLIN)
248   poller.register(child.stderr, select.POLLIN)
249   out = StringIO()
250   err = StringIO()
251   fdmap = {
252     child.stdout.fileno(): (out, child.stdout),
253     child.stderr.fileno(): (err, child.stderr),
254     }
255   for fd in fdmap:
256     status = fcntl.fcntl(fd, fcntl.F_GETFL)
257     fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
258
259   while fdmap:
260     for fd, event in poller.poll():
261       if event & select.POLLIN or event & select.POLLPRI:
262         data = fdmap[fd][1].read()
263         # no data from read signifies EOF (the same as POLLHUP)
264         if not data:
265           poller.unregister(fd)
266           del fdmap[fd]
267           continue
268         fdmap[fd][0].write(data)
269       if (event & select.POLLNVAL or event & select.POLLHUP or
270           event & select.POLLERR):
271         poller.unregister(fd)
272         del fdmap[fd]
273
274   out = out.getvalue()
275   err = err.getvalue()
276
277   status = child.wait()
278   if status >= 0:
279     exitcode = status
280     signal = None
281   else:
282     exitcode = None
283     signal = -status
284
285   return RunResult(exitcode, signal, out, err, strcmd)
286
287
288 def RunCmdUnlocked(cmd):
289   """Execute a shell command without the 'cmd' lock.
290
291   This variant of `RunCmd()` drops the 'cmd' lock before running the
292   command and re-aquires it afterwards, thus it can be used to call
293   other ganeti commands.
294
295   The argument and return values are the same as for the `RunCmd()`
296   function.
297
298   Args:
299     cmd - command to run. (str)
300
301   Returns:
302     `RunResult`
303
304   """
305   Unlock('cmd')
306   ret = RunCmd(cmd)
307   Lock('cmd')
308
309   return ret
310
311
312 def RemoveFile(filename):
313   """Remove a file ignoring some errors.
314
315   Remove a file, ignoring non-existing ones or directories. Other
316   errors are passed.
317
318   """
319   try:
320     os.unlink(filename)
321   except OSError, err:
322     if err.errno not in (errno.ENOENT, errno.EISDIR):
323       raise
324
325
326 def _FingerprintFile(filename):
327   """Compute the fingerprint of a file.
328
329   If the file does not exist, a None will be returned
330   instead.
331
332   Args:
333     filename - Filename (str)
334
335   """
336   if not (os.path.exists(filename) and os.path.isfile(filename)):
337     return None
338
339   f = open(filename)
340
341   fp = sha.sha()
342   while True:
343     data = f.read(4096)
344     if not data:
345       break
346
347     fp.update(data)
348
349   return fp.hexdigest()
350
351
352 def FingerprintFiles(files):
353   """Compute fingerprints for a list of files.
354
355   Args:
356     files - array of filenames.  ( [str, ...] )
357
358   Return value:
359     dictionary of filename: fingerprint for the files that exist
360
361   """
362   ret = {}
363
364   for filename in files:
365     cksum = _FingerprintFile(filename)
366     if cksum:
367       ret[filename] = cksum
368
369   return ret
370
371
372 def CheckDict(target, template, logname=None):
373   """Ensure a dictionary has a required set of keys.
374
375   For the given dictionaries `target` and `template`, ensure target
376   has all the keys from template. Missing keys are added with values
377   from template.
378
379   Args:
380     target   - the dictionary to check
381     template - template dictionary
382     logname  - a caller-chosen string to identify the debug log
383                entry; if None, no logging will be done
384
385   Returns value:
386     None
387
388   """
389   missing = []
390   for k in template:
391     if k not in target:
392       missing.append(k)
393       target[k] = template[k]
394
395   if missing and logname:
396     logger.Debug('%s missing keys %s' %
397                  (logname, ', '.join(missing)))
398
399
400 def IsProcessAlive(pid):
401   """Check if a given pid exists on the system.
402
403   Returns: true or false, depending on if the pid exists or not
404
405   Remarks: zombie processes treated as not alive
406
407   """
408   try:
409     f = open("/proc/%d/status" % pid)
410   except IOError, err:
411     if err.errno in (errno.ENOENT, errno.ENOTDIR):
412       return False
413
414   alive = True
415   try:
416     data = f.readlines()
417     if len(data) > 1:
418       state = data[1].split()
419       if len(state) > 1 and state[1] == "Z":
420         alive = False
421   finally:
422     f.close()
423
424   return alive
425
426
427 def MatchNameComponent(key, name_list):
428   """Try to match a name against a list.
429
430   This function will try to match a name like test1 against a list
431   like ['test1.example.com', 'test2.example.com', ...]. Against this
432   list, 'test1' as well as 'test1.example' will match, but not
433   'test1.ex'. A multiple match will be considered as no match at all
434   (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
435
436   Args:
437     key: the name to be searched
438     name_list: the list of strings against which to search the key
439
440   Returns:
441     None if there is no match *or* if there are multiple matches
442     otherwise the element from the list which matches
443
444   """
445   mo = re.compile("^%s(\..*)?$" % re.escape(key))
446   names_filtered = [name for name in name_list if mo.match(name) is not None]
447   if len(names_filtered) != 1:
448     return None
449   return names_filtered[0]
450
451
452 class HostInfo:
453   """Class implementing resolver and hostname functionality
454
455   """
456   def __init__(self, name=None):
457     """Initialize the host name object.
458
459     If the name argument is not passed, it will use this system's
460     name.
461
462     """
463     if name is None:
464       name = self.SysName()
465
466     self.query = name
467     self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
468     self.ip = self.ipaddrs[0]
469
470   def ShortName(self):
471     """Returns the hostname without domain.
472
473     """
474     return self.name.split('.')[0]
475
476   @staticmethod
477   def SysName():
478     """Return the current system's name.
479
480     This is simply a wrapper over socket.gethostname()
481
482     """
483     return socket.gethostname()
484
485   @staticmethod
486   def LookupHostname(hostname):
487     """Look up hostname
488
489     Args:
490       hostname: hostname to look up
491
492     Returns:
493       a tuple (name, aliases, ipaddrs) as returned by socket.gethostbyname_ex
494       in case of errors in resolving, we raise a ResolverError
495
496     """
497     try:
498       result = socket.gethostbyname_ex(hostname)
499     except socket.gaierror, err:
500       # hostname not found in DNS
501       raise errors.ResolverError(hostname, err.args[0], err.args[1])
502
503     return result
504
505
506 def ListVolumeGroups():
507   """List volume groups and their size
508
509   Returns:
510      Dictionary with keys volume name and values the size of the volume
511
512   """
513   command = "vgs --noheadings --units m --nosuffix -o name,size"
514   result = RunCmd(command)
515   retval = {}
516   if result.failed:
517     return retval
518
519   for line in result.stdout.splitlines():
520     try:
521       name, size = line.split()
522       size = int(float(size))
523     except (IndexError, ValueError), err:
524       logger.Error("Invalid output from vgs (%s): %s" % (err, line))
525       continue
526
527     retval[name] = size
528
529   return retval
530
531
532 def BridgeExists(bridge):
533   """Check whether the given bridge exists in the system
534
535   Returns:
536      True if it does, false otherwise.
537
538   """
539   return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
540
541
542 def NiceSort(name_list):
543   """Sort a list of strings based on digit and non-digit groupings.
544
545   Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
546   sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
547
548   The sort algorithm breaks each name in groups of either only-digits
549   or no-digits. Only the first eight such groups are considered, and
550   after that we just use what's left of the string.
551
552   Return value
553     - a copy of the list sorted according to our algorithm
554
555   """
556   _SORTER_BASE = "(\D+|\d+)"
557   _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
558                                                   _SORTER_BASE, _SORTER_BASE,
559                                                   _SORTER_BASE, _SORTER_BASE,
560                                                   _SORTER_BASE, _SORTER_BASE)
561   _SORTER_RE = re.compile(_SORTER_FULL)
562   _SORTER_NODIGIT = re.compile("^\D*$")
563   def _TryInt(val):
564     """Attempts to convert a variable to integer."""
565     if val is None or _SORTER_NODIGIT.match(val):
566       return val
567     rval = int(val)
568     return rval
569
570   to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
571              for name in name_list]
572   to_sort.sort()
573   return [tup[1] for tup in to_sort]
574
575
576 def CheckDaemonAlive(pid_file, process_string):
577   """Check wether the specified daemon is alive.
578
579   Args:
580    - pid_file: file to read the daemon pid from, the file is
581                expected to contain only a single line containing
582                only the PID
583    - process_string: a substring that we expect to find in
584                      the command line of the daemon process
585
586   Returns:
587    - True if the daemon is judged to be alive (that is:
588       - the PID file exists, is readable and contains a number
589       - a process of the specified PID is running
590       - that process contains the specified string in its
591         command line
592       - the process is not in state Z (zombie))
593    - False otherwise
594
595   """
596   try:
597     pid_file = file(pid_file, 'r')
598     try:
599       pid = int(pid_file.readline())
600     finally:
601       pid_file.close()
602
603     cmdline_file_path = "/proc/%s/cmdline" % (pid)
604     cmdline_file = open(cmdline_file_path, 'r')
605     try:
606       cmdline = cmdline_file.readline()
607     finally:
608       cmdline_file.close()
609
610     if not process_string in cmdline:
611       return False
612
613     stat_file_path =  "/proc/%s/stat" % (pid)
614     stat_file = open(stat_file_path, 'r')
615     try:
616       process_state = stat_file.readline().split()[2]
617     finally:
618       stat_file.close()
619
620     if process_state == 'Z':
621       return False
622
623   except (IndexError, IOError, ValueError):
624     return False
625
626   return True
627
628
629 def TryConvert(fn, val):
630   """Try to convert a value ignoring errors.
631
632   This function tries to apply function `fn` to `val`. If no
633   ValueError or TypeError exceptions are raised, it will return the
634   result, else it will return the original value. Any other exceptions
635   are propagated to the caller.
636
637   """
638   try:
639     nv = fn(val)
640   except (ValueError, TypeError), err:
641     nv = val
642   return nv
643
644
645 def IsValidIP(ip):
646   """Verifies the syntax of an IP address.
647
648   This function checks if the ip address passes is valid or not based
649   on syntax (not ip range, class calculations or anything).
650
651   """
652   unit = "(0|[1-9]\d{0,2})"
653   return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
654
655
656 def IsValidShellParam(word):
657   """Verifies is the given word is safe from the shell's p.o.v.
658
659   This means that we can pass this to a command via the shell and be
660   sure that it doesn't alter the command line and is passed as such to
661   the actual command.
662
663   Note that we are overly restrictive here, in order to be on the safe
664   side.
665
666   """
667   return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
668
669
670 def BuildShellCmd(template, *args):
671   """Build a safe shell command line from the given arguments.
672
673   This function will check all arguments in the args list so that they
674   are valid shell parameters (i.e. they don't contain shell
675   metacharaters). If everything is ok, it will return the result of
676   template % args.
677
678   """
679   for word in args:
680     if not IsValidShellParam(word):
681       raise errors.ProgrammerError("Shell argument '%s' contains"
682                                    " invalid characters" % word)
683   return template % args
684
685
686 def FormatUnit(value):
687   """Formats an incoming number of MiB with the appropriate unit.
688
689   Value needs to be passed as a numeric type. Return value is always a string.
690
691   """
692   if value < 1024:
693     return "%dM" % round(value, 0)
694
695   elif value < (1024 * 1024):
696     return "%0.1fG" % round(float(value) / 1024, 1)
697
698   else:
699     return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
700
701
702 def ParseUnit(input_string):
703   """Tries to extract number and scale from the given string.
704
705   Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
706   is specified, it defaults to MiB. Return value is always an int in MiB.
707
708   """
709   m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
710   if not m:
711     raise errors.UnitParseError("Invalid format")
712
713   value = float(m.groups()[0])
714
715   unit = m.groups()[1]
716   if unit:
717     lcunit = unit.lower()
718   else:
719     lcunit = 'm'
720
721   if lcunit in ('m', 'mb', 'mib'):
722     # Value already in MiB
723     pass
724
725   elif lcunit in ('g', 'gb', 'gib'):
726     value *= 1024
727
728   elif lcunit in ('t', 'tb', 'tib'):
729     value *= 1024 * 1024
730
731   else:
732     raise errors.UnitParseError("Unknown unit: %s" % unit)
733
734   # Make sure we round up
735   if int(value) < value:
736     value += 1
737
738   # Round up to the next multiple of 4
739   value = int(value)
740   if value % 4:
741     value += 4 - value % 4
742
743   return value
744
745
746 def AddAuthorizedKey(file_name, key):
747   """Adds an SSH public key to an authorized_keys file.
748
749   Args:
750     file_name: Path to authorized_keys file
751     key: String containing key
752   """
753   key_fields = key.split()
754
755   f = open(file_name, 'a+')
756   try:
757     nl = True
758     for line in f:
759       # Ignore whitespace changes
760       if line.split() == key_fields:
761         break
762       nl = line.endswith('\n')
763     else:
764       if not nl:
765         f.write("\n")
766       f.write(key.rstrip('\r\n'))
767       f.write("\n")
768       f.flush()
769   finally:
770     f.close()
771
772
773 def RemoveAuthorizedKey(file_name, key):
774   """Removes an SSH public key from an authorized_keys file.
775
776   Args:
777     file_name: Path to authorized_keys file
778     key: String containing key
779   """
780   key_fields = key.split()
781
782   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
783   try:
784     out = os.fdopen(fd, 'w')
785     try:
786       f = open(file_name, 'r')
787       try:
788         for line in f:
789           # Ignore whitespace changes while comparing lines
790           if line.split() != key_fields:
791             out.write(line)
792
793         out.flush()
794         os.rename(tmpname, file_name)
795       finally:
796         f.close()
797     finally:
798       out.close()
799   except:
800     RemoveFile(tmpname)
801     raise
802
803
804 def SetEtcHostsEntry(file_name, ip, hostname, aliases):
805   """Sets the name of an IP address and hostname in /etc/hosts.
806
807   """
808   # Ensure aliases are unique
809   aliases = UniqueSequence([hostname] + aliases)[1:]
810
811   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
812   try:
813     out = os.fdopen(fd, 'w')
814     try:
815       f = open(file_name, 'r')
816       try:
817         written = False
818         for line in f:
819           fields = line.split()
820           if fields and not fields[0].startswith('#') and ip == fields[0]:
821             continue
822           out.write(line)
823
824         out.write("%s\t%s" % (ip, hostname))
825         if aliases:
826           out.write(" %s" % ' '.join(aliases))
827         out.write('\n')
828
829         out.flush()
830         os.fsync(out)
831         os.rename(tmpname, file_name)
832       finally:
833         f.close()
834     finally:
835       out.close()
836   except:
837     RemoveFile(tmpname)
838     raise
839
840
841 def RemoveEtcHostsEntry(file_name, hostname):
842   """Removes a hostname from /etc/hosts.
843
844   IP addresses without names are removed from the file.
845   """
846   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
847   try:
848     out = os.fdopen(fd, 'w')
849     try:
850       f = open(file_name, 'r')
851       try:
852         for line in f:
853           fields = line.split()
854           if len(fields) > 1 and not fields[0].startswith('#'):
855             names = fields[1:]
856             if hostname in names:
857               while hostname in names:
858                 names.remove(hostname)
859               if names:
860                 out.write("%s %s\n" % (fields[0], ' '.join(names)))
861               continue
862
863           out.write(line)
864
865         out.flush()
866         os.fsync(out)
867         os.rename(tmpname, file_name)
868       finally:
869         f.close()
870     finally:
871       out.close()
872   except:
873     RemoveFile(tmpname)
874     raise
875
876
877 def CreateBackup(file_name):
878   """Creates a backup of a file.
879
880   Returns: the path to the newly created backup file.
881
882   """
883   if not os.path.isfile(file_name):
884     raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
885                                 file_name)
886
887   prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
888   dir_name = os.path.dirname(file_name)
889
890   fsrc = open(file_name, 'rb')
891   try:
892     (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
893     fdst = os.fdopen(fd, 'wb')
894     try:
895       shutil.copyfileobj(fsrc, fdst)
896     finally:
897       fdst.close()
898   finally:
899     fsrc.close()
900
901   return backup_name
902
903
904 def ShellQuote(value):
905   """Quotes shell argument according to POSIX.
906
907   """
908   if _re_shell_unquoted.match(value):
909     return value
910   else:
911     return "'%s'" % value.replace("'", "'\\''")
912
913
914 def ShellQuoteArgs(args):
915   """Quotes all given shell arguments and concatenates using spaces.
916
917   """
918   return ' '.join([ShellQuote(i) for i in args])
919
920
921 def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
922   """Simple ping implementation using TCP connect(2).
923
924   Try to do a TCP connect(2) from an optional source IP to the
925   specified target IP and the specified target port. If the optional
926   parameter live_port_needed is set to true, requires the remote end
927   to accept the connection. The timeout is specified in seconds and
928   defaults to 10 seconds. If the source optional argument is not
929   passed, the source address selection is left to the kernel,
930   otherwise we try to connect using the passed address (failures to
931   bind other than EADDRNOTAVAIL will be ignored).
932
933   """
934   sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
935
936   sucess = False
937
938   if source is not None:
939     try:
940       sock.bind((source, 0))
941     except socket.error, (errcode, errstring):
942       if errcode == errno.EADDRNOTAVAIL:
943         success = False
944
945   sock.settimeout(timeout)
946
947   try:
948     sock.connect((target, port))
949     sock.close()
950     success = True
951   except socket.timeout:
952     success = False
953   except socket.error, (errcode, errstring):
954     success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
955
956   return success
957
958
959 def ListVisibleFiles(path):
960   """Returns a list of all visible files in a directory.
961
962   """
963   files = [i for i in os.listdir(path) if not i.startswith(".")]
964   files.sort()
965   return files
966
967
968 def GetHomeDir(user, default=None):
969   """Try to get the homedir of the given user.
970
971   The user can be passed either as a string (denoting the name) or as
972   an integer (denoting the user id). If the user is not found, the
973   'default' argument is returned, which defaults to None.
974
975   """
976   try:
977     if isinstance(user, basestring):
978       result = pwd.getpwnam(user)
979     elif isinstance(user, (int, long)):
980       result = pwd.getpwuid(user)
981     else:
982       raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
983                                    type(user))
984   except KeyError:
985     return default
986   return result.pw_dir
987
988
989 def NewUUID():
990   """Returns a random UUID.
991
992   """
993   f = open("/proc/sys/kernel/random/uuid", "r")
994   try:
995     return f.read(128).rstrip("\n")
996   finally:
997     f.close()
998
999
1000 def WriteFile(file_name, fn=None, data=None,
1001               mode=None, uid=-1, gid=-1,
1002               atime=None, mtime=None):
1003   """(Over)write a file atomically.
1004
1005   The file_name and either fn (a function taking one argument, the
1006   file descriptor, and which should write the data to it) or data (the
1007   contents of the file) must be passed. The other arguments are
1008   optional and allow setting the file mode, owner and group, and the
1009   mtime/atime of the file.
1010
1011   If the function doesn't raise an exception, it has succeeded and the
1012   target file has the new contents. If the file has raised an
1013   exception, an existing target file should be unmodified and the
1014   temporary file should be removed.
1015
1016   """
1017   if not os.path.isabs(file_name):
1018     raise errors.ProgrammerError("Path passed to WriteFile is not"
1019                                  " absolute: '%s'" % file_name)
1020
1021   if [fn, data].count(None) != 1:
1022     raise errors.ProgrammerError("fn or data required")
1023
1024   if [atime, mtime].count(None) == 1:
1025     raise errors.ProgrammerError("Both atime and mtime must be either"
1026                                  " set or None")
1027
1028
1029   dir_name, base_name = os.path.split(file_name)
1030   fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1031   # here we need to make sure we remove the temp file, if any error
1032   # leaves it in place
1033   try:
1034     if uid != -1 or gid != -1:
1035       os.chown(new_name, uid, gid)
1036     if mode:
1037       os.chmod(new_name, mode)
1038     if data is not None:
1039       os.write(fd, data)
1040     else:
1041       fn(fd)
1042     os.fsync(fd)
1043     if atime is not None and mtime is not None:
1044       os.utime(new_name, (atime, mtime))
1045     os.rename(new_name, file_name)
1046   finally:
1047     os.close(fd)
1048     RemoveFile(new_name)
1049
1050
1051 def all(seq, pred=bool):
1052   "Returns True if pred(x) is True for every element in the iterable"
1053   for elem in itertools.ifilterfalse(pred, seq):
1054     return False
1055   return True
1056
1057
1058 def any(seq, pred=bool):
1059   "Returns True if pred(x) is True for at least one element in the iterable"
1060   for elem in itertools.ifilter(pred, seq):
1061     return True
1062   return False
1063
1064
1065 def UniqueSequence(seq):
1066   """Returns a list with unique elements.
1067
1068   Element order is preserved.
1069   """
1070   seen = set()
1071   return [i for i in seq if i not in seen and not seen.add(i)]
1072
1073
1074 def IsValidMac(mac):
1075   """Predicate to check if a MAC address is valid.
1076
1077   Checks wether the supplied MAC address is formally correct, only
1078   accepts colon separated format.
1079   """
1080   mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
1081   return mac_check.match(mac) is not None
1082
1083
1084 def TestDelay(duration):
1085   """Sleep for a fixed amount of time.
1086
1087   """
1088   if duration < 0:
1089     return False
1090   time.sleep(duration)
1091   return True