Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ f65f63ef

History | View | Annotate | Download (52.6 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
    try:
207
      pollresult = poller.poll()
208
    except EnvironmentError, eerr:
209
      if eerr.errno == errno.EINTR:
210
        continue
211
      raise
212
    except select.error, serr:
213
      if serr[0] == errno.EINTR:
214
        continue
215
      raise
216

    
217
    for fd, event in pollresult:
218
      if event & select.POLLIN or event & select.POLLPRI:
219
        data = fdmap[fd][1].read()
220
        # no data from read signifies EOF (the same as POLLHUP)
221
        if not data:
222
          poller.unregister(fd)
223
          del fdmap[fd]
224
          continue
225
        fdmap[fd][0].write(data)
226
      if (event & select.POLLNVAL or event & select.POLLHUP or
227
          event & select.POLLERR):
228
        poller.unregister(fd)
229
        del fdmap[fd]
230

    
231
  out = out.getvalue()
232
  err = err.getvalue()
233

    
234
  status = child.wait()
235
  return out, err, status
236

    
237

    
238
def _RunCmdFile(cmd, env, via_shell, output, cwd):
239
  """Run a command and save its output to a file.
240

241
  @type  cmd: string or list
242
  @param cmd: Command to run
243
  @type env: dict
244
  @param env: The environment to use
245
  @type via_shell: bool
246
  @param via_shell: if we should run via the shell
247
  @type output: str
248
  @param output: the filename in which to save the output
249
  @type cwd: string
250
  @param cwd: the working directory for the program
251
  @rtype: int
252
  @return: the exit status
253

254
  """
255
  fh = open(output, "a")
256
  try:
257
    child = subprocess.Popen(cmd, shell=via_shell,
258
                             stderr=subprocess.STDOUT,
259
                             stdout=fh,
260
                             stdin=subprocess.PIPE,
261
                             close_fds=True, env=env,
262
                             cwd=cwd)
263

    
264
    child.stdin.close()
265
    status = child.wait()
266
  finally:
267
    fh.close()
268
  return status
269

    
270

    
271
def RemoveFile(filename):
272
  """Remove a file ignoring some errors.
273

274
  Remove a file, ignoring non-existing ones or directories. Other
275
  errors are passed.
276

277
  @type filename: str
278
  @param filename: the file to be removed
279

280
  """
281
  try:
282
    os.unlink(filename)
283
  except OSError, err:
284
    if err.errno not in (errno.ENOENT, errno.EISDIR):
285
      raise
286

    
287

    
288
def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
289
  """Renames a file.
290

291
  @type old: string
292
  @param old: Original path
293
  @type new: string
294
  @param new: New path
295
  @type mkdir: bool
296
  @param mkdir: Whether to create target directory if it doesn't exist
297
  @type mkdir_mode: int
298
  @param mkdir_mode: Mode for newly created directories
299

300
  """
301
  try:
302
    return os.rename(old, new)
303
  except OSError, err:
304
    # In at least one use case of this function, the job queue, directory
305
    # creation is very rare. Checking for the directory before renaming is not
306
    # as efficient.
307
    if mkdir and err.errno == errno.ENOENT:
308
      # Create directory and try again
309
      os.makedirs(os.path.dirname(new), mkdir_mode)
310
      return os.rename(old, new)
311
    raise
312

    
313

    
314
def _FingerprintFile(filename):
315
  """Compute the fingerprint of a file.
316

317
  If the file does not exist, a None will be returned
318
  instead.
319

320
  @type filename: str
321
  @param filename: the filename to checksum
322
  @rtype: str
323
  @return: the hex digest of the sha checksum of the contents
324
      of the file
325

326
  """
327
  if not (os.path.exists(filename) and os.path.isfile(filename)):
328
    return None
329

    
330
  f = open(filename)
331

    
332
  fp = sha.sha()
333
  while True:
334
    data = f.read(4096)
335
    if not data:
336
      break
337

    
338
    fp.update(data)
339

    
340
  return fp.hexdigest()
341

    
342

    
343
def FingerprintFiles(files):
344
  """Compute fingerprints for a list of files.
345

346
  @type files: list
347
  @param files: the list of filename to fingerprint
348
  @rtype: dict
349
  @return: a dictionary filename: fingerprint, holding only
350
      existing files
351

352
  """
353
  ret = {}
354

    
355
  for filename in files:
356
    cksum = _FingerprintFile(filename)
357
    if cksum:
358
      ret[filename] = cksum
359

    
360
  return ret
361

    
362

    
363
def CheckDict(target, template, logname=None):
364
  """Ensure a dictionary has a required set of keys.
365

366
  For the given dictionaries I{target} and I{template}, ensure
367
  I{target} has all the keys from I{template}. Missing keys are added
368
  with values from template.
369

370
  @type target: dict
371
  @param target: the dictionary to update
372
  @type template: dict
373
  @param template: the dictionary holding the default values
374
  @type logname: str or None
375
  @param logname: if not None, causes the missing keys to be
376
      logged with this name
377

378
  """
379
  missing = []
380
  for k in template:
381
    if k not in target:
382
      missing.append(k)
383
      target[k] = template[k]
384

    
385
  if missing and logname:
386
    logging.warning('%s missing keys %s', logname, ', '.join(missing))
387

    
388

    
389
def IsProcessAlive(pid):
390
  """Check if a given pid exists on the system.
391

392
  @note: zombie status is not handled, so zombie processes
393
      will be returned as alive
394
  @type pid: int
395
  @param pid: the process ID to check
396
  @rtype: boolean
397
  @return: True if the process exists
398

399
  """
400
  if pid <= 0:
401
    return False
402

    
403
  try:
404
    os.stat("/proc/%d/status" % pid)
405
    return True
406
  except EnvironmentError, err:
407
    if err.errno in (errno.ENOENT, errno.ENOTDIR):
408
      return False
409
    raise
410

    
411

    
412
def ReadPidFile(pidfile):
413
  """Read a pid from a file.
414

415
  @type  pidfile: string
416
  @param pidfile: path to the file containing the pid
417
  @rtype: int
418
  @return: The process id, if the file exista and contains a valid PID,
419
           otherwise 0
420

421
  """
422
  try:
423
    pf = open(pidfile, 'r')
424
  except EnvironmentError, err:
425
    if err.errno != errno.ENOENT:
426
      logging.exception("Can't read pid file?!")
427
    return 0
428

    
429
  try:
430
    pid = int(pf.read())
431
  except ValueError, err:
432
    logging.info("Can't parse pid file contents", exc_info=True)
433
    return 0
434

    
435
  return pid
436

    
437

    
438
def MatchNameComponent(key, name_list):
439
  """Try to match a name against a list.
440

441
  This function will try to match a name like test1 against a list
442
  like C{['test1.example.com', 'test2.example.com', ...]}. Against
443
  this list, I{'test1'} as well as I{'test1.example'} will match, but
444
  not I{'test1.ex'}. A multiple match will be considered as no match
445
  at all (e.g. I{'test1'} against C{['test1.example.com',
446
  'test1.example.org']}).
447

448
  @type key: str
449
  @param key: the name to be searched
450
  @type name_list: list
451
  @param name_list: the list of strings against which to search the key
452

453
  @rtype: None or str
454
  @return: None if there is no match I{or} if there are multiple matches,
455
      otherwise the element from the list which matches
456

457
  """
458
  mo = re.compile("^%s(\..*)?$" % re.escape(key))
459
  names_filtered = [name for name in name_list if mo.match(name) is not None]
460
  if len(names_filtered) != 1:
461
    return None
462
  return names_filtered[0]
463

    
464

    
465
class HostInfo:
466
  """Class implementing resolver and hostname functionality
467

468
  """
469
  def __init__(self, name=None):
470
    """Initialize the host name object.
471

472
    If the name argument is not passed, it will use this system's
473
    name.
474

475
    """
476
    if name is None:
477
      name = self.SysName()
478

    
479
    self.query = name
480
    self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
481
    self.ip = self.ipaddrs[0]
482

    
483
  def ShortName(self):
484
    """Returns the hostname without domain.
485

486
    """
487
    return self.name.split('.')[0]
488

    
489
  @staticmethod
490
  def SysName():
491
    """Return the current system's name.
492

493
    This is simply a wrapper over C{socket.gethostname()}.
494

495
    """
496
    return socket.gethostname()
497

    
498
  @staticmethod
499
  def LookupHostname(hostname):
500
    """Look up hostname
501

502
    @type hostname: str
503
    @param hostname: hostname to look up
504

505
    @rtype: tuple
506
    @return: a tuple (name, aliases, ipaddrs) as returned by
507
        C{socket.gethostbyname_ex}
508
    @raise errors.ResolverError: in case of errors in resolving
509

510
    """
511
    try:
512
      result = socket.gethostbyname_ex(hostname)
513
    except socket.gaierror, err:
514
      # hostname not found in DNS
515
      raise errors.ResolverError(hostname, err.args[0], err.args[1])
516

    
517
    return result
518

    
519

    
520
def ListVolumeGroups():
521
  """List volume groups and their size
522

523
  @rtype: dict
524
  @return:
525
       Dictionary with keys volume name and values
526
       the size of the volume
527

528
  """
529
  command = "vgs --noheadings --units m --nosuffix -o name,size"
530
  result = RunCmd(command)
531
  retval = {}
532
  if result.failed:
533
    return retval
534

    
535
  for line in result.stdout.splitlines():
536
    try:
537
      name, size = line.split()
538
      size = int(float(size))
539
    except (IndexError, ValueError), err:
540
      logging.error("Invalid output from vgs (%s): %s", err, line)
541
      continue
542

    
543
    retval[name] = size
544

    
545
  return retval
546

    
547

    
548
def BridgeExists(bridge):
549
  """Check whether the given bridge exists in the system
550

551
  @type bridge: str
552
  @param bridge: the bridge name to check
553
  @rtype: boolean
554
  @return: True if it does
555

556
  """
557
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
558

    
559

    
560
def CheckBEParams(beparams):
561
  """Checks whether the user-supplied be-params are valid,
562
  and converts them from string format where appropriate.
563

564
  @type beparams: dict
565
  @param beparams: new params dict
566

567
  """
568
  if beparams:
569
    for item in beparams:
570
      if item not in constants.BES_PARAMETERS:
571
        raise errors.OpPrereqError("Unknown backend parameter %s" % item)
572
      if item in (constants.BE_MEMORY, constants.BE_VCPUS):
573
        val = beparams[item]
574
        if val != constants.VALUE_DEFAULT:
575
          try:
576
            val = int(val)
577
          except ValueError, err:
578
            raise errors.OpPrereqError("Invalid %s size: %s" % (item, str(err)))
579
          beparams[item] = val
580
      if item in (constants.BE_AUTO_BALANCE):
581
        val = beparams[item]
582
        if not isinstance(val, bool):
583
          if val == constants.VALUE_TRUE:
584
            beparams[item] = True
585
          elif val == constants.VALUE_FALSE:
586
            beparams[item] = False
587
          else:
588
            raise errors.OpPrereqError("Invalid %s value: %s" % (item, val))
589

    
590

    
591
def NiceSort(name_list):
592
  """Sort a list of strings based on digit and non-digit groupings.
593

594
  Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
595
  will sort the list in the logical order C{['a1', 'a2', 'a10',
596
  'a11']}.
597

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

602
  @type name_list: list
603
  @param name_list: the names to be sorted
604
  @rtype: list
605
  @return: a copy of the name list sorted with our algorithm
606

607
  """
608
  _SORTER_BASE = "(\D+|\d+)"
609
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
610
                                                  _SORTER_BASE, _SORTER_BASE,
611
                                                  _SORTER_BASE, _SORTER_BASE,
612
                                                  _SORTER_BASE, _SORTER_BASE)
