Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ e1bd0072

History | View | Annotate | Download (47.4 kB)

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 NiceSort(name_list):
524
  """Sort a list of strings based on digit and non-digit groupings.
525

526
  Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
527
  will sort the list in the logical order C{['a1', 'a2', 'a10',
528
  'a11']}.
529

530
  The sort algorithm breaks each name in groups of either only-digits
531
  or no-digits. Only the first eight such groups are considered, and
532
  after that we just use what's left of the string.
533

534
  @type name_list: list
535
  @param name_list: the names to be sorted
536
  @rtype: list
537
  @return: a copy of the name list sorted with our algorithm
538

539
  """
540
  _SORTER_BASE = "(\D+|\d+)"
541
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
542
                                                  _SORTER_BASE, _SORTER_BASE,
543
                                                  _SORTER_BASE, _SORTER_BASE,
544
                                                  _SORTER_BASE, _SORTER_BASE)
545
  _SORTER_RE = re.compile(_SORTER_FULL)
546
  _SORTER_NODIGIT = re.compile("^\D*$")
547
  def _TryInt(val):
548
    """Attempts to convert a variable to integer."""
549
    if val is None or _SORTER_NODIGIT.match(val):
550
      return val
551
    rval = int(val)
552
    return rval
553

    
554
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
555
             for name in name_list]
556
  to_sort.sort()
557
  return [tup[1] for tup in to_sort]
558

    
559

    
560
def TryConvert(fn, val):
561
  """Try to convert a value ignoring errors.
562

563
  This function tries to apply function I{fn} to I{val}. If no
564
  C{ValueError} or C{TypeError} exceptions are raised, it will return
565
  the result, else it will return the original value. Any other
566
  exceptions are propagated to the caller.
567

568
  @type fn: callable
569
  @param fn: function to apply to the value
570
  @param val: the value to be converted
571
  @return: The converted value if the conversion was successful,
572
      otherwise the original value.
573

574
  """
575
  try:
576
    nv = fn(val)
577
  except (ValueError, TypeError), err:
578
    nv = val
579
  return nv
580

    
581

    
582
def IsValidIP(ip):
583
  """Verifies the syntax of an IPv4 address.
584

585
  This function checks if the IPv4 address passes is valid or not based
586
  on syntax (not IP range, class calculations, etc.).
587

588
  @type ip: str
589
  @param ip: the address to be checked
590
  @rtype: a regular expression match object
591
  @return: a regular epression match object, or None if the
592
      address is not valid
593

594
  """
595
  unit = "(0|[1-9]\d{0,2})"
596
  #TODO: convert and return only boolean
597
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
598

    
599

    
600
def IsValidShellParam(word):
601
  """Verifies is the given word is safe from the shell's p.o.v.
602

603
  This means that we can pass this to a command via the shell and be
604
  sure that it doesn't alter the command line and is passed as such to
605
  the actual command.
606

607
  Note that we are overly restrictive here, in order to be on the safe
608
  side.
609

610
  @type word: str
611
  @param word: the word to check
612
  @rtype: boolean
613
  @return: True if the word is 'safe'
614

615
  """
616
  return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
617

    
618

    
619
def BuildShellCmd(template, *args):
620
  """Build a safe shell command line from the given arguments.
621

622
  This function will check all arguments in the args list so that they
623
  are valid shell parameters (i.e. they don't contain shell
624
  metacharaters). If everything is ok, it will return the result of
625
  template % args.
626

627
  @type template: str
628
  @param template: the string holding the template for the
629
      string formatting
630
  @rtype: str
631
  @return: the expanded command line
632

633
  """
634
  for word in args:
635
    if not IsValidShellParam(word):
636
      raise errors.ProgrammerError("Shell argument '%s' contains"
637
                                   " invalid characters" % word)
638
  return template % args
639

    
640

    
641
def FormatUnit(value):
642
  """Formats an incoming number of MiB with the appropriate unit.
643

644
  @type value: int
645
  @param value: integer representing the value in MiB (1048576)
646
  @rtype: str
647
  @return: the formatted value (with suffix)
648

649
  """
650
  if value < 1024:
651
    return "%dM" % round(value, 0)
652

    
653
  elif value < (1024 * 1024):
654
    return "%0.1fG" % round(float(value) / 1024, 1)
655

    
656
  else:
657
    return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
658

    
659

    
660
def ParseUnit(input_string):
661
  """Tries to extract number and scale from the given string.
662

663
  Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
664
  [UNIT]}. If no unit is specified, it defaults to MiB. Return value
665
  is always an int in MiB.
666

667
  """
668
  m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
669
  if not m:
670
    raise errors.UnitParseError("Invalid format")
671

    
672
  value = float(m.groups()[0])
673

    
674
  unit = m.groups()[1]
675
  if unit:
676
    lcunit = unit.lower()
677
  else:
678
    lcunit = 'm'
679

    
680
  if lcunit in ('m', 'mb', 'mib'):
681
    # Value already in MiB
682
    pass
683

    
684
  elif lcunit in ('g', 'gb', 'gib'):
685
    value *= 1024
686

    
687
  elif lcunit in ('t', 'tb', 'tib'):
688
    value *= 1024 * 1024
689

    
690
  else:
