ganeti.http: Add constant for DELETE
[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 utility module.
23
24 This module holds functions that can be used in both daemons (all) and
25 the command line scripts.
26
27 """
28
29
30 import sys
31 import os
32 import sha
33 import time
34 import subprocess
35 import re
36 import socket
37 import tempfile
38 import shutil
39 import errno
40 import pwd
41 import itertools
42 import select
43 import fcntl
44 import resource
45 import logging
46 import signal
47
48 from cStringIO import StringIO
49
50 from ganeti import errors
51 from ganeti import constants
52
53
54 _locksheld = []
55 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
56
57 debug = False
58 debug_locks = False
59
60 #: when set to True, L{RunCmd} is disabled
61 no_fork = False
62
63
64 class RunResult(object):
65   """Holds the result of running external programs.
66
67   @type exit_code: int
68   @ivar exit_code: the exit code of the program, or None (if the program
69       didn't exit())
70   @type signal: int or None
71   @ivar signal: the signal that caused the program to finish, or None
72       (if the program wasn't terminated by a signal)
73   @type stdout: str
74   @ivar stdout: the standard output of the program
75   @type stderr: str
76   @ivar stderr: the standard error of the program
77   @type failed: boolean
78   @ivar failed: True in case the program was
79       terminated by a signal or exited with a non-zero exit code
80   @ivar fail_reason: a string detailing the termination reason
81
82   """
83   __slots__ = ["exit_code", "signal", "stdout", "stderr",
84                "failed", "fail_reason", "cmd"]
85
86
87   def __init__(self, exit_code, signal_, stdout, stderr, cmd):
88     self.cmd = cmd
89     self.exit_code = exit_code
90     self.signal = signal_
91     self.stdout = stdout
92     self.stderr = stderr
93     self.failed = (signal_ is not None or exit_code != 0)
94
95     if self.signal is not None:
96       self.fail_reason = "terminated by signal %s" % self.signal
97     elif self.exit_code is not None:
98       self.fail_reason = "exited with exit code %s" % self.exit_code
99     else:
100       self.fail_reason = "unable to determine termination reason"
101
102     if self.failed:
103       logging.debug("Command '%s' failed (%s); output: %s",
104                     self.cmd, self.fail_reason, self.output)
105
106   def _GetOutput(self):
107     """Returns the combined stdout and stderr for easier usage.
108
109     """
110     return self.stdout + self.stderr
111
112   output = property(_GetOutput, None, None, "Return full output")
113
114
115 def RunCmd(cmd, env=None, output=None, cwd='/'):
116   """Execute a (shell) command.
117
118   The command should not read from its standard input, as it will be
119   closed.
120
121   @type  cmd: string or list
122   @param cmd: Command to run
123   @type env: dict
124   @param env: Additional environment
125   @type output: str
126   @param output: if desired, the output of the command can be
127       saved in a file instead of the RunResult instance; this
128       parameter denotes the file name (if not None)
129   @type cwd: string
130   @param cwd: if specified, will be used as the working
131       directory for the command; the default will be /
132   @rtype: L{RunResult}
133   @return: RunResult instance
134   @raise erors.ProgrammerError: if we call this when forks are disabled
135
136   """
137   if no_fork:
138     raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
139
140   if isinstance(cmd, list):
141     cmd = [str(val) for val in cmd]
142     strcmd = " ".join(cmd)
143     shell = False
144   else:
145     strcmd = cmd
146     shell = True
147   logging.debug("RunCmd '%s'", strcmd)
148
149   cmd_env = os.environ.copy()
150   cmd_env["LC_ALL"] = "C"
151   if env is not None:
152     cmd_env.update(env)
153
154   if output is None:
155     out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd)
156   else:
157     status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
158     out = err = ""
159
160   if status >= 0:
161     exitcode = status
162     signal_ = None
163   else:
164     exitcode = None
165     signal_ = -status
166
167   return RunResult(exitcode, signal_, out, err, strcmd)
168
169 def _RunCmdPipe(cmd, env, via_shell, cwd):
170   """Run a command and return its output.
171
172   @type  cmd: string or list
173   @param cmd: Command to run
174   @type env: dict
175   @param env: The environment to use
176   @type via_shell: bool
177   @param via_shell: if we should run via the shell
178   @type cwd: string
179   @param cwd: the working directory for the program
180   @rtype: tuple
181   @return: (out, err, status)
182
183   """
184   poller = select.poll()
185   child = subprocess.Popen(cmd, shell=via_shell,
186                            stderr=subprocess.PIPE,
187                            stdout=subprocess.PIPE,
188                            stdin=subprocess.PIPE,
189                            close_fds=True, env=env,
190                            cwd=cwd)
191
192   child.stdin.close()
193   poller.register(child.stdout, select.POLLIN)
194   poller.register(child.stderr, select.POLLIN)
195   out = StringIO()
196   err = StringIO()
197   fdmap = {
198     child.stdout.fileno(): (out, child.stdout),
199     child.stderr.fileno(): (err, child.stderr),
200     }
201   for fd in fdmap:
202     status = fcntl.fcntl(fd, fcntl.F_GETFL)
203     fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
204
205   while fdmap:
206     for fd, event in poller.poll():
207       if event & select.POLLIN or event & select.POLLPRI:
208         data = fdmap[fd][1].read()
209         # no data from read signifies EOF (the same as POLLHUP)
210         if not data:
211           poller.unregister(fd)
212           del fdmap[fd]
213           continue
214         fdmap[fd][0].write(data)
215       if (event & select.POLLNVAL or event & select.POLLHUP or
216           event & select.POLLERR):
217         poller.unregister(fd)
218         del fdmap[fd]
219
220   out = out.getvalue()
221   err = err.getvalue()
222
223   status = child.wait()
224   return out, err, status
225
226
227 def _RunCmdFile(cmd, env, via_shell, output, cwd):
228   """Run a command and save its output to a file.
229
230   @type  cmd: string or list
231   @param cmd: Command to run
232   @type env: dict
233   @param env: The environment to use
234   @type via_shell: bool
235   @param via_shell: if we should run via the shell
236   @type output: str
237   @param output: the filename in which to save the output
238   @type cwd: string
239   @param cwd: the working directory for the program
240   @rtype: int
241   @return: the exit status
242
243   """
244   fh = open(output, "a")
245   try:
246     child = subprocess.Popen(cmd, shell=via_shell,
247                              stderr=subprocess.STDOUT,
248                              stdout=fh,
249                              stdin=subprocess.PIPE,
250                              close_fds=True, env=env,
251                              cwd=cwd)
252
253     child.stdin.close()
254     status = child.wait()
255   finally:
256     fh.close()
257   return status
258
259
260 def RemoveFile(filename):
261   """Remove a file ignoring some errors.
262
263   Remove a file, ignoring non-existing ones or directories. Other
264   errors are passed.
265
266   @type filename: str
267   @param filename: the file to be removed
268
269   """
270   try:
271     os.unlink(filename)
272   except OSError, err:
273     if err.errno not in (errno.ENOENT, errno.EISDIR):
274       raise
275
276
277 def _FingerprintFile(filename):
278   """Compute the fingerprint of a file.
279
280   If the file does not exist, a None will be returned
281   instead.
282
283   @type filename: str
284   @param filename: the filename to checksum
285   @rtype: str
286   @return: the hex digest of the sha checksum of the contents
287       of the file
288
289   """
290   if not (os.path.exists(filename) and os.path.isfile(filename)):
291     return None
292
293   f = open(filename)
294
295   fp = sha.sha()
296   while True:
297     data = f.read(4096)
298     if not data:
299       break
300
301     fp.update(data)
302
303   return fp.hexdigest()
304
305
306 def FingerprintFiles(files):
307   """Compute fingerprints for a list of files.
308
309   @type files: list
310   @param files: the list of filename to fingerprint
311   @rtype: dict
312   @return: a dictionary filename: fingerprint, holding only
313       existing files
314
315   """
316   ret = {}
317
318   for filename in files:
319     cksum = _FingerprintFile(filename)
320     if cksum:
321       ret[filename] = cksum
322
323   return ret
324
325
326 def CheckDict(target, template, logname=None):
327   """Ensure a dictionary has a required set of keys.
328
329   For the given dictionaries I{target} and I{template}, ensure
330   I{target} has all the keys from I{template}. Missing keys are added
331   with values from template.
332
333   @type target: dict
334   @param target: the dictionary to update
335   @type template: dict
336   @param template: the dictionary holding the default values
337   @type logname: str or None
338   @param logname: if not None, causes the missing keys to be
339       logged with this name
340
341   """
342   missing = []
343   for k in template:
344     if k not in target:
345       missing.append(k)
346       target[k] = template[k]
347
348   if missing and logname:
349     logging.warning('%s missing keys %s', logname, ', '.join(missing))
350
351
352 def IsProcessAlive(pid):
353   """Check if a given pid exists on the system.
354
355   @note: zombie status is not handled, so zombie processes
356       will be returned as alive
357   @type pid: int
358   @param pid: the process ID to check
359   @rtype: boolean
360   @return: True if the process exists
361
362   """
363   if pid <= 0:
364     return False
365
366   try:
367     os.stat("/proc/%d/status" % pid)
368     return True
369   except EnvironmentError, err:
370     if err.errno in (errno.ENOENT, errno.ENOTDIR):
371       return False
372     raise
373
374
375 def ReadPidFile(pidfile):
376   """Read a pid from a file.
377
378   @type  pidfile: string
379   @param pidfile: path to the file containing the pid
380   @rtype: int
381   @return: The process id, if the file exista and contains a valid PID,
382            otherwise 0
383
384   """
385   try:
386     pf = open(pidfile, 'r')
387   except EnvironmentError, err:
388     if err.errno != errno.ENOENT:
389       logging.exception("Can't read pid file?!")
390     return 0
391
392   try:
393     pid = int(pf.read())
394   except ValueError, err:
395     logging.info("Can't parse pid file contents", exc_info=True)
396     return 0
397
398   return pid
399
400
401 def MatchNameComponent(key, name_list):
402   """Try to match a name against a list.
403
404   This function will try to match a name like test1 against a list
405   like C{['test1.example.com', 'test2.example.com', ...]}. Against
406   this list, I{'test1'} as well as I{'test1.example'} will match, but
407   not I{'test1.ex'}. A multiple match will be considered as no match
408   at all (e.g. I{'test1'} against C{['test1.example.com',
409   'test1.example.org']}).
410
411   @type key: str
412   @param key: the name to be searched
413   @type name_list: list
414   @param name_list: the list of strings against which to search the key
415
416   @rtype: None or str
417   @return: None if there is no match I{or} if there are multiple matches,
418       otherwise the element from the list which matches
419
420   """
421   mo = re.compile("^%s(\..*)?$" % re.escape(key))
422   names_filtered = [name for name in name_list if mo.match(name) is not None]
423   if len(names_filtered) != 1:
424     return None
425   return names_filtered[0]
426
427
428 class HostInfo:
429   """Class implementing resolver and hostname functionality
430
431   """
432   def __init__(self, name=None):
433     """Initialize the host name object.
434
435     If the name argument is not passed, it will use this system's
436     name.
437
438     """
439     if name is None:
440       name = self.SysName()
441
442     self.query = name
443     self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
444     self.ip = self.ipaddrs[0]
445
446   def ShortName(self):
447     """Returns the hostname without domain.
448
449     """
450     return self.name.split('.')[0]
451
452   @staticmethod
453   def SysName():
454     """Return the current system's name.
455
456     This is simply a wrapper over C{socket.gethostname()}.
457
458     """
459     return socket.gethostname()
460
461   @staticmethod
462   def LookupHostname(hostname):
463     """Look up hostname
464
465     @type hostname: str
466     @param hostname: hostname to look up
467
468     @rtype: tuple
469     @return: a tuple (name, aliases, ipaddrs) as returned by
470         C{socket.gethostbyname_ex}
471     @raise errors.ResolverError: in case of errors in resolving
472
473     """
474     try:
475       result = socket.gethostbyname_ex(hostname)
476     except socket.gaierror, err:
477       # hostname not found in DNS
478       raise errors.ResolverError(hostname, err.args[0], err.args[1])
479
480     return result
481
482
483 def ListVolumeGroups():
484   """List volume groups and their size
485
486   @rtype: dict
487   @return:
488        Dictionary with keys volume name and values
489        the size of the volume
490
491   """
492   command = "vgs --noheadings --units m --nosuffix -o name,size"
493   result = RunCmd(command)
494   retval = {}
495   if result.failed:
496     return retval
497
498   for line in result.stdout.splitlines():
499     try:
500       name, size = line.split()
501       size = int(float(size))
502     except (IndexError, ValueError), err:
503       logging.error("Invalid output from vgs (%s): %s", err, line)
504       continue
505
506     retval[name] = size
507
508   return retval
509
510
511 def BridgeExists(bridge):
512   """Check whether the given bridge exists in the system
513
514   @type bridge: str
515   @param bridge: the bridge name to check
516   @rtype: boolean
517   @return: True if it does
518
519   """
520   return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
521
522
523 def CheckBEParams(beparams):
524   """Checks whether the user-supplied be-params are valid,
525   and converts them from string format where appropriate.
526
527   @type beparams: dict
528   @param beparams: new params dict
529
530   """
531   if beparams:
532     for item in beparams:
533       if item not in constants.BES_PARAMETERS:
534         raise errors.OpPrereqError("Unknown backend parameter %s" % item)
535       if item in (constants.BE_MEMORY, constants.BE_VCPUS):
536         val = beparams[item]
537         if val != constants.VALUE_DEFAULT:
538           try:
539             val = int(val)
540           except ValueError, err:
541             raise errors.OpPrereqError("Invalid %s size: %s" % (item, str(err)))
542           beparams[item] = val
543       if item in (constants.BE_AUTO_BALANCE):
544         val = beparams[item]
545         if not isinstance(val, bool):
546           if val == constants.VALUE_TRUE:
547             beparams[item] = True
548           elif val == constants.VALUE_FALSE:
549             beparams[item] = False
550           else:
551             raise errors.OpPrereqError("Invalid %s value: %s" % (item, val))
552
553
554 def NiceSort(name_list):
555   """Sort a list of strings based on digit and non-digit groupings.
556
557   Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
558   will sort the list in the logical order C{['a1', 'a2', 'a10',
559   'a11']}.
560
561   The sort algorithm breaks each name in groups of either only-digits
562   or no-digits. Only the first eight such groups are considered, and
563   after that we just use what's left of the string.
564
565   @type name_list: list
566   @param name_list: the names to be sorted
567   @rtype: list
568   @return: a copy of the name list sorted with our algorithm
569
570   """
571   _SORTER_BASE = "(\D+|\d+)"
572   _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
573                                                   _SORTER_BASE, _SORTER_BASE,
574                                                   _SORTER_BASE, _SORTER_BASE,
575                                                   _SORTER_BASE, _SORTER_BASE)
576   _SORTER_RE = re.compile(_SORTER_FULL)
577   _SORTER_NODIGIT = re.compile("^\D*$")
578   def _TryInt(val):
579     """Attempts to convert a variable to integer."""
580     if val is None or _SORTER_NODIGIT.match(val):
581       return val
582     rval = int(val)
583     return rval
584
585   to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
586              for name in name_list]
587   to_sort.sort()
588   return [tup[1] for tup in to_sort]
589
590
591 def TryConvert(fn, val):
592   """Try to convert a value ignoring errors.
593
594   This function tries to apply function I{fn} to I{val}. If no
595   C{ValueError} or C{TypeError} exceptions are raised, it will return
596   the result, else it will return the original value. Any other
597   exceptions are propagated to the caller.
598
599   @type fn: callable
600   @param fn: function to apply to the value
601   @param val: the value to be converted
602   @return: The converted value if the conversion was successful,
603       otherwise the original value.
604
605   """
606   try:
607     nv = fn(val)
608   except (ValueError, TypeError), err:
609     nv = val
610   return nv
611
612
613 def IsValidIP(ip):
614   """Verifies the syntax of an IPv4 address.
615
616   This function checks if the IPv4 address passes is valid or not based
617   on syntax (not IP range, class calculations, etc.).
618
619   @type ip: str
620   @param ip: the address to be checked
621   @rtype: a regular expression match object
622   @return: a regular epression match object, or None if the
623       address is not valid
624
625   """
626   unit = "(0|[1-9]\d{0,2})"
627   #TODO: convert and return only boolean
628   return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
629
630
631 def IsValidShellParam(word):
632   """Verifies is the given word is safe from the shell's p.o.v.
633
634   This means that we can pass this to a command via the shell and be
635   sure that it doesn't alter the command line and is passed as such to
636   the actual command.
637
638   Note that we are overly restrictive here, in order to be on the safe
639   side.
640
641   @type word: str
642   @param word: the word to check
643   @rtype: boolean
644   @return: True if the word is 'safe'
645
646   """
647   return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
648
649
650 def BuildShellCmd(template, *args):
651   """Build a safe shell command line from the given arguments.
652
653   This function will check all arguments in the args list so that they
654   are valid shell parameters (i.e. they don't contain shell
655   metacharaters). If everything is ok, it will return the result of
656   template % args.
657
658   @type template: str
659   @param template: the string holding the template for the
660       string formatting
661   @rtype: str
662   @return: the expanded command line
663
664   """
665   for word in args:
666     if not IsValidShellParam(word):
667       raise errors.ProgrammerError("Shell argument '%s' contains"
668                                    " invalid characters" % word)
669   return template % args
670
671
672 def FormatUnit(value, units):
673   """Formats an incoming number of MiB with the appropriate unit.
674
675   @type value: int
676   @param value: integer representing the value in MiB (1048576)
677   @type units: char
678   @param units: the type of formatting we should do:
679       - 'h' for automatic scaling
680       - 'm' for MiBs
681       - 'g' for GiBs
682       - 't' for TiBs
683   @rtype: str
684   @return: the formatted value (with suffix)
685
686   """
687   if units not in ('m', 'g', 't', 'h'):
688     raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
689
690   suffix = ''
691
692   if units == 'm' or (units == 'h' and value < 1024):
693     if units == 'h':
694       suffix = 'M'
695     return "%d%s" % (round(value, 0), suffix)
696
697   elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
698     if units == 'h':
699       suffix = 'G'
700     return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
701
702   else:
703     if units == 'h':
704       suffix = 'T'
705     return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
706
707
708 def ParseUnit(input_string):
709   """Tries to extract number and scale from the given string.
710
711   Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
712   [UNIT]}. If no unit is specified, it defaults to MiB. Return value
713   is always an int in MiB.
714
715   """
716   m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
717   if not m:
718     raise errors.UnitParseError("Invalid format")
719
720   value = float(m.groups()[0])
721
722   unit = m.groups()[1]
723   if unit:
724     lcunit = unit.lower()
725   else:
726     lcunit = 'm'
727
728   if lcunit in ('m', 'mb', 'mib'):
729     # Value already in MiB
730     pass
731
732   elif lcunit in ('g', 'gb', 'gib'):
733     value *= 1024
734
735   elif lcunit in ('t', 'tb', 'tib'):
736     value *= 1024 * 1024
737
738   else:
739     raise errors.UnitParseError("Unknown unit: %s" % unit)
740
741   # Make sure we round up
742   if int(value) < value:
743     value += 1
744
745   # Round up to the next multiple of 4
746   value = int(value)
747   if value % 4:
748     value += 4 - value % 4
749
750   return value
751
752
753 def AddAuthorizedKey(file_name, key):
754   """Adds an SSH public key to an authorized_keys file.
755
756   @type file_name: str
757   @param file_name: path to authorized_keys file
758   @type key: str
759   @param key: string containing key
760
761   """
762   key_fields = key.split()
763
764   f = open(file_name, 'a+')
765   try:
766     nl = True
767     for line in f:
768       # Ignore whitespace changes
769       if line.split() == key_fields:
770         break
771       nl = line.endswith('\n')
772     else:
773       if not nl:
774         f.write("\n")
775       f.write(key.rstrip('\r\n'))
776       f.write("\n")
777       f.flush()
778   finally:
779     f.close()
780
781
782 def RemoveAuthorizedKey(file_name, key):
783   """Removes an SSH public key from an authorized_keys file.
784
785   @type file_name: str
786   @param file_name: path to authorized_keys file
787   @type key: str
788   @param key: string containing key
789
790   """
791   key_fields = key.split()
792
793   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
794   try:
795     out = os.fdopen(fd, 'w')
796     try:
797       f = open(file_name, 'r')
798       try:
799         for line in f:
800           # Ignore whitespace changes while comparing lines
801           if line.split() != key_fields:
802             out.write(line)
803
804         out.flush()
805         os.rename(tmpname, file_name)
806       finally:
807         f.close()
808     finally:
809       out.close()
810   except:
811     RemoveFile(tmpname)
812     raise
813
814
815 def SetEtcHostsEntry(file_name, ip, hostname, aliases):
816   """Sets the name of an IP address and hostname in /etc/hosts.
817
818   @type file_name: str
819   @param file_name: path to the file to modify (usually C{/etc/hosts})
820   @type ip: str
821   @param ip: the IP address
822   @type hostname: str
823   @param hostname: the hostname to be added
824   @type aliases: list
825   @param aliases: the list of aliases to add for the hostname
826
827   """
828   # Ensure aliases are unique
829   aliases = UniqueSequence([hostname] + aliases)[1:]
830
831   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
832   try:
833     out = os.fdopen(fd, 'w')
834     try:
835       f = open(file_name, 'r')
836       try:
837         written = False
838         for line in f:
839           fields = line.split()
840           if fields and not fields[0].startswith('#') and ip == fields[0]:
841             continue
842           out.write(line)
843
844         out.write("%s\t%s" % (ip, hostname))
845         if aliases:
846           out.write(" %s" % ' '.join(aliases))
847         out.write('\n')
848
849         out.flush()
850         os.fsync(out)
851         os.rename(tmpname, file_name)
852       finally:
853         f.close()
854     finally:
855       out.close()
856   except:
857     RemoveFile(tmpname)
858     raise
859
860
861 def AddHostToEtcHosts(hostname):
862   """Wrapper around SetEtcHostsEntry.
863
864   @type hostname: str
865   @param hostname: a hostname that will be resolved and added to
866       L{constants.ETC_HOSTS}
867
868   """
869   hi = HostInfo(name=hostname)
870   SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
871
872
873 def RemoveEtcHostsEntry(file_name, hostname):
874   """Removes a hostname from /etc/hosts.
875
876   IP addresses without names are removed from the file.
877
878   @type file_name: str
879   @param file_name: path to the file to modify (usually C{/etc/hosts})
880   @type hostname: str
881   @param hostname: the hostname to be removed
882
883   """
884   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
885   try:
886     out = os.fdopen(fd, 'w')
887     try:
888       f = open(file_name, 'r')
889       try:
890         for line in f:
891           fields = line.split()
892           if len(fields) > 1 and not fields[0].startswith('#'):
893             names = fields[1:]
894             if hostname in names:
895               while hostname in names:
896                 names.remove(hostname)
897               if names:
898                 out.write("%s %s\n" % (fields[0], ' '.join(names)))
899               continue
900
901           out.write(line)
902
903         out.flush()
904         os.fsync(out)
905         os.rename(tmpname, file_name)
906       finally:
907         f.close()
908     finally:
909       out.close()
910   except:
911     RemoveFile(tmpname)
912     raise
913
914
915 def RemoveHostFromEtcHosts(hostname):
916   """Wrapper around RemoveEtcHostsEntry.
917
918   @type hostname: str
919   @param hostname: hostname that will be resolved and its
920       full and shot name will be removed from
921       L{constants.ETC_HOSTS}
922
923   """
924   hi = HostInfo(name=hostname)
925   RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
926   RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
927
928
929 def CreateBackup(file_name):
930   """Creates a backup of a file.
931
932   @type file_name: str
933   @param file_name: file to be backed up
934   @rtype: str
935   @return: the path to the newly created backup
936   @raise errors.ProgrammerError: for invalid file names
937
938   """
939   if not os.path.isfile(file_name):
940     raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
941                                 file_name)
942
943   prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
944   dir_name = os.path.dirname(file_name)
945
946   fsrc = open(file_name, 'rb')
947   try:
948     (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
949     fdst = os.fdopen(fd, 'wb')
950     try:
951       shutil.copyfileobj(fsrc, fdst)
952     finally:
953       fdst.close()
954   finally:
955     fsrc.close()
956
957   return backup_name
958
959
960 def ShellQuote(value):
961   """Quotes shell argument according to POSIX.
962
963   @type value: str
964   @param value: the argument to be quoted
965   @rtype: str
966   @return: the quoted value
967
968   """
969   if _re_shell_unquoted.match(value):
970     return value
971   else:
972     return "'%s'" % value.replace("'", "'\\''")
973
974
975 def ShellQuoteArgs(args):
976   """Quotes a list of shell arguments.
977
978   @type args: list
979   @param args: list of arguments to be quoted
980   @rtype: str
981   @return: the quoted arguments concatenaned with spaces
982
983   """
984   return ' '.join([ShellQuote(i) for i in args])
985
986
987 def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
988   """Simple ping implementation using TCP connect(2).
989
990   Check if the given IP is reachable by doing attempting a TCP connect
991   to it.
992
993   @type target: str
994   @param target: the IP or hostname to ping
995   @type port: int
996   @param port: the port to connect to
997   @type timeout: int
998   @param timeout: the timeout on the connection attemp
999   @type live_port_needed: boolean
1000   @param live_port_needed: whether a closed port will cause the
1001       function to return failure, as if there was a timeout
1002   @type source: str or None
1003   @param source: if specified, will cause the connect to be made
1004       from this specific source address; failures to bind other
1005       than C{EADDRNOTAVAIL} will be ignored
1006
1007   """
1008   sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1009
1010   sucess = False
1011
1012   if source is not None:
1013     try:
1014       sock.bind((source, 0))
1015     except socket.error, (errcode, errstring):
1016       if errcode == errno.EADDRNOTAVAIL:
1017         success = False
1018
1019   sock.settimeout(timeout)
1020
1021   try:
1022     sock.connect((target, port))
1023     sock.close()
1024     success = True
1025   except socket.timeout:
1026     success = False
1027   except socket.error, (errcode, errstring):
1028     success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
1029
1030   return success
1031
1032
1033 def OwnIpAddress(address):
1034   """Check if the current host has the the given IP address.
1035
1036   Currently this is done by TCP-pinging the address from the loopback
1037   address.
1038
1039   @type address: string
1040   @param address: the addres to check
1041   @rtype: bool
1042   @return: True if we own the address
1043
1044   """
1045   return TcpPing(address, constants.DEFAULT_NODED_PORT,
1046                  source=constants.LOCALHOST_IP_ADDRESS)
1047
1048
1049 def ListVisibleFiles(path):
1050   """Returns a list of visible files in a directory.
1051
1052   @type path: str
1053   @param path: the directory to enumerate
1054   @rtype: list
1055   @return: the list of all files not starting with a dot
1056
1057   """
1058   files = [i for i in os.listdir(path) if not i.startswith(".")]
1059   files.sort()
1060   return files
1061
1062
1063 def GetHomeDir(user, default=None):
1064   """Try to get the homedir of the given user.
1065
1066   The user can be passed either as a string (denoting the name) or as
1067   an integer (denoting the user id). If the user is not found, the
1068   'default' argument is returned, which defaults to None.
1069
1070   """
1071   try:
1072     if isinstance(user, basestring):
1073       result = pwd.getpwnam(user)
1074     elif isinstance(user, (int, long)):
1075       result = pwd.getpwuid(user)
1076     else:
1077       raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
1078                                    type(user))
1079   except KeyError:
1080     return default
1081   return result.pw_dir
1082
1083
1084 def NewUUID():
1085   """Returns a random UUID.
1086
1087   @note: This is a Linux-specific method as it uses the /proc
1088       filesystem.
1089   @rtype: str
1090
1091   """
1092   f = open("/proc/sys/kernel/random/uuid", "r")
1093   try:
1094     return f.read(128).rstrip("\n")
1095   finally:
1096     f.close()
1097
1098
1099 def GenerateSecret():
1100   """Generates a random secret.
1101
1102   This will generate a pseudo-random secret, and return its sha digest
1103   (so that it can be used where an ASCII string is needed).
1104
1105   @rtype: str
1106   @return: a sha1 hexdigest of a block of 64 random bytes
1107
1108   """
1109   return sha.new(os.urandom(64)).hexdigest()
1110
1111
1112 def ReadFile(file_name, size=None):
1113   """Reads a file.
1114
1115   @type size: None or int
1116   @param size: Read at most size bytes
1117   @rtype: str
1118   @return: the (possibly partial) conent of the file
1119
1120   """
1121   f = open(file_name, "r")
1122   try:
1123     if size is None:
1124       return f.read()
1125     else:
1126       return f.read(size)
1127   finally:
1128     f.close()
1129
1130
1131 def WriteFile(file_name, fn=None, data=None,
1132               mode=None, uid=-1, gid=-1,
1133               atime=None, mtime=None, close=True,
1134               dry_run=False, backup=False,
1135               prewrite=None, postwrite=None):
1136   """(Over)write a file atomically.
1137
1138   The file_name and either fn (a function taking one argument, the
1139   file descriptor, and which should write the data to it) or data (the
1140   contents of the file) must be passed. The other arguments are
1141   optional and allow setting the file mode, owner and group, and the
1142   mtime/atime of the file.
1143
1144   If the function doesn't raise an exception, it has succeeded and the
1145   target file has the new contents. If the file has raised an
1146   exception, an existing target file should be unmodified and the
1147   temporary file should be removed.
1148
1149   @type file_name: str
1150   @param file_name: the target filename
1151   @type fn: callable
1152   @param fn: content writing function, called with
1153       file descriptor as parameter
1154   @type data: sr
1155   @param data: contents of the file
1156   @type mode: int
1157   @param mode: file mode
1158   @type uid: int
1159   @param uid: the owner of the file
1160   @type gid: int
1161   @param gid: the group of the file
1162   @type atime: int
1163   @param atime: a custom access time to be set on the file
1164   @type mtime: int
1165   @param mtime: a custom modification time to be set on the file
1166   @type close: boolean
1167   @param close: whether to close file after writing it
1168   @type prewrite: callable
1169   @param prewrite: function to be called before writing content
1170   @type postwrite: callable
1171   @param postwrite: function to be called after writing content
1172
1173   @rtype: None or int
1174   @return: None if the 'close' parameter evaluates to True,
1175       otherwise the file descriptor
1176
1177   @raise errors.ProgrammerError: if an of the arguments are not valid
1178
1179   """
1180   if not os.path.isabs(file_name):
1181     raise errors.ProgrammerError("Path passed to WriteFile is not"
1182                                  " absolute: '%s'" % file_name)
1183
1184   if [fn, data].count(None) != 1:
1185     raise errors.ProgrammerError("fn or data required")
1186
1187   if [atime, mtime].count(None) == 1:
1188     raise errors.ProgrammerError("Both atime and mtime must be either"
1189                                  " set or None")
1190
1191   if backup and not dry_run and os.path.isfile(file_name):
1192     CreateBackup(file_name)
1193
1194   dir_name, base_name = os.path.split(file_name)
1195   fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1196   # here we need to make sure we remove the temp file, if any error
1197   # leaves it in place
1198   try:
1199     if uid != -1 or gid != -1:
1200       os.chown(new_name, uid, gid)
1201     if mode:
1202       os.chmod(new_name, mode)
1203     if callable(prewrite):
1204       prewrite(fd)
1205     if data is not None:
1206       os.write(fd, data)
1207     else:
1208       fn(fd)
1209     if callable(postwrite):
1210       postwrite(fd)
1211     os.fsync(fd)
1212     if atime is not None and mtime is not None:
1213       os.utime(new_name, (atime, mtime))
1214     if not dry_run:
1215       os.rename(new_name, file_name)
1216   finally:
1217     if close:
1218       os.close(fd)
1219       result = None
1220     else:
1221       result = fd
1222     RemoveFile(new_name)
1223
1224   return result
1225
1226
1227 def FirstFree(seq, base=0):
1228   """Returns the first non-existing integer from seq.
1229
1230   The seq argument should be a sorted list of positive integers. The
1231   first time the index of an element is smaller than the element
1232   value, the index will be returned.
1233
1234   The base argument is used to start at a different offset,
1235   i.e. C{[3, 4, 6]} with I{offset=3} will return 5.
1236
1237   Example: C{[0, 1, 3]} will return I{2}.
1238
1239   @type seq: sequence
1240   @param seq: the sequence to be analyzed.
1241   @type base: int
1242   @param base: use this value as the base index of the sequence
1243   @rtype: int
1244   @return: the first non-used index in the sequence
1245
1246   """
1247   for idx, elem in enumerate(seq):
1248     assert elem >= base, "Passed element is higher than base offset"
1249     if elem > idx + base:
1250       # idx is not used
1251       return idx + base
1252   return None
1253
1254
1255 def all(seq, pred=bool):
1256   "Returns True if pred(x) is True for every element in the iterable"
1257   for elem in itertools.ifilterfalse(pred, seq):
1258     return False
1259   return True
1260
1261
1262 def any(seq, pred=bool):
1263   "Returns True if pred(x) is True for at least one element in the iterable"
1264   for elem in itertools.ifilter(pred, seq):
1265     return True
1266   return False
1267
1268
1269 def UniqueSequence(seq):
1270   """Returns a list with unique elements.
1271
1272   Element order is preserved.
1273
1274   @type seq: sequence
1275   @param seq: the sequence with the source elementes
1276   @rtype: list
1277   @return: list of unique elements from seq
1278
1279   """
1280   seen = set()
1281   return [i for i in seq if i not in seen and not seen.add(i)]
1282
1283
1284 def IsValidMac(mac):
1285   """Predicate to check if a MAC address is valid.
1286
1287   Checks wether the supplied MAC address is formally correct, only
1288   accepts colon separated format.
1289
1290   @type mac: str
1291   @param mac: the MAC to be validated
1292   @rtype: boolean
1293   @return: True is the MAC seems valid
1294
1295   """
1296   mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
1297   return mac_check.match(mac) is not None
1298
1299
1300 def TestDelay(duration):
1301   """Sleep for a fixed amount of time.
1302
1303   @type duration: float
1304   @param duration: the sleep duration
1305   @rtype: boolean
1306   @return: False for negative value, True otherwise
1307
1308   """
1309   if duration < 0:
1310     return False
1311   time.sleep(duration)
1312   return True
1313
1314
1315 def Daemonize(logfile, noclose_fds=None):
1316   """Daemonize the current process.
1317
1318   This detaches the current process from the controlling terminal and
1319   runs it in the background as a daemon.
1320
1321   @type logfile: str
1322   @param logfile: the logfile to which we should redirect stdout/stderr
1323   @type noclose_fds: list or None
1324   @param noclose_fds: if given, it denotes a list of file descriptor
1325       that should not be closed
1326   @rtype: int
1327   @returns: the value zero
1328
1329   """
1330   UMASK = 077
1331   WORKDIR = "/"
1332   # Default maximum for the number of available file descriptors.
1333   if 'SC_OPEN_MAX' in os.sysconf_names:
1334     try:
1335       MAXFD = os.sysconf('SC_OPEN_MAX')
1336       if MAXFD < 0:
1337         MAXFD = 1024
1338     except OSError:
1339       MAXFD = 1024
1340   else:
1341     MAXFD = 1024
1342
1343   # this might fail
1344   pid = os.fork()
1345   if (pid == 0):  # The first child.
1346     os.setsid()
1347     # this might fail
1348     pid = os.fork() # Fork a second child.
1349     if (pid == 0):  # The second child.
1350       os.chdir(WORKDIR)
1351       os.umask(UMASK)
1352     else:
1353       # exit() or _exit()?  See below.
1354       os._exit(0) # Exit parent (the first child) of the second child.
1355   else:
1356     os._exit(0) # Exit parent of the first child.
1357   maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1358   if (maxfd == resource.RLIM_INFINITY):
1359     maxfd = MAXFD
1360
1361   # Iterate through and close all file descriptors.
1362   for fd in range(0, maxfd):
1363     if noclose_fds and fd in noclose_fds:
1364       continue
1365     try:
1366       os.close(fd)
1367     except OSError: # ERROR, fd wasn't open to begin with (ignored)
1368       pass
1369   os.open(logfile, os.O_RDWR|os.O_CREAT|os.O_APPEND, 0600)
1370   # Duplicate standard input to standard output and standard error.
1371   os.dup2(0, 1)     # standard output (1)
1372   os.dup2(0, 2)     # standard error (2)
1373   return 0
1374
1375
1376 def DaemonPidFileName(name):
1377   """Compute a ganeti pid file absolute path
1378
1379   @type name: str
1380   @param name: the daemon name
1381   @rtype: str
1382   @return: the full path to the pidfile corresponding to the given
1383       daemon name
1384
1385   """
1386   return os.path.join(constants.RUN_GANETI_DIR, "%s.pid" % name)
1387
1388
1389 def WritePidFile(name):
1390   """Write the current process pidfile.
1391
1392   The file will be written to L{constants.RUN_GANETI_DIR}I{/name.pid}
1393
1394   @type name: str
1395   @param name: the daemon name to use
1396   @raise errors.GenericError: if the pid file already exists and
1397       points to a live process
1398
1399   """
1400   pid = os.getpid()
1401   pidfilename = DaemonPidFileName(name)
1402   if IsProcessAlive(ReadPidFile(pidfilename)):
1403     raise errors.GenericError("%s contains a live process" % pidfilename)
1404
1405   WriteFile(pidfilename, data="%d\n" % pid)
1406
1407
1408 def RemovePidFile(name):
1409   """Remove the current process pidfile.
1410
1411   Any errors are ignored.
1412
1413   @type name: str
1414   @param name: the daemon name used to derive the pidfile name
1415
1416   """
1417   pid = os.getpid()
1418   pidfilename = DaemonPidFileName(name)
1419   # TODO: we could check here that the file contains our pid
1420   try:
1421     RemoveFile(pidfilename)
1422   except:
1423     pass
1424
1425
1426 def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1427                 waitpid=False):
1428   """Kill a process given by its pid.
1429
1430   @type pid: int
1431   @param pid: The PID to terminate.
1432   @type signal_: int
1433   @param signal_: The signal to send, by default SIGTERM
1434   @type timeout: int
1435   @param timeout: The timeout after which, if the process is still alive,
1436                   a SIGKILL will be sent. If not positive, no such checking
1437                   will be done
1438   @type waitpid: boolean
1439   @param waitpid: If true, we should waitpid on this process after
1440       sending signals, since it's our own child and otherwise it
1441       would remain as zombie
1442
1443   """
1444   def _helper(pid, signal_, wait):
1445     """Simple helper to encapsulate the kill/waitpid sequence"""
1446     os.kill(pid, signal_)
1447     if wait:
1448       try:
1449         os.waitpid(pid, os.WNOHANG)
1450       except OSError:
1451         pass
1452
1453   if pid <= 0:
1454     # kill with pid=0 == suicide
1455     raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1456
1457   if not IsProcessAlive(pid):
1458     return
1459   _helper(pid, signal_, waitpid)
1460   if timeout <= 0:
1461     return
1462   end = time.time() + timeout
1463   while time.time() < end and IsProcessAlive(pid):
1464     time.sleep(0.1)
1465   if IsProcessAlive(pid):
1466     _helper(pid, signal.SIGKILL, waitpid)
1467
1468
1469 def FindFile(name, search_path, test=os.path.exists):
1470   """Look for a filesystem object in a given path.
1471
1472   This is an abstract method to search for filesystem object (files,
1473   dirs) under a given search path.
1474
1475   @type name: str
1476   @param name: the name to look for
1477   @type search_path: str
1478   @param search_path: location to start at
1479   @type test: callable
1480   @param test: a function taking one argument that should return True
1481       if the a given object is valid; the default value is
1482       os.path.exists, causing only existing files to be returned
1483   @rtype: str or None
1484   @return: full path to the object if found, None otherwise
1485
1486   """
1487   for dir_name in search_path:
1488     item_name = os.path.sep.join([dir_name, name])
1489     if test(item_name):
1490       return item_name
1491   return None
1492
1493
1494 def CheckVolumeGroupSize(vglist, vgname, minsize):
1495   """Checks if the volume group list is valid.
1496
1497   The function will check if a given volume group is in the list of
1498   volume groups and has a minimum size.
1499
1500   @type vglist: dict
1501   @param vglist: dictionary of volume group names and their size
1502   @type vgname: str
1503   @param vgname: the volume group we should check
1504   @type minsize: int
1505   @param minsize: the minimum size we accept
1506   @rtype: None or str
1507   @return: None for success, otherwise the error message
1508
1509   """
1510   vgsize = vglist.get(vgname, None)
1511   if vgsize is None:
1512     return "volume group '%s' missing" % vgname
1513   elif vgsize < minsize:
1514     return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1515             (vgname, minsize, vgsize))
1516   return None
1517
1518
1519 def SplitTime(value):
1520   """Splits time as floating point number into a tuple.
1521
1522   @param value: Time in seconds
1523   @type value: int or float
1524   @return: Tuple containing (seconds, microseconds)
1525
1526   """
1527   (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
1528
1529   assert 0 <= seconds, \
1530     "Seconds must be larger than or equal to 0, but are %s" % seconds
1531   assert 0 <= microseconds <= 999999, \
1532     "Microseconds must be 0-999999, but are %s" % microseconds
1533
1534   return (int(seconds), int(microseconds))
1535
1536
1537 def MergeTime(timetuple):
1538   """Merges a tuple into time as a floating point number.
1539
1540   @param timetuple: Time as tuple, (seconds, microseconds)
1541   @type timetuple: tuple
1542   @return: Time as a floating point number expressed in seconds
1543
1544   """
1545   (seconds, microseconds) = timetuple
1546
1547   assert 0 <= seconds, \
1548     "Seconds must be larger than or equal to 0, but are %s" % seconds
1549   assert 0 <= microseconds <= 999999, \
1550     "Microseconds must be 0-999999, but are %s" % microseconds
1551
1552   return float(seconds) + (float(microseconds) * 0.000001)
1553
1554
1555 def GetNodeDaemonPort():
1556   """Get the node daemon port for this cluster.
1557
1558   Note that this routine does not read a ganeti-specific file, but
1559   instead uses C{socket.getservbyname} to allow pre-customization of
1560   this parameter outside of Ganeti.
1561
1562   @rtype: int
1563
1564   """
1565   try:
1566     port = socket.getservbyname("ganeti-noded", "tcp")
1567   except socket.error:
1568     port = constants.DEFAULT_NODED_PORT
1569
1570   return port
1571
1572
1573 def SetupLogging(logfile, debug=False, stderr_logging=False, program=""):
1574   """Configures the logging module.
1575
1576   @type logfile: str
1577   @param logfile: the filename to which we should log
1578   @type debug: boolean
1579   @param debug: whether to enable debug messages too or
1580       only those at C{INFO} and above level
1581   @type stderr_logging: boolean
1582   @param stderr_logging: whether we should also log to the standard error
1583   @type program: str
1584   @param program: the name under which we should log messages
1585   @raise EnvironmentError: if we can't open the log file and
1586       stderr logging is disabled
1587
1588   """
1589   fmt = "%(asctime)s: " + program + " "
1590   if debug:
1591     fmt += ("pid=%(process)d/%(threadName)s %(levelname)s"
1592            " %(module)s:%(lineno)s %(message)s")
1593   else:
1594     fmt += "pid=%(process)d %(levelname)s %(message)s"
1595   formatter = logging.Formatter(fmt)
1596
1597   root_logger = logging.getLogger("")
1598   root_logger.setLevel(logging.NOTSET)
1599
1600   # Remove all previously setup handlers
1601   for handler in root_logger.handlers:
1602     root_logger.removeHandler(handler)
1603
1604   if stderr_logging:
1605     stderr_handler = logging.StreamHandler()
1606     stderr_handler.setFormatter(formatter)
1607     if debug:
1608       stderr_handler.setLevel(logging.NOTSET)
1609     else:
1610       stderr_handler.setLevel(logging.CRITICAL)
1611     root_logger.addHandler(stderr_handler)
1612
1613   # this can fail, if the logging directories are not setup or we have
1614   # a permisssion problem; in this case, it's best to log but ignore
1615   # the error if stderr_logging is True, and if false we re-raise the
1616   # exception since otherwise we could run but without any logs at all
1617   try:
1618     logfile_handler = logging.FileHandler(logfile)
1619     logfile_handler.setFormatter(formatter)
1620     if debug:
1621       logfile_handler.setLevel(logging.DEBUG)
1622     else:
1623       logfile_handler.setLevel(logging.INFO)
1624     root_logger.addHandler(logfile_handler)
1625   except EnvironmentError, err:
1626     if stderr_logging:
1627       logging.exception("Failed to enable logging to file '%s'", logfile)
1628     else:
1629       # we need to re-raise the exception
1630       raise
1631
1632
1633 def LockedMethod(fn):
1634   """Synchronized object access decorator.
1635
1636   This decorator is intended to protect access to an object using the
1637   object's own lock which is hardcoded to '_lock'.
1638
1639   """
1640   def _LockDebug(*args, **kwargs):
1641     if debug_locks:
1642       logging.debug(*args, **kwargs)
1643
1644   def wrapper(self, *args, **kwargs):
1645     assert hasattr(self, '_lock')
1646     lock = self._lock
1647     _LockDebug("Waiting for %s", lock)
1648     lock.acquire()
1649     try:
1650       _LockDebug("Acquired %s", lock)
1651       result = fn(self, *args, **kwargs)
1652     finally:
1653       _LockDebug("Releasing %s", lock)
1654       lock.release()
1655       _LockDebug("Released %s", lock)
1656     return result
1657   return wrapper
1658
1659
1660 def LockFile(fd):
1661   """Locks a file using POSIX locks.
1662
1663   @type fd: int
1664   @param fd: the file descriptor we need to lock
1665
1666   """
1667   try:
1668     fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1669   except IOError, err:
1670     if err.errno == errno.EAGAIN:
1671       raise errors.LockError("File already locked")
1672     raise
1673
1674
1675 class FileLock(object):
1676   """Utility class for file locks.
1677
1678   """
1679   def __init__(self, filename):
1680     """Constructor for FileLock.
1681
1682     This will open the file denoted by the I{filename} argument.
1683
1684     @type filename: str
1685     @param filename: path to the file to be locked
1686
1687     """
1688     self.filename = filename
1689     self.fd = open(self.filename, "w")
1690
1691   def __del__(self):
1692     self.Close()
1693
1694   def Close(self):
1695     """Close the file and release the lock.
1696
1697     """
1698     if self.fd:
1699       self.fd.close()
1700       self.fd = None
1701
1702   def _flock(self, flag, blocking, timeout, errmsg):
1703     """Wrapper for fcntl.flock.
1704
1705     @type flag: int
1706     @param flag: operation flag
1707     @type blocking: bool
1708     @param blocking: whether the operation should be done in blocking mode.
1709     @type timeout: None or float
1710     @param timeout: for how long the operation should be retried (implies
1711                     non-blocking mode).
1712     @type errmsg: string
1713     @param errmsg: error message in case operation fails.
1714
1715     """
1716     assert self.fd, "Lock was closed"
1717     assert timeout is None or timeout >= 0, \
1718       "If specified, timeout must be positive"
1719
1720     if timeout is not None:
1721       flag |= fcntl.LOCK_NB
1722       timeout_end = time.time() + timeout
1723
1724     # Blocking doesn't have effect with timeout
1725     elif not blocking:
1726       flag |= fcntl.LOCK_NB
1727       timeout_end = None
1728
1729     retry = True
1730     while retry:
1731       try:
1732         fcntl.flock(self.fd, flag)
1733         retry = False
1734       except IOError, err:
1735         if err.errno in (errno.EAGAIN, ):
1736           if timeout_end is not None and time.time() < timeout_end:
1737             # Wait before trying again
1738             time.sleep(max(0.1, min(1.0, timeout)))
1739           else:
1740             raise errors.LockError(errmsg)
1741         else:
1742           logging.exception("fcntl.flock failed")
1743           raise
1744
1745   def Exclusive(self, blocking=False, timeout=None):
1746     """Locks the file in exclusive mode.
1747
1748     @type blocking: boolean
1749     @param blocking: whether to block and wait until we
1750         can lock the file or return immediately
1751     @type timeout: int or None
1752     @param timeout: if not None, the duration to wait for the lock
1753         (in blocking mode)
1754
1755     """
1756     self._flock(fcntl.LOCK_EX, blocking, timeout,
1757                 "Failed to lock %s in exclusive mode" % self.filename)
1758
1759   def Shared(self, blocking=False, timeout=None):
1760     """Locks the file in shared mode.
1761
1762     @type blocking: boolean
1763     @param blocking: whether to block and wait until we
1764         can lock the file or return immediately
1765     @type timeout: int or None
1766     @param timeout: if not None, the duration to wait for the lock
1767         (in blocking mode)
1768
1769     """
1770     self._flock(fcntl.LOCK_SH, blocking, timeout,
1771                 "Failed to lock %s in shared mode" % self.filename)
1772
1773   def Unlock(self, blocking=True, timeout=None):
1774     """Unlocks the file.
1775
1776     According to C{flock(2)}, unlocking can also be a nonblocking
1777     operation::
1778
1779       To make a non-blocking request, include LOCK_NB with any of the above
1780       operations.
1781
1782     @type blocking: boolean
1783     @param blocking: whether to block and wait until we
1784         can lock the file or return immediately
1785     @type timeout: int or None
1786     @param timeout: if not None, the duration to wait for the lock
1787         (in blocking mode)
1788
1789     """
1790     self._flock(fcntl.LOCK_UN, blocking, timeout,
1791                 "Failed to unlock %s" % self.filename)
1792
1793
1794 class SignalHandler(object):
1795   """Generic signal handler class.
1796
1797   It automatically restores the original handler when deconstructed or
1798   when L{Reset} is called. You can either pass your own handler
1799   function in or query the L{called} attribute to detect whether the
1800   signal was sent.
1801
1802   @type signum: list
1803   @ivar signum: the signals we handle
1804   @type called: boolean
1805   @ivar called: tracks whether any of the signals have been raised
1806
1807   """
1808   def __init__(self, signum):
1809     """Constructs a new SignalHandler instance.
1810
1811     @type signum: int or list of ints
1812     @param signum: Single signal number or set of signal numbers
1813
1814     """
1815     if isinstance(signum, (int, long)):
1816       self.signum = set([signum])
1817     else:
1818       self.signum = set(signum)
1819
1820     self.called = False
1821
1822     self._previous = {}
1823     try:
1824       for signum in self.signum:
1825         # Setup handler
1826         prev_handler = signal.signal(signum, self._HandleSignal)
1827         try:
1828           self._previous[signum] = prev_handler
1829         except:
1830           # Restore previous handler
1831           signal.signal(signum, prev_handler)
1832           raise
1833     except:
1834       # Reset all handlers
1835       self.Reset()
1836       # Here we have a race condition: a handler may have already been called,
1837       # but there's not much we can do about it at this point.
1838       raise
1839
1840   def __del__(self):
1841     self.Reset()
1842
1843   def Reset(self):
1844     """Restore previous handler.
1845
1846     This will reset all the signals to their previous handlers.
1847
1848     """
1849     for signum, prev_handler in self._previous.items():
1850       signal.signal(signum, prev_handler)
1851       # If successful, remove from dict
1852       del self._previous[signum]
1853
1854   def Clear(self):
1855     """Unsets the L{called} flag.
1856
1857     This function can be used in case a signal may arrive several times.
1858
1859     """
1860     self.called = False
1861
1862   def _HandleSignal(self, signum, frame):
1863     """Actual signal handling function.
1864
1865     """
1866     # This is not nice and not absolutely atomic, but it appears to be the only
1867     # solution in Python -- there are no atomic types.
1868     self.called = True
1869
1870
1871 class FieldSet(object):
1872   """A simple field set.
1873
1874   Among the features are:
1875     - checking if a string is among a list of static string or regex objects
1876     - checking if a whole list of string matches
1877     - returning the matching groups from a regex match
1878
1879   Internally, all fields are held as regular expression objects.
1880
1881   """
1882   def __init__(self, *items):
1883     self.items = [re.compile("^%s$" % value) for value in items]
1884
1885   def Extend(self, other_set):
1886     """Extend the field set with the items from another one"""
1887     self.items.extend(other_set.items)
1888
1889   def Matches(self, field):
1890     """Checks if a field matches the current set
1891
1892     @type field: str
1893     @param field: the string to match
1894     @return: either False or a regular expression match object
1895
1896     """
1897     for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
1898       return m
1899     return False
1900
1901   def NonMatching(self, items):
1902     """Returns the list of fields not matching the current set
1903
1904     @type items: list
1905     @param items: the list of fields to check
1906     @rtype: list
1907     @return: list of non-matching fields
1908
1909     """
1910     return [val for val in items if not self.Matches(val)]