613
  _SORTER_RE = re.compile(_SORTER_FULL)
614
  _SORTER_NODIGIT = re.compile("^\D*$")
615
  def _TryInt(val):
616
    """Attempts to convert a variable to integer."""
617
    if val is None or _SORTER_NODIGIT.match(val):
618
      return val
619
    rval = int(val)
620
    return rval
621

    
622
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
623
             for name in name_list]
624
  to_sort.sort()
625
  return [tup[1] for tup in to_sort]
626

    
627

    
628
def TryConvert(fn, val):
629
  """Try to convert a value ignoring errors.
630

631
  This function tries to apply function I{fn} to I{val}. If no
632
  C{ValueError} or C{TypeError} exceptions are raised, it will return
633
  the result, else it will return the original value. Any other
634
  exceptions are propagated to the caller.
635

636
  @type fn: callable
637
  @param fn: function to apply to the value
638
  @param val: the value to be converted
639
  @return: The converted value if the conversion was successful,
640
      otherwise the original value.
641

642
  """
643
  try:
644
    nv = fn(val)
645
  except (ValueError, TypeError), err:
646
    nv = val
647
  return nv
648

    
649

    
650
def IsValidIP(ip):
651
  """Verifies the syntax of an IPv4 address.
652

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

656
  @type ip: str
657
  @param ip: the address to be checked
658
  @rtype: a regular expression match object
659
  @return: a regular epression match object, or None if the
660
      address is not valid
661

662
  """
663
  unit = "(0|[1-9]\d{0,2})"
664
  #TODO: convert and return only boolean
665
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
666

    
667

    
668
def IsValidShellParam(word):
669
  """Verifies is the given word is safe from the shell's p.o.v.
670

671
  This means that we can pass this to a command via the shell and be
672
  sure that it doesn't alter the command line and is passed as such to
673
  the actual command.
674

675
  Note that we are overly restrictive here, in order to be on the safe
676
  side.
677

678
  @type word: str
679
  @param word: the word to check
680
  @rtype: boolean
681
  @return: True if the word is 'safe'
682

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

    
686

    
687
def BuildShellCmd(template, *args):
688
  """Build a safe shell command line from the given arguments.