691
    raise errors.UnitParseError("Unknown unit: %s" % unit)
692

    
693
  # Make sure we round up
694
  if int(value) < value:
695
    value += 1
696

    
697
  # Round up to the next multiple of 4
698
  value = int(value)
699
  if value % 4:
700
    value += 4 - value % 4
701

    
702
  return value
703

    
704

    
705
def AddAuthorizedKey(file_name, key):
706
  """Adds an SSH public key to an authorized_keys file.
707

708
  @type file_name: str
709
  @param file_name: path to authorized_keys file
710
  @type key: str
711
  @param key: string containing key
712

713
  """
714
  key_fields = key.split()
715

    
716
  f = open(file_name, 'a+')
717
  try:
718
    nl = True
719
    for line in f:
720
      # Ignore whitespace changes
721
      if line.split() == key_fields:
722
        break
723
      nl = line.endswith('\n')
724
    else:
725
      if not nl:
726
        f.write("\n")
727
      f.write(key.rstrip('\r\n'))
728
      f.write("\n")
729
      f.flush()
730
  finally:
731
    f.close()
732

    
733

    
734
def RemoveAuthorizedKey(file_name, key):
735
  """Removes an SSH public key from an authorized_keys file.
736

737
  @type file_name: str
738
  @param file_name: path to authorized_keys file
739
  @type key: str
740
  @param key: string containing key
741

742
  """
743
  key_fields = key.split()
744

    
745
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
746
  try:
747
    out = os.fdopen(fd, 'w')
748
    try:
749
      f = open(file_name, 'r')
750
      try:
751
        for line in f:
752
          # Ignore whitespace changes while comparing lines
753
          if line.split() != key_fields:
754
            out.write(line)
755

    
756
        out.flush()
757
        os.rename(tmpname, file_name)
758
      finally:
759
        f.close()
760
    finally:
761
      out.close()
762
  except:
763
    RemoveFile(tmpname)
764
    raise
765

    
766

    
767
def SetEtcHostsEntry(file_name, ip, hostname, aliases):
768
  """Sets the name of an IP address and hostname in /etc/hosts.
769

770
  @type file_name: str
771
  @param file_name: path to the file to modify (usually C{/etc/hosts})
772
  @type ip: str
773
  @param ip: the IP address
774
  @type hostname: str
775
  @param hostname: the hostname to be added
776
  @type aliases: list
777
  @param aliases: the list of aliases to add for the hostname
778

779
  """
780
  # Ensure aliases are unique
781
  aliases = UniqueSequence([hostname] + aliases)[1:]
782

    
783
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
784
  try:
785
    out = os.fdopen(fd, 'w')
786
    try:
787
      f = open(file_name, 'r')
788
      try:
789
        written = False
790
        for line in f:
791
          fields = line.split()
792
          if fields and not fields[0].startswith('#') and ip == fields[0]:
793
            continue
794
          out.write(line)
795

    
796
        out.write("%s\t%s" % (ip, hostname))
797
        if aliases:
798
          out.write(" %s" % ' '.join(aliases))
799
        out.write('\n')
800

    
801
        out.flush()
802
        os.fsync(out)
803
        os.rename(tmpname, file_name)
804
      finally:
805
        f.close()
806
    finally:
807
      out.close()
808
  except:
809
    RemoveFile(tmpname)
810
    raise
811

    
812

    
813
def AddHostToEtcHosts(hostname):
814
  """Wrapper around SetEtcHostsEntry.
815

816
  @type hostname: str
817
  @param hostname: a hostname that will be resolved and added to
818
      L{constants.ETC_HOSTS}
819

820
  """
821
  hi = HostInfo(name=hostname)
822
  SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
823

    
824

    
825
def RemoveEtcHostsEntry(file_name, hostname):
826
  """Removes a hostname from /etc/hosts.
827

828
  IP addresses without names are removed from the file.
829

830
  @type file_name: str
831
  @param file_name: path to the file to modify (usually C{/etc/hosts})
832
  @type hostname: str
833
  @param hostname: the hostname to be removed
834

835
  """
836
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
837
  try:
838
    out = os.fdopen(fd, 'w')
839
    try:
840
      f = open(file_name, 'r')
841
      try:
842
        for line in f:
843
          fields = line.split()
844
          if len(fields) > 1 and not fields[0].startswith('#'):
845
            names = fields[1:]
846
            if hostname in names:
847
              while hostname in names:
848
                names.remove(hostname)
849
              if names:
850
                out.write("%s %s\n" % (fields[0], ' '.join(names)))
851
              continue
852

    
853
          out.write(line)
854

    
855
        out.flush()
856
        os.fsync(out)
857
        os.rename(tmpname, file_name)
858
      finally:
859
        f.close()
860
    finally:
861
      out.close()
862
  except:
863
    RemoveFile(tmpname)
864
    raise
865

    
866

    
867
def RemoveHostFromEtcHosts(hostname):
868
  """Wrapper around RemoveEtcHostsEntry.
869

870
  @type hostname: str
871
  @param hostname: hostname that will be resolved and its
872
      full and shot name will be removed from
873
      L{constants.ETC_HOSTS}
874

875
  """
876
  hi = HostInfo(name=hostname)
877
  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
878
  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
879

    
880

    
881
def CreateBackup(file_name):
882
  """Creates a backup of a file.
883

884
  @type file_name: str
885
  @param file_name: file to be backed up
886
  @rtype: str
887
  @return: the path to the newly created backup
888
  @raise errors.ProgrammerError: for invalid file names
889

890
  """
891
  if not os.path.isfile(file_name):
892
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
893
                                file_name)
894

    
895
  prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
896
  dir_name = os.path.dirname(file_name)
897

    
898
  fsrc = open(file_name, 'rb')
899
  try:
900
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
901
    fdst = os.fdopen(fd, 'wb')
902
    try:
903
      shutil.copyfileobj(fsrc, fdst)
904
    finally:
905
      fdst.close()
906
  finally:
907
    fsrc.close()
908

    
909
  return backup_name
910

    
911

    
912
def ShellQuote(value):
913
  """Quotes shell argument according to POSIX.
914

915
  @type value: str
916
  @param value: the argument to be quoted
917
  @rtype: str
918
  @return: the quoted value
919

920
  """
921
  if _re_shell_unquoted.match(value):
922
    return value
923
  else:
924
    return "'%s'" % value.replace("'", "'\\''")
925

    
926

    
927
def ShellQuoteArgs(args):
928
  """Quotes a list of shell arguments.
929

930
  @type args: list
931
  @param args: list of arguments to be quoted
932
  @rtype: str
933
  @return: the quoted arguments concatenaned with spaces
934

935
  """
936
  return ' '.join([ShellQuote(i) for i in args])
937

    
938

    
939
def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
940
  """Simple ping implementation using TCP connect(2).
941

942
  Check if the given IP is reachable by doing attempting a TCP connect
943
  to it.
944

945
  @type target: str
946
  @param target: the IP or hostname to ping
947
  @type port: int
948
  @param port: the port to connect to
949
  @type timeout: int
950
  @param timeout: the timeout on the connection attemp
951
  @type live_port_needed: boolean
952
  @param live_port_needed: whether a closed port will cause the
953
      function to return failure, as if there was a timeout
954
  @type source: str or None
955
  @param source: if specified, will cause the connect to be made
956
      from this specific source address; failures to bind other
957
      than C{EADDRNOTAVAIL} will be ignored
958

959
  """
960
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
961

    
962
  sucess = False
963

    
964
  if source is not None:
965
    try:
966
      sock.bind((source, 0))
967
    except socket.error, (errcode, errstring):
968
      if errcode == errno.EADDRNOTAVAIL:
969
        success = False
970

    
971
  sock.settimeout(timeout)
972

    
973
  try:
974
    sock.connect((target, port))
975
    sock.close()
976
    success = True
977
  except socket.timeout:
978
    success = False
979
  except socket.error, (errcode, errstring):
980
    success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
981

    
982
  return success
983

    
984

    
985
def OwnIpAddress(address):
986
  """Check if the current host has the the given IP address.
987

988
  Currently this is done by TCP-pinging the address from the loopback
989
  address.
990

991
  @type address: string
992
  @param address: the addres to check
993
  @rtype: bool
994
  @return: True if we own the address
995

996
  """
997
  return TcpPing(address, constants.DEFAULT_NODED_PORT,
998
                 source=constants.LOCALHOST_IP_ADDRESS)
999

    
1000

    
1001
def ListVisibleFiles(path):
1002
  """Returns a list of visible files in a directory.
1003

1004
  @type path: str
1005
  @param path: the directory to enumerate
1006
  @rtype: list
1007
  @return: the list of all files not starting with a dot
1008

1009
  """
1010
  files = [i for i in os.listdir(path) if not i.startswith(".")]
1011
  files.sort()
1012
  return files
1013

    
1014

    
1015
def GetHomeDir(user, default=None):
1016
  """Try to get the homedir of the given user.
1017

1018
  The user can be passed either as a string (denoting the name) or as
1019
  an integer (denoting the user id). If the user is not found, the
1020
  'default' argument is returned, which defaults to None.
1021

1022
  """
1023
  try:
1024
    if isinstance(user, basestring):
1025
      result = pwd.getpwnam(user)
1026
    elif isinstance(user, (int, long)):
1027
      result = pwd.getpwuid(user)
1028
    else:
1029
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
1030
                                   type(user))
1031
  except KeyError:
1032
    return default
1033
  return result.pw_dir
1034

    
1035

    
1036
def NewUUID():
1037
  """Returns a random UUID.
1038

1039
  @note: This is a Linux-specific method as it uses the /proc
1040
      filesystem.
1041
  @rtype: str
1042

1043
  """
1044
  f = open("/proc/sys/kernel/random/uuid", "r")
1045
  try:
1046
    return f.read(128).rstrip("\n")
1047
  finally:
1048
    f.close()
1049

    
1050

    
1051
def GenerateSecret():
1052
  """Generates a random secret.
1053

1054
  This will generate a pseudo-random secret, and return its sha digest
1055
  (so that it can be used where an ASCII string is needed).
1056

1057
  @rtype: str
1058
  @return: a sha1 hexdigest of a block of 64 random bytes
1059

1060
  """
1061
  return sha.new(os.urandom(64)).hexdigest()