689

690
  This function will check all arguments in the args list so that they
691
  are valid shell parameters (i.e. they don't contain shell
692
  metacharaters). If everything is ok, it will return the result of
693
  template % args.
694

695
  @type template: str
696
  @param template: the string holding the template for the
697
      string formatting
698
  @rtype: str
699
  @return: the expanded command line
700

701
  """
702
  for word in args:
703
    if not IsValidShellParam(word):
704
      raise errors.ProgrammerError("Shell argument '%s' contains"
705
                                   " invalid characters" % word)
706
  return template % args
707

    
708

    
709
def FormatUnit(value, units):
710
  """Formats an incoming number of MiB with the appropriate unit.
711

712
  @type value: int
713
  @param value: integer representing the value in MiB (1048576)
714
  @type units: char
715
  @param units: the type of formatting we should do:
716
      - 'h' for automatic scaling
717
      - 'm' for MiBs
718
      - 'g' for GiBs
719
      - 't' for TiBs
720
  @rtype: str
721
  @return: the formatted value (with suffix)
722

723
  """
724
  if units not in ('m', 'g', 't', 'h'):
725
    raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
726

    
727
  suffix = ''
728

    
729
  if units == 'm' or (units == 'h' and value < 1024):
730
    if units == 'h':
731
      suffix = 'M'
732
    return "%d%s" % (round(value, 0), suffix)
733

    
734
  elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
735
    if units == 'h':
736
      suffix = 'G'
737
    return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
738

    
739
  else:
740
    if units == 'h':
741
      suffix = 'T'
742
    return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
743

    
744

    
745
def ParseUnit(input_string):
746
  """Tries to extract number and scale from the given string.
747

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

752
  """
753
  m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
754
  if not m:
755
    raise errors.UnitParseError("Invalid format")
756

    
757
  value = float(m.groups()[0])
758

    
759
  unit = m.groups()[1]
760
  if unit:
761
    lcunit = unit.lower()
762
  else:
763
    lcunit = 'm'
764

    
765
  if lcunit in ('m', 'mb', 'mib'):
766
    # Value already in MiB
767
    pass
768

    
769
  elif lcunit in ('g', 'gb', 'gib'):
770
    value *= 1024
771

    
772
  elif lcunit in ('t', 'tb', 'tib'):
773
    value *= 1024 * 1024
774

    
775
  else:
776
    raise errors.UnitParseError("Unknown unit: %s" % unit)
777

    
778
  # Make sure we round up
779
  if int(value) < value:
780
    value += 1
781

    
782
  # Round up to the next multiple of 4
783
  value = int(value)
784
  if value % 4:
785
    value += 4 - value % 4
786

    
787
  return value
788

    
789

    
790
def AddAuthorizedKey(file_name, key):
791
  """Adds an SSH public key to an authorized_keys file.
792

793
  @type file_name: str
794
  @param file_name: path to authorized_keys file
795
  @type key: str
796
  @param key: string containing key
797

798
  """
799
  key_fields = key.split()
800

    
801
  f = open(file_name, 'a+')
802
  try:
803
    nl = True
804
    for line in f:
805
      # Ignore whitespace changes
806
      if line.split() == key_fields:
807
        break
808
      nl = line.endswith('\n')
809
    else:
810
      if not nl:
811
        f.write("\n")
812
      f.write(key.rstrip('\r\n'))
813
      f.write("\n")
814
      f.flush()
815
  finally:
816
    f.close()
817

    
818

    
819
def RemoveAuthorizedKey(file_name, key):
820
  """Removes an SSH public key from an authorized_keys file.
821

822
  @type file_name: str
823
  @param file_name: path to authorized_keys file
824
  @type key: str
825
  @param key: string containing key
826

827
  """
828
  key_fields = key.split()
829

    
830
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
831
  try:
832
    out = os.fdopen(fd, 'w')
833
    try:
834
      f = open(file_name, 'r')
835
      try:
836
        for line in f:
837
          # Ignore whitespace changes while comparing lines
838
          if line.split() != key_fields:
839
            out.write(line)
840

    
841
        out.flush()
842
        os.rename(tmpname, file_name)
843
      finally:
844
        f.close()
845
    finally:
846
      out.close()
847
  except:
848
    RemoveFile(tmpname)
849
    raise
850

    
851

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

855
  @type file_name: str
856
  @param file_name: path to the file to modify (usually C{/etc/hosts})
857
  @type ip: str
858
  @param ip: the IP address
859
  @type hostname: str
860
  @param hostname: the hostname to be added
861
  @type aliases: list
862
  @param aliases: the list of aliases to add for the hostname
863

864
  """
865
  # Ensure aliases are unique
866
  aliases = UniqueSequence([hostname] + aliases)[1:]
867

    
868
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
869
  try:
870
    out = os.fdopen(fd, 'w')
871
    try:
872
      f = open(file_name, 'r')
873
      try:
874
        for line in f:
875
          fields = line.split()
876
          if fields and not fields[0].startswith('#') and ip == fields[0]:
877
            continue
878
          out.write(line)
879

    
880
        out.write("%s\t%s" % (ip, hostname))
881
        if aliases:
882
          out.write(" %s" % ' '.join(aliases))
883
        out.write('\n')
884

    
885
        out.flush()
886
        os.fsync(out)
887
        os.rename(tmpname, file_name)
888
      finally:
889
        f.close()
890
    finally:
891
      out.close()
892
  except:
893
    RemoveFile(tmpname)
894
    raise
895

    
896

    
897
def AddHostToEtcHosts(hostname):
898
  """Wrapper around SetEtcHostsEntry.
899

900
  @type hostname: str
901
  @param hostname: a hostname that will be resolved and added to
902
      L{constants.ETC_HOSTS}
903

904
  """
905
  hi = HostInfo(name=hostname)
906
  SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
907

    
908

    
909
def RemoveEtcHostsEntry(file_name, hostname):
910
  """Removes a hostname from /etc/hosts.
911

912
  IP addresses without names are removed from the file.
913

914
  @type file_name: str
915
  @param file_name: path to the file to modify (usually C{/etc/hosts})
916
  @type hostname: str
917
  @param hostname: the hostname to be removed
918

919
  """
920
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
921
  try:
922
    out = os.fdopen(fd, 'w')
923
    try:
924
      f = open(file_name, 'r')
925
      try:
926
        for line in f:
927
          fields = line.split()
928
          if len(fields) > 1 and not fields[0].startswith('#'):
929
            names = fields[1:]
930
            if hostname in names:
931
              while hostname in names:
932
                names.remove(hostname)
933
              if names:
934
                out.write("%s %s\n" % (fields[0], ' '.join(names)))
935
              continue
936

    
937
          out.write(line)
938

    
939
        out.flush()
940
        os.fsync(out)
941
        os.rename(tmpname, file_name)
942
      finally:
943
        f.close()
944
    finally:
945
      out.close()
946
  except:
947
    RemoveFile(tmpname)
948
    raise
949

    
950

    
951
def RemoveHostFromEtcHosts(hostname):
952
  """Wrapper around RemoveEtcHostsEntry.
953

954
  @type hostname: str
955
  @param hostname: hostname that will be resolved and its
956
      full and shot name will be removed from
957
      L{constants.ETC_HOSTS}
958

959
  """
960
  hi = HostInfo(name=hostname)
961
  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
962
  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
963

    
964

    
965
def CreateBackup(file_name):
966
  """Creates a backup of a file.
967

968
  @type file_name: str
969
  @param file_name: file to be backed up
970
  @rtype: str
971
  @return: the path to the newly created backup
972
  @raise errors.ProgrammerError: for invalid file names
973

974
  """
975
  if not os.path.isfile(file_name):
976
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
977
                                file_name)
978

    
979
  prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
980
  dir_name = os.path.dirname(file_name)
981

    
982
  fsrc = open(file_name, 'rb')
983
  try:
984
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
985
    fdst = os.fdopen(fd, 'wb')
986
    try:
987
      shutil.copyfileobj(fsrc, fdst)
988
    finally:
989
      fdst.close()
990
  finally:
991
    fsrc.close()
992

    
993
  return backup_name
994

    
995

    
996
def ShellQuote(value):
997
  """Quotes shell argument according to POSIX.
998

999
  @type value: str
1000
  @param value: the argument to be quoted
1001
  @rtype: str
1002
  @return: the quoted value
1003

1004
  """
1005
  if _re_shell_unquoted.match(value):
1006
    return value
1007
  else:
1008
    return "'%s'" % value.replace("'", "'\\''")
1009

    
1010

    
1011
def ShellQuoteArgs(args):
1012
  """Quotes a list of shell arguments.
1013

1014
  @type args: list
1015
  @param args: list of arguments to be quoted
1016
  @rtype: str
1017
  @return: the quoted arguments concatenaned with spaces
1018

1019
  """
1020
  return ' '.join([ShellQuote(i) for i in args])
1021

    
1022

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

1026
  Check if the given IP is reachable by doing attempting a TCP connect
1027
  to it.
1028

1029
  @type target: str
1030
  @param target: the IP or hostname to ping
1031
  @type port: int
1032
  @param port: the port to connect to
1033
  @type timeout: int
1034
  @param timeout: the timeout on the connection attemp
1035
  @type live_port_needed: boolean
1036
  @param live_port_needed: whether a closed port will cause the
1037
      function to return failure, as if there was a timeout
1038
  @type source: str or None
1039
  @param source: if specified, will cause the connect to be made
1040
      from this specific source address; failures to bind other
1041
      than C{EADDRNOTAVAIL} will be ignored
1042

1043
  """
1044
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1045

    
1046
  success = False
1047

    
1048
  if source is not None:
1049
    try:
1050
      sock.bind((source, 0))
1051
    except socket.error, (errcode, errstring):
1052
      if errcode == errno.EADDRNOTAVAIL:
1053
        success = False
1054

    
1055
  sock.settimeout(timeout)
1056

    
1057
  try:
1058
    sock.connect((target, port))
1059
    sock.close()
1060
    success = True
1061
  except socket.timeout:
1062
    success = False
1063
  except socket.error, (errcode, errstring):
1064
    success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
1065

    
1066
  return success
1067

    
1068

    
1069
def OwnIpAddress(address):
1070
  """Check if the current host has the the given IP address.
1071

1072
  Currently this is done by TCP-pinging the address from the loopback
1073
  address.
1074

1075
  @type address: string
1076
  @param address: the addres to check
1077
  @rtype: bool
1078
  @return: True if we own the address
1079

1080
  """
1081
  return TcpPing(address, constants.DEFAULT_NODED_PORT,
1082
                 source=constants.LOCALHOST_IP_ADDRESS)
1083

    
1084

    
1085
def ListVisibleFiles(path):
1086
  """Returns a list of visible files in a directory.
1087

1088
  @type path: str
1089
  @param path: the directory to enumerate
1090
  @rtype: list
1091
  @return: the list of all files not starting with a dot
1092

1093
  """
1094
  files = [i for i in os.listdir(path) if not i.startswith(".")]
1095
  files.sort()
1096
  return files
1097

    
1098

    
1099
def GetHomeDir(user, default=None):
1100
  """Try to get the homedir of the given user.
1101

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

1106
  """
1107
  try:
1108
    if isinstance(user, basestring):
1109
      result = pwd.getpwnam(user)
1110
    elif isinstance(user, (int, long)):
1111
      result = pwd.getpwuid(user)
1112
    else:
1113
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
1114
                                   type(user))
1115
  except KeyError:
1116
    return default
1117
  return result.pw_dir
1118

    
1119

    
1120
def NewUUID():
1121
  """Returns a random UUID.
1122

1123
  @note: This is a Linux-specific method as it uses the /proc
1124
      filesystem.
1125
  @rtype: str
1126

1127
  """
1128
  f = open("/proc/sys/kernel/random/uuid", "r")
1129
  try:
1130
    return f.read(128).rstrip("\n")
1131
  finally:
1132
    f.close()
1133

    
1134

    
1135
def GenerateSecret():
1136
  """Generates a random secret.
1137

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

1141
  @rtype: str
1142
  @return: a sha1 hexdigest of a block of 64 random bytes
1143

1144
  """
1145
  return sha.new(os.urandom(64)).hexdigest()
1146

    
1147

    
1148
def ReadFile(file_name, size=None):
1149
  """Reads a file.
1150

1151
  @type size: None or int
1152
  @param size: Read at most size bytes
1153
  @rtype: str
1154
  @return: the (possibly partial) conent of the file
1155

1156
  """
1157
  f = open(file_name, "r")
1158
  try:
1159
    if size is None:
1160
      return f.read()
1161
    else:
1162
      return f.read(size)
1163
  finally:
1164
    f.close()
1165

    
1166

    
1167
def WriteFile(file_name, fn=None, data=None,
1168
              mode=None, uid=-1, gid=-1,
1169
              atime=None, mtime=None, close=True,
1170
              dry_run=False, backup=False,
1171
              prewrite=None, postwrite=None):
1172
  """(Over)write a file atomically.
1173

1174
  The file_name and either fn (a function taking one argument, the
1175
  file descriptor, and which should write the data to it) or data (the
1176
  contents of the file) must be passed. The other arguments are
1177
  optional and allow setting the file mode, owner and group, and the
1178
  mtime/atime of the file.
1179

1180
  If the function doesn't raise an exception, it has succeeded and the
1181
  target file has the new contents. If the file has raised an
1182
  exception, an existing target file should be unmodified and the
1183
  temporary file should be removed.
1184

1185
  @type file_name: str
1186
  @param file_name: the target filename