1062

    
1063

    
1064
def ReadFile(file_name, size=None):
1065
  """Reads a file.
1066

1067
  @type size: None or int
1068
  @param size: Read at most size bytes
1069
  @rtype: str
1070
  @return: the (possibly partial) conent of the file
1071

1072
  """
1073
  f = open(file_name, "r")
1074
  try:
1075
    if size is None:
1076
      return f.read()
1077
    else:
1078
      return f.read(size)
1079
  finally:
1080
    f.close()
1081

    
1082

    
1083
def WriteFile(file_name, fn=None, data=None,
1084
              mode=None, uid=-1, gid=-1,
1085
              atime=None, mtime=None, close=True,
1086
              dry_run=False, backup=False,
1087
              prewrite=None, postwrite=None):
1088
  """(Over)write a file atomically.
1089

1090
  The file_name and either fn (a function taking one argument, the
1091
  file descriptor, and which should write the data to it) or data (the
1092
  contents of the file) must be passed. The other arguments are
1093
  optional and allow setting the file mode, owner and group, and the
1094
  mtime/atime of the file.
1095

1096
  If the function doesn't raise an exception, it has succeeded and the
1097
  target file has the new contents. If the file has raised an
1098
  exception, an existing target file should be unmodified and the
1099
  temporary file should be removed.
1100

1101
  @type file_name: str
1102
  @param file_name: the target filename
1103
  @type fn: callable
1104
  @param fn: content writing function, called with
1105
      file descriptor as parameter
1106
  @type data: sr
1107
  @param data: contents of the file
1108
  @type mode: int
1109
  @param mode: file mode
1110
  @type uid: int
1111
  @param uid: the owner of the file
1112
  @type gid: int
1113
  @param gid: the group of the file
1114
  @type atime: int
1115
  @param atime: a custom access time to be set on the file
1116
  @type mtime: int
1117
  @param mtime: a custom modification time to be set on the file
1118
  @type close: boolean
1119
  @param close: whether to close file after writing it
1120
  @type prewrite: callable
1121
  @param prewrite: function to be called before writing content
1122
  @type postwrite: callable
1123
  @param postwrite: function to be called after writing content
1124

1125
  @rtype: None or int
1126
  @return: None if the 'close' parameter evaluates to True,
1127
      otherwise the file descriptor
1128

1129
  @raise errors.ProgrammerError: if an of the arguments are not valid
1130

1131
  """
1132
  if not os.path.isabs(file_name):
1133
    raise errors.ProgrammerError("Path passed to WriteFile is not"
1134
                                 " absolute: '%s'" % file_name)
1135

    
1136
  if [fn, data].count(None) != 1:
1137
    raise errors.ProgrammerError("fn or data required")
1138

    
1139
  if [atime, mtime].count(None) == 1:
1140
    raise errors.ProgrammerError("Both atime and mtime must be either"
1141
                                 " set or None")
1142

    
1143
  if backup and not dry_run and os.path.isfile(file_name):
1144
    CreateBackup(file_name)
1145

    
1146
  dir_name, base_name = os.path.split(file_name)
1147
  fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1148
  # here we need to make sure we remove the temp file, if any error
1149
  # leaves it in place
1150
  try:
1151
    if uid != -1 or gid != -1:
1152
      os.chown(new_name, uid, gid)
1153
    if mode:
1154
      os.chmod(new_name, mode)
1155
    if callable(prewrite):
1156
      prewrite(fd)
1157
    if data is not None:
1158
      os.write(fd, data)
1159
    else:
1160
      fn(fd)
1161
    if callable(postwrite):
1162
      postwrite(fd)
1163
    os.fsync(fd)
1164
    if atime is not None and mtime is not None:
1165
      os.utime(new_name, (atime, mtime))
1166
    if not dry_run:
1167
      os.rename(new_name, file_name)
1168
  finally:
1169
    if close:
1170
      os.close(fd)
1171
      result = None
1172
    else:
1173
      result = fd
1174
    RemoveFile(new_name)
1175

    
1176
  return result
1177

    
1178

    
1179
def FirstFree(seq, base=0):
1180
  """Returns the first non-existing integer from seq.
1181

1182
  The seq argument should be a sorted list of positive integers. The
1183
  first time the index of an element is smaller than the element
1184
  value, the index will be returned.
1185

1186
  The base argument is used to start at a different offset,
1187
  i.e. C{[3, 4, 6]} with I{offset=3} will return 5.
1188

1189
  Example: C{[0, 1, 3]} will return I{2}.
1190

1191
  @type seq: sequence
1192
  @param seq: the sequence to be analyzed.
1193
  @type base: int
1194
  @param base: use this value as the base index of the sequence
1195
  @rtype: int
1196
  @return: the first non-used index in the sequence
1197

1198
  """
1199
  for idx, elem in enumerate(seq):
1200
    assert elem >= base, "Passed element is higher than base offset"
1201
    if elem > idx + base:
1202
      # idx is not used
1203
      return idx + base
1204
  return None
1205

    
1206

    
1207
def all(seq, pred=bool):
1208
  "Returns True if pred(x) is True for every element in the iterable"
1209
  for elem in itertools.ifilterfalse(pred, seq):
1210
    return False
1211
  return True
1212

    
1213

    
1214
def any(seq, pred=bool):
1215
  "Returns True if pred(x) is True for at least one element in the iterable"