1187
  @type fn: callable
1188
  @param fn: content writing function, called with
1189
      file descriptor as parameter
1190
  @type data: sr
1191
  @param data: contents of the file
1192
  @type mode: int
1193
  @param mode: file mode
1194
  @type uid: int
1195
  @param uid: the owner of the file
1196
  @type gid: int
1197
  @param gid: the group of the file
1198
  @type atime: int
1199
  @param atime: a custom access time to be set on the file
1200
  @type mtime: int
1201
  @param mtime: a custom modification time to be set on the file
1202
  @type close: boolean
1203
  @param close: whether to close file after writing it
1204
  @type prewrite: callable
1205
  @param prewrite: function to be called before writing content
1206
  @type postwrite: callable
1207
  @param postwrite: function to be called after writing content
1208

1209
  @rtype: None or int
1210
  @return: None if the 'close' parameter evaluates to True,
1211
      otherwise the file descriptor
1212

1213
  @raise errors.ProgrammerError: if an of the arguments are not valid
1214

1215
  """
1216
  if not os.path.isabs(file_name):
1217
    raise errors.ProgrammerError("Path passed to WriteFile is not"
1218
                                 " absolute: '%s'" % file_name)
1219

    
1220
  if [fn, data].count(None) != 1:
1221
    raise errors.ProgrammerError("fn or data required")
1222

    
1223
  if [atime, mtime].count(None) == 1:
1224
    raise errors.ProgrammerError("Both atime and mtime must be either"
1225
                                 " set or None")
1226

    
1227
  if backup and not dry_run and os.path.isfile(file_name):
1228
    CreateBackup(file_name)
1229

    
1230
  dir_name, base_name = os.path.split(file_name)
1231
  fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1232
  # here we need to make sure we remove the temp file, if any error
1233
  # leaves it in place
1234
  try:
1235
    if uid != -1 or gid != -1:
1236
      os.chown(new_name, uid, gid)
1237
    if mode:
1238
      os.chmod(new_name, mode)
1239
    if callable(prewrite):
1240
      prewrite(fd)
1241
    if data is not None:
1242
      os.write(fd, data)
1243
    else:
1244
      fn(fd)
1245
    if callable(postwrite):
1246
      postwrite(fd)
1247
    os.fsync(fd)
1248
    if atime is not None and mtime is not None:
1249
      os.utime(new_name, (atime, mtime))
1250
    if not dry_run:
1251
      os.rename(new_name, file_name)
1252
  finally:
1253
    if close:
1254
      os.close(fd)
1255
      result = None
1256
    else:
1257
      result = fd
1258
    RemoveFile(new_name)
1259

    
1260
  return result
1261

    
1262

    
1263
def FirstFree(seq, base=0):
1264
  """Returns the first non-existing integer from seq.
1265

1266
  The seq argument should be a sorted list of positive integers. The
1267
  first time the index of an element is smaller than the element
1268
  value, the index will be returned.
1269

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

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

1275
  @type seq: sequence
1276
  @param seq: the sequence to be analyzed.
1277
  @type base: int
1278
  @param base: use this value as the base index of the sequence
1279
  @rtype: int
1280
  @return: the first non-used index in the sequence
1281

1282
  """
1283
  for idx, elem in enumerate(seq):
1284
    assert elem >= base, "Passed element is higher than base offset"
1285
    if elem > idx + base:
1286
      # idx is not used
1287
      return idx + base
1288
  return None
1289

    
1290

    
1291
def all(seq, pred=bool):
1292
  "Returns True if pred(x) is True for every element in the iterable"
1293
  for elem in itertools.ifilterfalse(pred, seq):
1294
    return False
1295
  return True
1296

    
1297

    
1298
def any(seq, pred=bool):
1299
  "Returns True if pred(x) is True for at least one element in the iterable"
1300
  for elem in itertools.ifilter(pred, seq):
1301
    return True
1302
  return False
1303

    
1304

    
1305
def UniqueSequence(seq):
1306
  """Returns a list with unique elements.
1307

1308
  Element order is preserved.
1309

1310
  @type seq: sequence
1311
  @param seq: the sequence with the source elementes
1312
  @rtype: list
1313
  @return: list of unique elements from seq
1314

1315
  """
1316
  seen = set()
1317
  return [i for i in seq if i not in seen and not seen.add(i)]
1318

    
1319

    
1320
def IsValidMac(mac):
1321
  """Predicate to check if a MAC address is valid.
1322

1323
  Checks wether the supplied MAC address is formally correct, only
1324
  accepts colon separated format.
1325

1326
  @type mac: str
1327
  @param mac: the MAC to be validated
1328
  @rtype: boolean
1329
  @return: True is the MAC seems valid
1330

1331
  """
1332
  mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
1333
  return mac_check.match(mac) is not None
1334

    
1335

    
1336
def TestDelay(duration):
1337
  """Sleep for a fixed amount of time.
1338

1339
  @type duration: float
1340
  @param duration: the sleep duration
1341
  @rtype: boolean
1342
  @return: False for negative value, True otherwise
1343

1344
  """
1345
  if duration < 0:
1346
    return False
1347
  time.sleep(duration)
1348
  return True
1349

    
1350

    
1351
def _CloseFDNoErr(fd, retries=5):
1352
  """Close a file descriptor ignoring errors.
1353

1354
  @type fd: int
1355
  @param fd: the file descriptor
1356
  @type retries: int
1357
  @param retries: how many retries to make, in case we get any
1358
      other error than EBADF
1359

1360
  """
1361
  try:
1362
    os.close(fd)
1363
  except OSError, err:
1364
    if err.errno != errno.EBADF:
1365
      if retries > 0:
1366
        _CloseFDNoErr(fd, retries - 1)
1367
    # else either it's closed already or we're out of retries, so we
1368
    # ignore this and go on
1369

    
1370

    
1371
def CloseFDs(noclose_fds=None):
1372
  """Close file descriptors.
1373

1374
  This closes all file descriptors above 2 (i.e. except
1375
  stdin/out/err).
1376

1377
  @type noclose_fds: list or None
1378
  @param noclose_fds: if given, it denotes a list of file descriptor
1379
      that should not be closed
1380

1381
  """
1382
  # Default maximum for the number of available file descriptors.
1383
  if 'SC_OPEN_MAX' in os.sysconf_names:
1384
    try:
1385
      MAXFD = os.sysconf('SC_OPEN_MAX')
1386
      if MAXFD < 0:
1387
        MAXFD = 1024
1388
    except OSError:
1389
      MAXFD = 1024
1390
  else:
1391
    MAXFD = 1024
1392
  maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1393
  if (maxfd == resource.RLIM_INFINITY):
1394
    maxfd = MAXFD
1395

    
1396
  # Iterate through and close all file descriptors (except the standard ones)
1397
  for fd in range(3, maxfd):
1398
    if noclose_fds and fd in noclose_fds:
1399
      continue
1400
    _CloseFDNoErr(fd)
1401

    
1402

    
1403
def Daemonize(logfile):
1404
  """Daemonize the current process.