1216
  for elem in itertools.ifilter(pred, seq):
1217
    return True
1218
  return False
1219

    
1220

    
1221
def UniqueSequence(seq):
1222
  """Returns a list with unique elements.
1223

1224
  Element order is preserved.
1225

1226
  @type seq: sequence
1227
  @param seq: the sequence with the source elementes
1228
  @rtype: list
1229
  @return: list of unique elements from seq
1230

1231
  """
1232
  seen = set()
1233
  return [i for i in seq if i not in seen and not seen.add(i)]
1234

    
1235

    
1236
def IsValidMac(mac):
1237
  """Predicate to check if a MAC address is valid.
1238

1239
  Checks wether the supplied MAC address is formally correct, only
1240
  accepts colon separated format.
1241

1242
  @type mac: str
1243
  @param mac: the MAC to be validated
1244
  @rtype: boolean
1245
  @return: True is the MAC seems valid
1246

1247
  """
1248
  mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
1249
  return mac_check.match(mac) is not None
1250

    
1251

    
1252
def TestDelay(duration):
1253
  """Sleep for a fixed amount of time.
1254

1255
  @type duration: float
1256
  @param duration: the sleep duration
1257
  @rtype: boolean
1258
  @return: False for negative value, True otherwise
1259

1260
  """
1261
  if duration < 0:
1262
    return False
1263
  time.sleep(duration)
1264
  return True
1265

    
1266

    
1267
def Daemonize(logfile, noclose_fds=None):
1268
  """Daemonize the current process.
1269

1270
  This detaches the current process from the controlling terminal and
1271
  runs it in the background as a daemon.
1272

1273
  @type logfile: str
1274
  @param logfile: the logfile to which we should redirect stdout/stderr
1275
  @type noclose_fds: list or None
1276
  @param noclose_fds: if given, it denotes a list of file descriptor
1277
      that should not be closed
1278
  @rtype: int
1279
  @returns: the value zero
1280

1281
  """
1282
  UMASK = 077
1283
  WORKDIR = "/"
1284
  # Default maximum for the number of available file descriptors.
1285
  if 'SC_OPEN_MAX' in os.sysconf_names:
1286
    try:
1287
      MAXFD = os.sysconf('SC_OPEN_MAX')
1288
      if MAXFD < 0:
1289
        MAXFD = 1024
1290
    except OSError:
1291
      MAXFD = 1024
1292
  else:
1293
    MAXFD = 1024
1294

    
1295
  # this might fail
1296
  pid = os.fork()
1297
  if (pid == 0):  # The first child.
1298
    os.setsid()
1299
    # this might fail
1300
    pid = os.fork() # Fork a second child.
1301
    if (pid == 0):  # The second child.
1302
      os.chdir(WORKDIR)
1303
      os.umask(UMASK)
1304
    else:
1305
      # exit() or _exit()?  See below.
1306
      os._exit(0) # Exit parent (the first child) of the second child.
1307
  else:
1308
    os._exit(0) # Exit parent of the first child.
1309
  maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1310
  if (maxfd == resource.RLIM_INFINITY):
1311
    maxfd = MAXFD
1312

    
1313
  # Iterate through and close all file descriptors.
1314
  for fd in range(0, maxfd):
1315
    if noclose_fds and fd in noclose_fds:
1316
      continue
1317
    try:
1318
      os.close(fd)
1319
    except OSError: # ERROR, fd wasn't open to begin with (ignored)
1320
      pass
1321
  os.open(logfile, os.O_RDWR|os.O_CREAT|os.O_APPEND, 0600)
1322
  # Duplicate standard input to standard output and standard error.
1323
  os.dup2(0, 1)     # standard output (1)
1324
  os.dup2(0, 2)     # standard error (2)
1325
  return 0
1326

    
1327

    
1328
def DaemonPidFileName(name):
1329
  """Compute a ganeti pid file absolute path
1330

1331
  @type name: str
1332
  @param name: the daemon name
1333
  @rtype: str
1334
  @return: the full path to the pidfile corresponding to the given
1335
      daemon name
1336

1337
  """
1338
  return os.path.join(constants.RUN_GANETI_DIR, "%s.pid" % name)
1339

    
1340

    
1341
def WritePidFile(name):
1342
  """Write the current process pidfile.
1343

1344
  The file will be written to L{constants.RUN_GANETI_DIR}I{/name.pid}
1345

1346
  @type name: str
1347
  @param name: the daemon name to use
1348
  @raise errors.GenericError: if the pid file already exists and
1349
      points to a live process
1350

1351
  """
1352
  pid = os.getpid()
1353
  pidfilename = DaemonPidFileName(name)
1354
  if IsProcessAlive(ReadPidFile(pidfilename)):
1355
    raise errors.GenericError("%s contains a live process" % pidfilename)
1356

    
1357
  WriteFile(pidfilename, data="%d\n" % pid)
1358

    
1359

    
1360
def RemovePidFile(name):
1361
  """Remove the current process pidfile.
1362

1363
  Any errors are ignored.
1364

1365
  @type name: str
1366
  @param name: the daemon name used to derive the pidfile name
1367

1368
  """
1369
  pid = os.getpid()
1370
  pidfilename = DaemonPidFileName(name)
1371
  # TODO: we could check here that the file contains our pid
1372
  try:
1373
    RemoveFile(pidfilename)
1374
  except:
1375
    pass
1376

    
1377

    
1378
def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1379
                waitpid=False):
1380
  """Kill a process given by its pid.
1381

1382
  @type pid: int
1383
  @param pid: The PID to terminate.
1384
  @type signal_: int
1385
  @param signal_: The signal to send, by default SIGTERM
1386
  @type timeout: int
1387
  @param timeout: The timeout after which, if the process is still alive,
1388
                  a SIGKILL will be sent. If not positive, no such checking
1389
                  will be done
1390
  @type waitpid: boolean
1391
  @param waitpid: If true, we should waitpid on this process after
1392
      sending signals, since it's our own child and otherwise it
1393
      would remain as zombie
1394

1395
  """
1396
  def _helper(pid, signal_, wait):
1397
    """Simple helper to encapsulate the kill/waitpid sequence"""
1398
    os.kill(pid, signal_)
1399
    if wait:
1400
      try:
1401
        os.waitpid(pid, os.WNOHANG)
1402
      except OSError:
1403
        pass
1404

    
1405
  if pid <= 0:
1406
    # kill with pid=0 == suicide
1407
    raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1408

    
1409
  if not IsProcessAlive(pid):
1410
    return
1411
  _helper(pid, signal_, waitpid)
1412
  if timeout <= 0:
1413
    return
1414
  end = time.time() + timeout
1415
  while time.time() < end and IsProcessAlive(pid):
1416
    time.sleep(0.1)
1417
  if IsProcessAlive(pid):
1418
    _helper(pid, signal.SIGKILL, waitpid)
1419

    
1420

    
1421
def FindFile(name, search_path, test=os.path.exists):
1422
  """Look for a filesystem object in a given path.
1423

1424
  This is an abstract method to search for filesystem object (files,
1425
  dirs) under a given search path.
1426

1427
  @type name: str
1428
  @param name: the name to look for
1429
  @type search_path: str
1430
  @param search_path: location to start at
1431
  @type test: callable
1432
  @param test: a function taking one argument that should return True
1433
      if the a given object is valid; the default value is
1434
      os.path.exists, causing only existing files to be returned
1435
  @rtype: str or None
1436
  @return: full path to the object if found, None otherwise
1437

1438
  """
1439
  for dir_name in search_path:
1440
    item_name = os.path.sep.join([dir_name, name])
1441
    if test(item_name):
1442
      return item_name
1443
  return None
1444

    
1445

    
1446
def CheckVolumeGroupSize(vglist, vgname, minsize):
1447
  """Checks if the volume group list is valid.
1448

1449
  The function will check if a given volume group is in the list of
1450
  volume groups and has a minimum size.
1451

1452
  @type vglist: dict
1453
  @param vglist: dictionary of volume group names and their size
1454
  @type vgname: str
1455
  @param vgname: the volume group we should check
1456
  @type minsize: int
1457
  @param minsize: the minimum size we accept
1458
  @rtype: None or str
1459
  @return: None for success, otherwise the error message
1460

1461
  """
1462
  vgsize = vglist.get(vgname, None)
1463
  if vgsize is None:
1464
    return "volume group '%s' missing" % vgname
1465
  elif vgsize < minsize:
1466
    return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1467
            (vgname, minsize, vgsize))
1468
  return None
1469

    
1470

    
1471
def SplitTime(value):
1472
  """Splits time as floating point number into a tuple.
1473

1474
  @param value: Time in seconds
1475
  @type value: int or float
1476
  @return: Tuple containing (seconds, microseconds)
1477

1478
  """