1405

1406
  This detaches the current process from the controlling terminal and
1407
  runs it in the background as a daemon.
1408

1409
  @type logfile: str
1410
  @param logfile: the logfile to which we should redirect stdout/stderr
1411
  @rtype: int
1412
  @returns: the value zero
1413

1414
  """
1415
  UMASK = 077
1416
  WORKDIR = "/"
1417

    
1418
  # this might fail
1419
  pid = os.fork()
1420
  if (pid == 0):  # The first child.
1421
    os.setsid()
1422
    # this might fail
1423
    pid = os.fork() # Fork a second child.
1424
    if (pid == 0):  # The second child.
1425
      os.chdir(WORKDIR)
1426
      os.umask(UMASK)
1427
    else:
1428
      # exit() or _exit()?  See below.
1429
      os._exit(0) # Exit parent (the first child) of the second child.
1430
  else:
1431
    os._exit(0) # Exit parent of the first child.
1432

    
1433
  for fd in range(3):
1434
    _CloseFDNoErr(fd)
1435
  i = os.open("/dev/null", os.O_RDONLY) # stdin
1436
  assert i == 0, "Can't close/reopen stdin"
1437
  i = os.open(logfile, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0600) # stdout
1438
  assert i == 1, "Can't close/reopen stdout"
1439
  # Duplicate standard output to standard error.
1440
  os.dup2(1, 2)
1441
  return 0
1442

    
1443

    
1444
def DaemonPidFileName(name):
1445
  """Compute a ganeti pid file absolute path
1446

1447
  @type name: str
1448
  @param name: the daemon name
1449
  @rtype: str
1450
  @return: the full path to the pidfile corresponding to the given
1451
      daemon name
1452

1453
  """
1454
  return os.path.join(constants.RUN_GANETI_DIR, "%s.pid" % name)
1455

    
1456

    
1457
def WritePidFile(name):
1458
  """Write the current process pidfile.
1459

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

1462
  @type name: str
1463
  @param name: the daemon name to use
1464
  @raise errors.GenericError: if the pid file already exists and
1465
      points to a live process
1466

1467
  """
1468
  pid = os.getpid()
1469
  pidfilename = DaemonPidFileName(name)
1470
  if IsProcessAlive(ReadPidFile(pidfilename)):
1471
    raise errors.GenericError("%s contains a live process" % pidfilename)
1472

    
1473
  WriteFile(pidfilename, data="%d\n" % pid)
1474

    
1475

    
1476
def RemovePidFile(name):
1477
  """Remove the current process pidfile.
1478

1479
  Any errors are ignored.
1480

1481
  @type name: str
1482
  @param name: the daemon name used to derive the pidfile name
1483

1484
  """
1485
  pid = os.getpid()
1486
  pidfilename = DaemonPidFileName(name)
1487
  # TODO: we could check here that the file contains our pid
1488
  try:
1489
    RemoveFile(pidfilename)
1490
  except:
1491
    pass
1492

    
1493

    
1494
def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1495
                waitpid=False):
1496
  """Kill a process given by its pid.
1497

1498
  @type pid: int
1499
  @param pid: The PID to terminate.
1500
  @type signal_: int
1501
  @param signal_: The signal to send, by default SIGTERM
1502
  @type timeout: int
1503
  @param timeout: The timeout after which, if the process is still alive,
1504
                  a SIGKILL will be sent. If not positive, no such checking
1505
                  will be done
1506
  @type waitpid: boolean
1507
  @param waitpid: If true, we should waitpid on this process after
1508
      sending signals, since it's our own child and otherwise it
1509
      would remain as zombie
1510

1511
  """
1512
  def _helper(pid, signal_, wait):
1513
    """Simple helper to encapsulate the kill/waitpid sequence"""
1514
    os.kill(pid, signal_)
1515
    if wait:
1516
      try:
1517
        os.waitpid(pid, os.WNOHANG)
1518
      except OSError:
1519
        pass
1520

    
1521
  if pid <= 0:
1522
    # kill with pid=0 == suicide
1523
    raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1524

    
1525
  if not IsProcessAlive(pid):
1526
    return
1527
  _helper(pid, signal_, waitpid)
1528
  if timeout <= 0:
1529
    return
1530

    
1531
  # Wait up to $timeout seconds
1532
  end = time.time() + timeout
1533
  wait = 0.01
1534
  while time.time() < end and IsProcessAlive(pid):
1535
    try:
1536
      (result_pid, _) = os.waitpid(pid, os.WNOHANG)
1537
      if result_pid > 0:
1538
        break
1539
    except OSError:
1540
      pass
1541
    time.sleep(wait)
1542
    # Make wait time longer for next try
1543
    if wait < 0.1:
1544
      wait *= 1.5
1545

    
1546
  if IsProcessAlive(pid):
1547
    # Kill process if it's still alive
1548
    _helper(pid, signal.SIGKILL, waitpid)
1549

    
1550

    
1551
def FindFile(name, search_path, test=os.path.exists):
1552
  """Look for a filesystem object in a given path.
1553

1554
  This is an abstract method to search for filesystem object (files,
1555
  dirs) under a given search path.
1556

1557
  @type name: str
1558
  @param name: the name to look for
1559
  @type search_path: str
1560
  @param search_path: location to start at
1561
  @type test: callable
1562
  @param test: a function taking one argument that should return True
1563
      if the a given object is valid; the default value is
1564
      os.path.exists, causing only existing files to be returned
1565
  @rtype: str or None
1566
  @return: full path to the object if found, None otherwise
1567

1568
  """
1569
  for dir_name in search_path:
1570
    item_name = os.path.sep.join([dir_name, name])
1571
    if test(item_name):
1572
      return item_name
1573
  return None
1574

    
1575

    
1576
def CheckVolumeGroupSize(vglist, vgname, minsize):
1577
  """Checks if the volume group list is valid.
1578

1579
  The function will check if a given volume group is in the list of
1580
  volume groups and has a minimum size.
1581

1582
  @type vglist: dict
1583
  @param vglist: dictionary of volume group names and their size
1584
  @type vgname: str
1585
  @param vgname: the volume group we should check
1586
  @type minsize: int
1587
  @param minsize: the minimum size we accept
1588
  @rtype: None or str
1589
  @return: None for success, otherwise the error message
1590

1591
  """
1592
  vgsize = vglist.get(vgname, None)
1593
  if vgsize is None:
1594
    return "volume group '%s' missing" % vgname
1595
  elif vgsize < minsize:
1596
    return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1597
            (vgname, minsize, vgsize))
1598
  return None
1599

    
1600

    
1601
def SplitTime(value):
1602
  """Splits time as floating point number into a tuple.
1603

1604
  @param value: Time in seconds
1605
  @type value: int or float
1606
  @return: Tuple containing (seconds, microseconds)
1607