1479
  (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
1480

    
1481
  assert 0 <= seconds, \
1482
    "Seconds must be larger than or equal to 0, but are %s" % seconds
1483
  assert 0 <= microseconds <= 999999, \
1484
    "Microseconds must be 0-999999, but are %s" % microseconds
1485

    
1486
  return (int(seconds), int(microseconds))
1487

    
1488

    
1489
def MergeTime(timetuple):
1490
  """Merges a tuple into time as a floating point number.
1491

1492
  @param timetuple: Time as tuple, (seconds, microseconds)
1493
  @type timetuple: tuple
1494
  @return: Time as a floating point number expressed in seconds
1495

1496
  """
1497
  (seconds, microseconds) = timetuple
1498

    
1499
  assert 0 <= seconds, \
1500
    "Seconds must be larger than or equal to 0, but are %s" % seconds
1501
  assert 0 <= microseconds <= 999999, \
1502
    "Microseconds must be 0-999999, but are %s" % microseconds
1503

    
1504
  return float(seconds) + (float(microseconds) * 0.000001)
1505

    
1506

    
1507
def GetNodeDaemonPort():
1508
  """Get the node daemon port for this cluster.
1509

1510
  Note that this routine does not read a ganeti-specific file, but
1511
  instead uses C{socket.getservbyname} to allow pre-customization of
1512
  this parameter outside of Ganeti.
1513

1514
  @rtype: int
1515

1516
  """
1517
  try:
1518
    port = socket.getservbyname("ganeti-noded", "tcp")
1519
  except socket.error:
1520
    port = constants.DEFAULT_NODED_PORT
1521

    
1522
  return port
1523

    
1524

    
1525
def GetNodeDaemonPassword():
1526
  """Get the node password for the cluster.
1527

1528
  @rtype: str
1529

1530
  """
1531
  return ReadFile(constants.CLUSTER_PASSWORD_FILE)
1532

    
1533

    
1534
def SetupLogging(logfile, debug=False, stderr_logging=False, program=""):
1535
  """Configures the logging module.
1536

1537
  @type logfile: str
1538
  @param logfile: the filename to which we should log
1539
  @type debug: boolean
1540
  @param debug: whether to enable debug messages too or
1541
      only those at C{INFO} and above level
1542
  @type stderr_logging: boolean
1543
  @param stderr_logging: whether we should also log to the standard error
1544
  @type program: str
1545
  @param program: the name under which we should log messages
1546
  @raise EnvironmentError: if we can't open the log file and
1547
      stderr logging is disabled
1548

1549
  """
1550
  fmt = "%(asctime)s: " + program + " "
1551
  if debug:
1552
    fmt += ("pid=%(process)d/%(threadName)s %(levelname)s"
1553
           " %(module)s:%(lineno)s %(message)s")
1554
  else:
1555
    fmt += "pid=%(process)d %(levelname)s %(message)s"
1556
  formatter = logging.Formatter(fmt)
1557

    
1558
  root_logger = logging.getLogger("")
1559
  root_logger.setLevel(logging.NOTSET)
1560

    
1561
  if stderr_logging:
1562
    stderr_handler = logging.StreamHandler()
1563
    stderr_handler.setFormatter(formatter)
1564
    if debug:
1565
      stderr_handler.setLevel(logging.NOTSET)
1566
    else:
1567
      stderr_handler.setLevel(logging.CRITICAL)
1568
    root_logger.addHandler(stderr_handler)
1569

    
1570
  # this can fail, if the logging directories are not setup or we have
1571
  # a permisssion problem; in this case, it's best to log but ignore
1572
  # the error if stderr_logging is True, and if false we re-raise the
1573
  # exception since otherwise we could run but without any logs at all
1574
  try:
1575
    logfile_handler = logging.FileHandler(logfile)
1576
    logfile_handler.setFormatter(formatter)
1577
    if debug:
1578
      logfile_handler.setLevel(logging.DEBUG)
1579
    else:
1580
      logfile_handler.setLevel(logging.INFO)
1581
    root_logger.addHandler(logfile_handler)
1582
  except EnvironmentError, err:
1583
    if stderr_logging:
1584
      logging.exception("Failed to enable logging to file '%s'", logfile)
1585
    else:
1586
      # we need to re-raise the exception
1587
      raise
1588

    
1589

    
1590
def LockedMethod(fn):
1591
  """Synchronized object access decorator.
1592

1593
  This decorator is intended to protect access to an object using the
1594
  object's own lock which is hardcoded to '_lock'.
1595

1596
  """
1597
  def _LockDebug(*args, **kwargs):
1598
    if debug_locks:
1599
      logging.debug(*args, **kwargs)
1600

    
1601
  def wrapper(self, *args, **kwargs):
1602
    assert hasattr(self, '_lock')
1603
    lock = self._lock
1604
    _LockDebug("Waiting for %s", lock)
1605
    lock.acquire()
1606
    try:
1607
      _LockDebug("Acquired %s", lock)
1608
      result = fn(self, *args, **kwargs)
1609
    finally:
1610
      _LockDebug("Releasing %s", lock)
1611
      lock.release()
1612
      _LockDebug("Released %s", lock)
1613
    return result
1614
  return wrapper
1615

    
1616

    
1617
def LockFile(fd):
1618
  """Locks a file using POSIX locks.
1619

1620
  @type fd: int
1621
  @param fd: the file descriptor we need to lock
1622

1623
  """
1624
  try:
1625
    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1626
  except IOError, err:
1627
    if err.errno == errno.EAGAIN:
1628
      raise errors.LockError("File already locked")
1629
    raise
1630

    
1631

    
1632
class FileLock(object):
1633
  """Utility class for file locks.
1634

1635
  """
1636
  def __init__(self, filename):
1637
    """Constructor for FileLock.
1638

1639
    This will open the file denoted by the I{filename} argument.
1640

1641
    @type filename: str
1642
    @param filename: path to the file to be locked
1643

1644
    """
1645
    self.filename = filename
1646
    self.fd = open(self.filename, "w")
1647

    
1648
  def __del__(self):
1649
    self.Close()
1650

    
1651
  def Close(self):
1652
    """Close the file and release the lock.
1653

1654
    """
1655
    if self.fd:
1656
      self.fd.close()
1657
      self.fd = None
1658

    
1659
  def _flock(self, flag, blocking, timeout, errmsg):
1660
    """Wrapper for fcntl.flock.
1661

1662
    @type flag: int
1663
    @param flag: operation flag
1664
    @type blocking: bool
1665
    @param blocking: whether the operation should be done in blocking mode.
1666
    @type timeout: None or float
1667
    @param timeout: for how long the operation should be retried (implies
1668
                    non-blocking mode).
1669
    @type errmsg: string
1670
    @param errmsg: error message in case operation fails.
1671

1672
    """
1673
    assert self.fd, "Lock was closed"
1674
    assert timeout is None or timeout >= 0, \
1675
      "If specified, timeout must be positive"
1676

    
1677
    if timeout is not None:
1678
      flag |= fcntl.LOCK_NB
1679
      timeout_end = time.time() + timeout
1680

    
1681
    # Blocking doesn't have effect with timeout
1682
    elif not blocking:
1683
      flag |= fcntl.LOCK_NB
1684
      timeout_end = None
1685

    
1686
    retry = True
1687
    while retry:
1688
      try:
1689
        fcntl.flock(self.fd, flag)
1690
        retry = False
1691
      except IOError, err:
1692
        if err.errno in (errno.EAGAIN, ):
1693
          if timeout_end is not None and time.time() < timeout_end:
1694
            # Wait before trying again
1695
            time.sleep(max(0.1, min(1.0, timeout)))
1696
          else:
1697
            raise errors.LockError(errmsg)
1698
        else:
1699
          logging.exception("fcntl.flock failed")
1700
          raise
1701

    
1702
  def Exclusive(self, blocking=False, timeout=None):
1703
    """Locks the file in exclusive mode.
1704

1705
    @type blocking: boolean
1706
    @param blocking: whether to block and wait until we
1707
        can lock the file or return immediately
1708
    @type timeout: int or None
1709
    @param timeout: if not None, the duration to wait for the lock
1710
        (in blocking mode)
1711

1712
    """
1713
    self._flock(fcntl.LOCK_EX, blocking, timeout,
1714
                "Failed to lock %s in exclusive mode" % self.filename)
1715

    
1716
  def Shared(self, blocking=False, timeout=None):
1717
    """Locks the file in shared mode.
1718

1719
    @type blocking: boolean
1720
    @param blocking: whether to block and wait until we
1721
        can lock the file or return immediately
1722
    @type timeout: int or None
1723
    @param timeout: if not None, the duration to wait for the lock
1724
        (in blocking mode)
1725

1726
    """
1727
    self._flock(fcntl.LOCK_SH, blocking, timeout,
1728
                "Failed to lock %s in shared mode" % self.filename)
1729

    
1730
  def Unlock(self, blocking=True, timeout=None):
1731
    """Unlocks the file.
1732

1733
    According to C{flock(2)}, unlocking can also be a nonblocking
1734
    operation::
1735

1736
      To make a non-blocking request, include LOCK_NB with any of the above
1737
      operations.
1738

1739
    @type blocking: boolean
1740
    @param blocking: whether to block and wait until we
1741
        can lock the file or return immediately
1742
    @type timeout: int or None
1743
    @param timeout: if not None, the duration to wait for the lock
1744
        (in blocking mode)
1745

1746
    """
1747
    self._flock(fcntl.LOCK_UN, blocking, timeout,
1748
                "Failed to unlock %s" % self.filename)
1749

    
1750

    
1751
class SignalHandler(object):
1752
  """Generic signal handler class.
1753

1754
  It automatically restores the original handler when deconstructed or
1755
  when L{Reset} is called. You can either pass your own handler
1756
  function in or query the L{called} attribute to detect whether the
1757
  signal was sent.
1758

1759
  @type signum: list
1760
  @ivar signum: the signals we handle
1761
  @type called: boolean
1762
  @ivar called: tracks whether any of the signals have been raised
1763

1764
  """
1765
  def __init__(self, signum):
1766
    """Constructs a new SignalHandler instance.
1767

1768
    @type signum: int or list of ints
1769
    @param signum: Single signal number or set of signal numbers
1770

1771
    """
1772
    if isinstance(signum, (int, long)):
1773
      self.signum = set([signum])
1774
    else:
1775
      self.signum = set(signum)
1776

    
1777
    self.called = False
1778

    
1779
    self._previous = {}
1780
    try:
1781
      for signum in self.signum:
1782
        # Setup handler
1783
        prev_handler = signal.signal(signum, self._HandleSignal)
1784
        try:
1785
          self._previous[signum] = prev_handler
1786
        except:
1787
          # Restore previous handler
1788
          signal.signal(signum, prev_handler)
1789
          raise
1790
    except:
1791
      # Reset all handlers
1792
      self.Reset()
1793
      # Here we have a race condition: a handler may have already been called,
1794
      # but there's not much we can do about it at this point.
1795
      raise
1796

    
1797
  def __del__(self):
1798
    self.Reset()
1799

    
1800
  def Reset(self):
1801
    """Restore previous handler.
1802

1803
    This will reset all the signals to their previous handlers.
1804

1805
    """
1806
    for signum, prev_handler in self._previous.items():
1807
      signal.signal(signum, prev_handler)
1808
      # If successful, remove from dict
1809
      del self._previous[signum]
1810

    
1811
  def Clear(self):
1812
    """Unsets the L{called} flag.
1813

1814
    This function can be used in case a signal may arrive several times.
1815

1816
    """
1817
    self.called = False
1818

    
1819
  def _HandleSignal(self, signum, frame):
1820
    """Actual signal handling function.
1821

1822
    """
1823
    # This is not nice and not absolutely atomic, but it appears to be the only
1824
    # solution in Python -- there are no atomic types.
1825
    self.called = True