1608
  """
1609
  (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
1610

    
1611
  assert 0 <= seconds, \
1612
    "Seconds must be larger than or equal to 0, but are %s" % seconds
1613
  assert 0 <= microseconds <= 999999, \
1614
    "Microseconds must be 0-999999, but are %s" % microseconds
1615

    
1616
  return (int(seconds), int(microseconds))
1617

    
1618

    
1619
def MergeTime(timetuple):
1620
  """Merges a tuple into time as a floating point number.
1621

1622
  @param timetuple: Time as tuple, (seconds, microseconds)
1623
  @type timetuple: tuple
1624
  @return: Time as a floating point number expressed in seconds
1625

1626
  """
1627
  (seconds, microseconds) = timetuple
1628

    
1629
  assert 0 <= seconds, \
1630
    "Seconds must be larger than or equal to 0, but are %s" % seconds
1631
  assert 0 <= microseconds <= 999999, \
1632
    "Microseconds must be 0-999999, but are %s" % microseconds
1633

    
1634
  return float(seconds) + (float(microseconds) * 0.000001)
1635

    
1636

    
1637
def GetNodeDaemonPort():
1638
  """Get the node daemon port for this cluster.
1639

1640
  Note that this routine does not read a ganeti-specific file, but
1641
  instead uses C{socket.getservbyname} to allow pre-customization of
1642
  this parameter outside of Ganeti.
1643

1644
  @rtype: int
1645

1646
  """
1647
  try:
1648
    port = socket.getservbyname("ganeti-noded", "tcp")
1649
  except socket.error:
1650
    port = constants.DEFAULT_NODED_PORT
1651

    
1652
  return port
1653

    
1654

    
1655
def SetupLogging(logfile, debug=False, stderr_logging=False, program=""):
1656
  """Configures the logging module.
1657

1658
  @type logfile: str
1659
  @param logfile: the filename to which we should log
1660
  @type debug: boolean
1661
  @param debug: whether to enable debug messages too or
1662
      only those at C{INFO} and above level
1663
  @type stderr_logging: boolean
1664
  @param stderr_logging: whether we should also log to the standard error
1665
  @type program: str
1666
  @param program: the name under which we should log messages
1667
  @raise EnvironmentError: if we can't open the log file and
1668
      stderr logging is disabled
1669

1670
  """
1671
  fmt = "%(asctime)s: " + program + " "
1672
  if debug:
1673
    fmt += ("pid=%(process)d/%(threadName)s %(levelname)s"
1674
           " %(module)s:%(lineno)s %(message)s")
1675
  else:
1676
    fmt += "pid=%(process)d %(levelname)s %(message)s"
1677
  formatter = logging.Formatter(fmt)
1678

    
1679
  root_logger = logging.getLogger("")
1680
  root_logger.setLevel(logging.NOTSET)
1681

    
1682
  # Remove all previously setup handlers
1683
  for handler in root_logger.handlers:
1684
    handler.close()
1685
    root_logger.removeHandler(handler)
1686

    
1687
  if stderr_logging:
1688
    stderr_handler = logging.StreamHandler()
1689
    stderr_handler.setFormatter(formatter)
1690
    if debug:
1691
      stderr_handler.setLevel(logging.NOTSET)
1692
    else:
1693
      stderr_handler.setLevel(logging.CRITICAL)
1694
    root_logger.addHandler(stderr_handler)
1695

    
1696
  # this can fail, if the logging directories are not setup or we have
1697
  # a permisssion problem; in this case, it's best to log but ignore
1698
  # the error if stderr_logging is True, and if false we re-raise the
1699
  # exception since otherwise we could run but without any logs at all
1700
  try:
1701
    logfile_handler = logging.FileHandler(logfile)
1702
    logfile_handler.setFormatter(formatter)
1703
    if debug:
1704
      logfile_handler.setLevel(logging.DEBUG)
1705
    else:
1706
      logfile_handler.setLevel(logging.INFO)
1707
    root_logger.addHandler(logfile_handler)
1708
  except EnvironmentError, err:
1709
    if stderr_logging:
1710
      logging.exception("Failed to enable logging to file '%s'", logfile)
1711
    else:
1712
      # we need to re-raise the exception
1713
      raise
1714

    
1715

    
1716
def TailFile(fname, lines=20):
1717
  """Return the last lines from a file.
1718

1719
  @note: this function will only read and parse the last 4KB of
1720
      the file; if the lines are very long, it could be that less
1721
      than the requested number of lines are returned
1722

1723
  @param fname: the file name
1724
  @type lines: int
1725
  @param lines: the (maximum) number of lines to return
1726

1727
  """
1728
  fd = open(fname, "r")
1729
  try:
1730
    fd.seek(0, 2)
1731
    pos = fd.tell()
1732
    pos = max(0, pos-4096)
1733
    fd.seek(pos, 0)
1734
    raw_data = fd.read()
1735
  finally:
1736
    fd.close()
1737

    
1738
  rows = raw_data.splitlines()
1739
  return rows[-lines:]
1740

    
1741

    
1742
def LockedMethod(fn):
1743
  """Synchronized object access decorator.
1744

1745
  This decorator is intended to protect access to an object using the
1746
  object's own lock which is hardcoded to '_lock'.
1747

1748
  """
1749
  def _LockDebug(*args, **kwargs):
1750
    if debug_locks:
1751
      logging.debug(*args, **kwargs)
1752

    
1753
  def wrapper(self, *args, **kwargs):
1754
    assert hasattr(self, '_lock')
1755
    lock = self._lock
1756
    _LockDebug("Waiting for %s", lock)
1757
    lock.acquire()
1758
    try:
1759
      _LockDebug("Acquired %s", lock)
1760
      result = fn(self, *args, **kwargs)
1761
    finally:
1762
      _LockDebug("Releasing %s", lock)
1763
      lock.release()
1764
      _LockDebug("Released %s", lock)
1765
    return result
1766
  return wrapper
1767

    
1768

    
1769
def LockFile(fd):
1770
  """Locks a file using POSIX locks.
1771

1772
  @type fd: int
1773
  @param fd: the file descriptor we need to lock
1774

1775
  """
1776
  try:
1777
    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1778
  except IOError, err:
1779
    if err.errno == errno.EAGAIN:
1780
      raise errors.LockError("File already locked")
1781
    raise
1782

    
1783

    
1784
class FileLock(object):
1785
  """Utility class for file locks.
1786

1787
  """
1788
  def __init__(self, filename):
1789
    """Constructor for FileLock.
1790

1791
    This will open the file denoted by the I{filename} argument.
1792

1793
    @type filename: str
1794
    @param filename: path to the file to be locked
1795

1796
    """
1797
    self.filename = filename
1798
    self.fd = open(self.filename, "w")
1799

    
1800
  def __del__(self):
1801
    self.Close()
1802

    
1803
  def Close(self):
1804
    """Close the file and release the lock.
1805

1806
    """
1807
    if self.fd:
1808
      self.fd.close()
1809
      self.fd = None
1810

    
1811
  def _flock(self, flag, blocking, timeout, errmsg):
1812
    """Wrapper for fcntl.flock.
1813

1814
    @type flag: int
1815
    @param flag: operation flag
1816
    @type blocking: bool
1817
    @param blocking: whether the operation should be done in blocking mode.
1818
    @type timeout: None or float
1819
    @param timeout: for how long the operation should be retried (implies
1820
                    non-blocking mode).
1821
    @type errmsg: string
1822
    @param errmsg: error message in case operation fails.
1823

1824
    """
1825
    assert self.fd, "Lock was closed"
1826
    assert timeout is None or timeout >= 0, \
1827
      "If specified, timeout must be positive"
1828

    
1829
    if timeout is not None:
1830
      flag |= fcntl.LOCK_NB
1831
      timeout_end = time.time() + timeout
1832

    
1833
    # Blocking doesn't have effect with timeout
1834
    elif not blocking:
1835
      flag |= fcntl.LOCK_NB
1836
      timeout_end = None
1837

    
1838
    retry = True
1839
    while retry:
1840
      try:
1841
        fcntl.flock(self.fd, flag)
1842
        retry = False
1843
      except IOError, err:
1844
        if err.errno in (errno.EAGAIN, ):
1845
          if timeout_end is not None and time.time() < timeout_end:
1846
            # Wait before trying again
1847
            time.sleep(max(0.1, min(1.0, timeout)))
1848
          else:
1849
            raise errors.LockError(errmsg)
1850
        else:
1851
          logging.exception("fcntl.flock failed")
1852
          raise
1853

    
1854
  def Exclusive(self, blocking=False, timeout=None):
1855
    """Locks the file in exclusive mode.
1856

1857
    @type blocking: boolean
1858
    @param blocking: whether to block and wait until we
1859
        can lock the file or return immediately
1860
    @type timeout: int or None
1861
    @param timeout: if not None, the duration to wait for the lock
1862
        (in blocking mode)
1863

1864
    """
1865
    self._flock(fcntl.LOCK_EX, blocking, timeout,
1866
                "Failed to lock %s in exclusive mode" % self.filename)
1867

    
1868
  def Shared(self, blocking=False, timeout=None):
1869
    """Locks the file in shared mode.
1870

1871
    @type blocking: boolean
1872
    @param blocking: whether to block and wait until we
1873
        can lock the file or return immediately
1874
    @type timeout: int or None
1875
    @param timeout: if not None, the duration to wait for the lock
1876
        (in blocking mode)
1877

1878
    """
1879
    self._flock(fcntl.LOCK_SH, blocking, timeout,
1880
                "Failed to lock %s in shared mode" % self.filename)
1881

    
1882
  def Unlock(self, blocking=True, timeout=None):
1883
    """Unlocks the file.
1884

1885
    According to C{flock(2)}, unlocking can also be a nonblocking
1886
    operation::
1887

1888
      To make a non-blocking request, include LOCK_NB with any of the above
1889
      operations.
1890

1891
    @type blocking: boolean
1892
    @param blocking: whether to block and wait until we
1893
        can lock the file or return immediately
1894
    @type timeout: int or None
1895
    @param timeout: if not None, the duration to wait for the lock
1896
        (in blocking mode)
1897

1898
    """
1899
    self._flock(fcntl.LOCK_UN, blocking, timeout,
1900
                "Failed to unlock %s" % self.filename)
1901

    
1902

    
1903
class SignalHandler(object):
1904
  """Generic signal handler class.
1905

1906
  It automatically restores the original handler when deconstructed or
1907
  when L{Reset} is called. You can either pass your own handler
1908
  function in or query the L{called} attribute to detect whether the
1909
  signal was sent.
1910

1911
  @type signum: list
1912
  @ivar signum: the signals we handle
1913
  @type called: boolean
1914
  @ivar called: tracks whether any of the signals have been raised
1915

1916
  """
1917
  def __init__(self, signum):
1918
    """Constructs a new SignalHandler instance.
1919

1920
    @type signum: int or list of ints
1921
    @param signum: Single signal number or set of signal numbers
1922

1923
    """
1924
    if isinstance(signum, (int, long)):
1925
      self.signum = set([signum])
1926
    else:
1927
      self.signum = set(signum)
1928

    
1929
    self.called = False
1930

    
1931
    self._previous = {}
1932
    try:
1933
      for signum in self.signum:
1934
        # Setup handler
1935
        prev_handler = signal.signal(signum, self._HandleSignal)
1936
        try:
1937
          self._previous[signum] = prev_handler
1938
        except:
1939
          # Restore previous handler
1940
          signal.signal(signum, prev_handler)
1941
          raise
1942
    except:
1943
      # Reset all handlers
1944
      self.Reset()
1945
      # Here we have a race condition: a handler may have already been called,
1946
      # but there's not much we can do about it at this point.
1947
      raise
1948

    
1949
  def __del__(self):
1950
    self.Reset()
1951

    
1952
  def Reset(self):
1953
    """Restore previous handler.
1954

1955
    This will reset all the signals to their previous handlers.
1956

1957
    """
1958
    for signum, prev_handler in self._previous.items():
1959
      signal.signal(signum, prev_handler)
1960
      # If successful, remove from dict
1961
      del self._previous[signum]
1962

    
1963
  def Clear(self):
1964
    """Unsets the L{called} flag.
1965

1966
    This function can be used in case a signal may arrive several times.
1967

1968
    """
1969
    self.called = False
1970

    
1971
  def _HandleSignal(self, signum, frame):
1972
    """Actual signal handling function.
1973

1974
    """
1975
    # This is not nice and not absolutely atomic, but it appears to be the only
1976
    # solution in Python -- there are no atomic types.
1977
    self.called = True
1978

    
1979

    
1980
class FieldSet(object):
1981
  """A simple field set.
1982

1983
  Among the features are:
1984
    - checking if a string is among a list of static string or regex objects
1985
    - checking if a whole list of string matches
1986
    - returning the matching groups from a regex match
1987

1988
  Internally, all fields are held as regular expression objects.
1989

1990
  """
1991
  def __init__(self, *items):
1992
    self.items = [re.compile("^%s$" % value) for value in items]
1993

    
1994
  def Extend(self, other_set):
1995
    """Extend the field set with the items from another one"""
1996
    self.items.extend(other_set.items)
1997

    
1998
  def Matches(self, field):
1999
    """Checks if a field matches the current set
2000

2001
    @type field: str
2002
    @param field: the string to match
2003
    @return: either False or a regular expression match object
2004

2005
    """
2006
    for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
2007
      return m
2008
    return False
2009

    
2010
  def NonMatching(self, items):
2011
    """Returns the list of fields not matching the current set
2012

2013
    @type items: list
2014
    @param items: the list of fields to check
2015
    @rtype: list
2016
    @return: list of non-matching fields
2017

2018
    """
2019
    return [val for val in items if not self.Matches(val)]