Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ c803b052

History | View | Annotate | Download (55.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
  try:
155
    if output is None:
156
      out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd)
157
    else:
158
      status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
159
      out = err = ""
160
  except OSError, err:
161
    if err.errno == errno.ENOENT:
162
      raise errors.OpExecError("Can't execute '%s': not found (%s)" %
163
                               (strcmd, err))
164
    else:
165
      raise
166

    
167
  if status >= 0:
168
    exitcode = status
169
    signal_ = None
170
  else:
171
    exitcode = None
172
    signal_ = -status
173

    
174
  return RunResult(exitcode, signal_, out, err, strcmd)
175

    
176

    
177
def _RunCmdPipe(cmd, env, via_shell, cwd):
178
  """Run a command and return its output.
179

180
  @type  cmd: string or list
181
  @param cmd: Command to run
182
  @type env: dict
183
  @param env: The environment to use
184
  @type via_shell: bool
185
  @param via_shell: if we should run via the shell
186
  @type cwd: string
187
  @param cwd: the working directory for the program
188
  @rtype: tuple
189
  @return: (out, err, status)
190

191
  """
192
  poller = select.poll()
193
  child = subprocess.Popen(cmd, shell=via_shell,
194
                           stderr=subprocess.PIPE,
195
                           stdout=subprocess.PIPE,
196
                           stdin=subprocess.PIPE,
197
                           close_fds=True, env=env,
198
                           cwd=cwd)
199

    
200
  child.stdin.close()
201
  poller.register(child.stdout, select.POLLIN)
202
  poller.register(child.stderr, select.POLLIN)
203
  out = StringIO()
204
  err = StringIO()
205
  fdmap = {
206
    child.stdout.fileno(): (out, child.stdout),
207
    child.stderr.fileno(): (err, child.stderr),
208
    }
209
  for fd in fdmap:
210
    status = fcntl.fcntl(fd, fcntl.F_GETFL)
211
    fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
212

    
213
  while fdmap:
214
    try:
215
      pollresult = poller.poll()
216
    except EnvironmentError, eerr:
217
      if eerr.errno == errno.EINTR:
218
        continue
219
      raise
220
    except select.error, serr:
221
      if serr[0] == errno.EINTR:
222
        continue
223
      raise
224

    
225
    for fd, event in pollresult:
226
      if event & select.POLLIN or event & select.POLLPRI:
227
        data = fdmap[fd][1].read()
228
        # no data from read signifies EOF (the same as POLLHUP)
229
        if not data:
230
          poller.unregister(fd)
231
          del fdmap[fd]
232
          continue
233
        fdmap[fd][0].write(data)
234
      if (event & select.POLLNVAL or event & select.POLLHUP or
235
          event & select.POLLERR):
236
        poller.unregister(fd)
237
        del fdmap[fd]
238

    
239
  out = out.getvalue()
240
  err = err.getvalue()
241

    
242
  status = child.wait()
243
  return out, err, status
244

    
245

    
246
def _RunCmdFile(cmd, env, via_shell, output, cwd):
247
  """Run a command and save its output to a file.
248

249
  @type  cmd: string or list
250
  @param cmd: Command to run
251
  @type env: dict
252
  @param env: The environment to use
253
  @type via_shell: bool
254
  @param via_shell: if we should run via the shell
255
  @type output: str
256
  @param output: the filename in which to save the output
257
  @type cwd: string
258
  @param cwd: the working directory for the program
259
  @rtype: int
260
  @return: the exit status
261

262
  """
263
  fh = open(output, "a")
264
  try:
265
    child = subprocess.Popen(cmd, shell=via_shell,
266
                             stderr=subprocess.STDOUT,
267
                             stdout=fh,
268
                             stdin=subprocess.PIPE,
269
                             close_fds=True, env=env,
270
                             cwd=cwd)
271

    
272
    child.stdin.close()
273
    status = child.wait()
274
  finally:
275
    fh.close()
276
  return status
277

    
278

    
279
def RemoveFile(filename):
280
  """Remove a file ignoring some errors.
281

282
  Remove a file, ignoring non-existing ones or directories. Other
283
  errors are passed.
284

285
  @type filename: str
286
  @param filename: the file to be removed
287

288
  """
289
  try:
290
    os.unlink(filename)
291
  except OSError, err:
292
    if err.errno not in (errno.ENOENT, errno.EISDIR):
293
      raise
294

    
295

    
296
def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
297
  """Renames a file.
298

299
  @type old: string
300
  @param old: Original path
301
  @type new: string
302
  @param new: New path
303
  @type mkdir: bool
304
  @param mkdir: Whether to create target directory if it doesn't exist
305
  @type mkdir_mode: int
306
  @param mkdir_mode: Mode for newly created directories
307

308
  """
309
  try:
310
    return os.rename(old, new)
311
  except OSError, err:
312
    # In at least one use case of this function, the job queue, directory
313
    # creation is very rare. Checking for the directory before renaming is not
314
    # as efficient.
315
    if mkdir and err.errno == errno.ENOENT:
316
      # Create directory and try again
317
      os.makedirs(os.path.dirname(new), mkdir_mode)
318
      return os.rename(old, new)
319
    raise
320

    
321

    
322
def _FingerprintFile(filename):
323
  """Compute the fingerprint of a file.
324

325
  If the file does not exist, a None will be returned
326
  instead.
327

328
  @type filename: str
329
  @param filename: the filename to checksum
330
  @rtype: str
331
  @return: the hex digest of the sha checksum of the contents
332
      of the file
333

334
  """
335
  if not (os.path.exists(filename) and os.path.isfile(filename)):
336
    return None
337

    
338
  f = open(filename)
339

    
340
  fp = sha.sha()
341
  while True:
342
    data = f.read(4096)
343
    if not data:
344
      break
345

    
346
    fp.update(data)
347

    
348
  return fp.hexdigest()
349

    
350

    
351
def FingerprintFiles(files):
352
  """Compute fingerprints for a list of files.
353

354
  @type files: list
355
  @param files: the list of filename to fingerprint
356
  @rtype: dict
357
  @return: a dictionary filename: fingerprint, holding only
358
      existing files
359

360
  """
361
  ret = {}
362

    
363
  for filename in files:
364
    cksum = _FingerprintFile(filename)
365
    if cksum:
366
      ret[filename] = cksum
367

    
368
  return ret
369

    
370

    
371
def CheckDict(target, template, logname=None):
372
  """Ensure a dictionary has a required set of keys.
373

374
  For the given dictionaries I{target} and I{template}, ensure
375
  I{target} has all the keys from I{template}. Missing keys are added
376
  with values from template.
377

378
  @type target: dict
379
  @param target: the dictionary to update
380
  @type template: dict
381
  @param template: the dictionary holding the default values
382
  @type logname: str or None
383
  @param logname: if not None, causes the missing keys to be
384
      logged with this name
385

386
  """
387
  missing = []
388
  for k in template:
389
    if k not in target:
390
      missing.append(k)
391
      target[k] = template[k]
392

    
393
  if missing and logname:
394
    logging.warning('%s missing keys %s', logname, ', '.join(missing))
395

    
396

    
397
def ForceDictType(target, key_types, allowed_values=None):
398
  """Force the values of a dict to have certain types.
399

400
  @type target: dict
401
  @param target: the dict to update
402
  @type key_types: dict
403
  @param key_types: dict mapping target dict keys to types
404
                    in constants.ENFORCEABLE_TYPES
405
  @type allowed_values: list
406
  @keyword allowed_values: list of specially allowed values
407

408
  """
409
  if allowed_values is None:
410
    allowed_values = []
411

    
412
  for key in target:
413
    if key not in key_types:
414
      msg = "Unknown key '%s'" % key
415
      raise errors.TypeEnforcementError(msg)
416

    
417
    if target[key] in allowed_values:
418
      continue
419

    
420
    type = key_types[key]
421
    if type not in constants.ENFORCEABLE_TYPES:
422
      msg = "'%s' has non-enforceable type %s" % (key, type)
423
      raise errors.ProgrammerError(msg)
424

    
425
    if type == constants.VTYPE_STRING:
426
      if not isinstance(target[key], basestring):
427
        if isinstance(target[key], bool) and not target[key]:
428
          target[key] = ''
429
        else:
430
          msg = "'%s' (value %s) is not a valid string" % (key, target[key])
431
          raise errors.TypeEnforcementError(msg)
432
    elif type == constants.VTYPE_BOOL:
433
      if isinstance(target[key], basestring) and target[key]:
434
        if target[key].lower() == constants.VALUE_FALSE:
435
          target[key] = False
436
        elif target[key].lower() == constants.VALUE_TRUE:
437
          target[key] = True
438
        else:
439
          msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
440
          raise errors.TypeEnforcementError(msg)
441
      elif target[key]:
442
        target[key] = True
443
      else:
444
        target[key] = False
445
    elif type == constants.VTYPE_SIZE:
446
      try:
447
        target[key] = ParseUnit(target[key])
448
      except errors.UnitParseError, err:
449
        msg = "'%s' (value %s) is not a valid size. error: %s" % \
450
              (key, target[key], err)
451
        raise errors.TypeEnforcementError(msg)
452
    elif type == constants.VTYPE_INT:
453
      try:
454
        target[key] = int(target[key])
455
      except (ValueError, TypeError):
456
        msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
457
        raise errors.TypeEnforcementError(msg)
458

    
459

    
460
def IsProcessAlive(pid):
461
  """Check if a given pid exists on the system.
462

463
  @note: zombie status is not handled, so zombie processes
464
      will be returned as alive
465
  @type pid: int
466
  @param pid: the process ID to check
467
  @rtype: boolean
468
  @return: True if the process exists
469

470
  """
471
  if pid <= 0:
472
    return False
473

    
474
  try:
475
    os.stat("/proc/%d/status" % pid)
476
    return True
477
  except EnvironmentError, err:
478
    if err.errno in (errno.ENOENT, errno.ENOTDIR):
479
      return False
480
    raise
481

    
482

    
483
def ReadPidFile(pidfile):
484
  """Read a pid from a file.
485

486
  @type  pidfile: string
487
  @param pidfile: path to the file containing the pid
488
  @rtype: int
489
  @return: The process id, if the file exists and contains a valid PID,
490
           otherwise 0
491

492
  """
493
  try:
494
    pf = open(pidfile, 'r')
495
  except EnvironmentError, err:
496
    if err.errno != errno.ENOENT:
497
      logging.exception("Can't read pid file?!")
498
    return 0
499

    
500
  try:
501
    pid = int(pf.read())
502
  except ValueError, err:
503
    logging.info("Can't parse pid file contents", exc_info=True)
504
    return 0
505

    
506
  return pid
507

    
508

    
509
def MatchNameComponent(key, name_list):
510
  """Try to match a name against a list.
511

512
  This function will try to match a name like test1 against a list
513
  like C{['test1.example.com', 'test2.example.com', ...]}. Against
514
  this list, I{'test1'} as well as I{'test1.example'} will match, but
515
  not I{'test1.ex'}. A multiple match will be considered as no match
516
  at all (e.g. I{'test1'} against C{['test1.example.com',
517
  'test1.example.org']}).
518

519
  @type key: str
520
  @param key: the name to be searched
521
  @type name_list: list
522
  @param name_list: the list of strings against which to search the key
523

524
  @rtype: None or str
525
  @return: None if there is no match I{or} if there are multiple matches,
526
      otherwise the element from the list which matches
527

528
  """
529
  mo = re.compile("^%s(\..*)?$" % re.escape(key))
530
  names_filtered = [name for name in name_list if mo.match(name) is not None]
531
  if len(names_filtered) != 1:
532
    return None
533
  return names_filtered[0]
534

    
535

    
536
class HostInfo:
537
  """Class implementing resolver and hostname functionality
538

539
  """
540
  def __init__(self, name=None):
541
    """Initialize the host name object.
542

543
    If the name argument is not passed, it will use this system's
544
    name.
545

546
    """
547
    if name is None:
548
      name = self.SysName()
549

    
550
    self.query = name
551
    self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
552
    self.ip = self.ipaddrs[0]
553

    
554
  def ShortName(self):
555
    """Returns the hostname without domain.
556

557
    """
558
    return self.name.split('.')[0]
559

    
560
  @staticmethod
561
  def SysName():
562
    """Return the current system's name.
563

564
    This is simply a wrapper over C{socket.gethostname()}.
565

566
    """
567
    return socket.gethostname()
568

    
569
  @staticmethod
570
  def LookupHostname(hostname):
571
    """Look up hostname
572

573
    @type hostname: str
574
    @param hostname: hostname to look up
575

576
    @rtype: tuple
577
    @return: a tuple (name, aliases, ipaddrs) as returned by
578
        C{socket.gethostbyname_ex}
579
    @raise errors.ResolverError: in case of errors in resolving
580

581
    """
582
    try:
583
      result = socket.gethostbyname_ex(hostname)
584
    except socket.gaierror, err:
585
      # hostname not found in DNS
586
      raise errors.ResolverError(hostname, err.args[0], err.args[1])
587

    
588
    return result
589

    
590

    
591
def ListVolumeGroups():
592
  """List volume groups and their size
593

594
  @rtype: dict
595
  @return:
596
       Dictionary with keys volume name and values
597
       the size of the volume
598

599
  """
600
  command = "vgs --noheadings --units m --nosuffix -o name,size"
601
  result = RunCmd(command)
602
  retval = {}
603
  if result.failed:
604
    return retval
605

    
606
  for line in result.stdout.splitlines():
607
    try:
608
      name, size = line.split()
609
      size = int(float(size))
610
    except (IndexError, ValueError), err:
611
      logging.error("Invalid output from vgs (%s): %s", err, line)
612
      continue
613

    
614
    retval[name] = size
615

    
616
  return retval
617

    
618

    
619
def BridgeExists(bridge):
620
  """Check whether the given bridge exists in the system
621

622
  @type bridge: str
623
  @param bridge: the bridge name to check
624
  @rtype: boolean
625
  @return: True if it does
626

627
  """
628
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
629

    
630

    
631
def NiceSort(name_list):
632
  """Sort a list of strings based on digit and non-digit groupings.
633

634
  Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
635
  will sort the list in the logical order C{['a1', 'a2', 'a10',
636
  'a11']}.
637

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

642
  @type name_list: list
643
  @param name_list: the names to be sorted
644
  @rtype: list
645
  @return: a copy of the name list sorted with our algorithm
646

647
  """
648
  _SORTER_BASE = "(\D+|\d+)"
649
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
650
                                                  _SORTER_BASE, _SORTER_BASE,
651
                                                  _SORTER_BASE, _SORTER_BASE,
652
                                                  _SORTER_BASE, _SORTER_BASE)
653
  _SORTER_RE = re.compile(_SORTER_FULL)
654
  _SORTER_NODIGIT = re.compile("^\D*$")
655
  def _TryInt(val):
656
    """Attempts to convert a variable to integer."""
657
    if val is None or _SORTER_NODIGIT.match(val):
658
      return val
659
    rval = int(val)
660
    return rval
661

    
662
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
663
             for name in name_list]
664
  to_sort.sort()
665
  return [tup[1] for tup in to_sort]
666

    
667

    
668
def TryConvert(fn, val):
669
  """Try to convert a value ignoring errors.
670

671
  This function tries to apply function I{fn} to I{val}. If no
672
  C{ValueError} or C{TypeError} exceptions are raised, it will return
673
  the result, else it will return the original value. Any other
674
  exceptions are propagated to the caller.
675

676
  @type fn: callable
677
  @param fn: function to apply to the value
678
  @param val: the value to be converted
679
  @return: The converted value if the conversion was successful,
680
      otherwise the original value.
681

682
  """
683
  try:
684
    nv = fn(val)
685
  except (ValueError, TypeError), err:
686
    nv = val
687
  return nv
688

    
689

    
690
def IsValidIP(ip):
691
  """Verifies the syntax of an IPv4 address.
692

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

696
  @type ip: str
697
  @param ip: the address to be checked
698
  @rtype: a regular expression match object
699
  @return: a regular epression match object, or None if the
700
      address is not valid
701

702
  """
703
  unit = "(0|[1-9]\d{0,2})"
704
  #TODO: convert and return only boolean
705
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
706

    
707

    
708
def IsValidShellParam(word):
709
  """Verifies is the given word is safe from the shell's p.o.v.
710

711
  This means that we can pass this to a command via the shell and be
712
  sure that it doesn't alter the command line and is passed as such to
713
  the actual command.
714

715
  Note that we are overly restrictive here, in order to be on the safe
716
  side.
717

718
  @type word: str
719
  @param word: the word to check
720
  @rtype: boolean
721
  @return: True if the word is 'safe'
722

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

    
726

    
727
def BuildShellCmd(template, *args):
728
  """Build a safe shell command line from the given arguments.
729

730
  This function will check all arguments in the args list so that they
731
  are valid shell parameters (i.e. they don't contain shell
732
  metacharaters). If everything is ok, it will return the result of
733
  template % args.
734

735
  @type template: str
736
  @param template: the string holding the template for the
737
      string formatting
738
  @rtype: str
739
  @return: the expanded command line
740

741
  """
742
  for word in args:
743
    if not IsValidShellParam(word):
744
      raise errors.ProgrammerError("Shell argument '%s' contains"
745
                                   " invalid characters" % word)
746
  return template % args
747

    
748

    
749
def FormatUnit(value, units):
750
  """Formats an incoming number of MiB with the appropriate unit.
751

752
  @type value: int
753
  @param value: integer representing the value in MiB (1048576)
754
  @type units: char
755
  @param units: the type of formatting we should do:
756
      - 'h' for automatic scaling
757
      - 'm' for MiBs
758
      - 'g' for GiBs
759
      - 't' for TiBs
760
  @rtype: str
761
  @return: the formatted value (with suffix)
762

763
  """
764
  if units not in ('m', 'g', 't', 'h'):
765
    raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
766

    
767
  suffix = ''
768

    
769
  if units == 'm' or (units == 'h' and value < 1024):
770
    if units == 'h':
771
      suffix = 'M'
772
    return "%d%s" % (round(value, 0), suffix)
773

    
774
  elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
775
    if units == 'h':
776
      suffix = 'G'
777
    return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
778

    
779
  else:
780
    if units == 'h':
781
      suffix = 'T'
782
    return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
783

    
784

    
785
def ParseUnit(input_string):
786
  """Tries to extract number and scale from the given string.
787

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

792
  """
793
  m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', str(input_string))
794
  if not m:
795
    raise errors.UnitParseError("Invalid format")
796

    
797
  value = float(m.groups()[0])
798

    
799
  unit = m.groups()[1]
800
  if unit:
801
    lcunit = unit.lower()
802
  else:
803
    lcunit = 'm'
804

    
805
  if lcunit in ('m', 'mb', 'mib'):
806
    # Value already in MiB
807
    pass
808

    
809
  elif lcunit in ('g', 'gb', 'gib'):
810
    value *= 1024
811

    
812
  elif lcunit in ('t', 'tb', 'tib'):
813
    value *= 1024 * 1024
814

    
815
  else:
816
    raise errors.UnitParseError("Unknown unit: %s" % unit)
817

    
818
  # Make sure we round up
819
  if int(value) < value:
820
    value += 1
821

    
822
  # Round up to the next multiple of 4
823
  value = int(value)
824
  if value % 4:
825
    value += 4 - value % 4
826

    
827
  return value
828

    
829

    
830
def AddAuthorizedKey(file_name, key):
831
  """Adds an SSH public key to an authorized_keys file.
832

833
  @type file_name: str
834
  @param file_name: path to authorized_keys file
835
  @type key: str
836
  @param key: string containing key
837

838
  """
839
  key_fields = key.split()
840

    
841
  f = open(file_name, 'a+')
842
  try:
843
    nl = True
844
    for line in f:
845
      # Ignore whitespace changes
846
      if line.split() == key_fields:
847
        break
848
      nl = line.endswith('\n')
849
    else:
850
      if not nl:
851
        f.write("\n")
852
      f.write(key.rstrip('\r\n'))
853
      f.write("\n")
854
      f.flush()
855
  finally:
856
    f.close()
857

    
858

    
859
def RemoveAuthorizedKey(file_name, key):
860
  """Removes an SSH public key from an authorized_keys file.
861

862
  @type file_name: str
863
  @param file_name: path to authorized_keys file
864
  @type key: str
865
  @param key: string containing key
866

867
  """
868
  key_fields = key.split()
869

    
870
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
871
  try:
872
    out = os.fdopen(fd, 'w')
873
    try:
874
      f = open(file_name, 'r')
875
      try:
876
        for line in f:
877
          # Ignore whitespace changes while comparing lines
878
          if line.split() != key_fields:
879
            out.write(line)
880

    
881
        out.flush()
882
        os.rename(tmpname, file_name)
883
      finally:
884
        f.close()
885
    finally:
886
      out.close()
887
  except:
888
    RemoveFile(tmpname)
889
    raise
890

    
891

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

895
  @type file_name: str
896
  @param file_name: path to the file to modify (usually C{/etc/hosts})
897
  @type ip: str
898
  @param ip: the IP address
899
  @type hostname: str
900
  @param hostname: the hostname to be added
901
  @type aliases: list
902
  @param aliases: the list of aliases to add for the hostname
903

904
  """
905
  # FIXME: use WriteFile + fn rather than duplicating its efforts
906
  # Ensure aliases are unique
907
  aliases = UniqueSequence([hostname] + aliases)[1:]
908

    
909
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
910
  try:
911
    out = os.fdopen(fd, 'w')
912
    try:
913
      f = open(file_name, 'r')
914
      try:
915
        for line in f:
916
          fields = line.split()
917
          if fields and not fields[0].startswith('#') and ip == fields[0]:
918
            continue
919
          out.write(line)
920

    
921
        out.write("%s\t%s" % (ip, hostname))
922
        if aliases:
923
          out.write(" %s" % ' '.join(aliases))
924
        out.write('\n')
925

    
926
        out.flush()
927
        os.fsync(out)
928
        os.chmod(tmpname, 0644)
929
        os.rename(tmpname, file_name)
930
      finally:
931
        f.close()
932
    finally:
933
      out.close()
934
  except:
935
    RemoveFile(tmpname)
936
    raise
937

    
938

    
939
def AddHostToEtcHosts(hostname):
940
  """Wrapper around SetEtcHostsEntry.
941

942
  @type hostname: str
943
  @param hostname: a hostname that will be resolved and added to
944
      L{constants.ETC_HOSTS}
945

946
  """
947
  hi = HostInfo(name=hostname)
948
  SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
949

    
950

    
951
def RemoveEtcHostsEntry(file_name, hostname):
952
  """Removes a hostname from /etc/hosts.
953

954
  IP addresses without names are removed from the file.
955

956
  @type file_name: str
957
  @param file_name: path to the file to modify (usually C{/etc/hosts})
958
  @type hostname: str
959
  @param hostname: the hostname to be removed
960

961
  """
962
  # FIXME: use WriteFile + fn rather than duplicating its efforts
963
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
964
  try:
965
    out = os.fdopen(fd, 'w')
966
    try:
967
      f = open(file_name, 'r')
968
      try:
969
        for line in f:
970
          fields = line.split()
971
          if len(fields) > 1 and not fields[0].startswith('#'):
972
            names = fields[1:]
973
            if hostname in names:
974
              while hostname in names:
975
                names.remove(hostname)
976
              if names:
977
                out.write("%s %s\n" % (fields[0], ' '.join(names)))
978
              continue
979

    
980
          out.write(line)
981

    
982
        out.flush()
983
        os.fsync(out)
984
        os.chmod(tmpname, 0644)
985
        os.rename(tmpname, file_name)
986
      finally:
987
        f.close()
988
    finally:
989
      out.close()
990
  except:
991
    RemoveFile(tmpname)
992
    raise
993

    
994

    
995
def RemoveHostFromEtcHosts(hostname):
996
  """Wrapper around RemoveEtcHostsEntry.
997

998
  @type hostname: str
999
  @param hostname: hostname that will be resolved and its
1000
      full and shot name will be removed from
1001
      L{constants.ETC_HOSTS}
1002

1003
  """
1004
  hi = HostInfo(name=hostname)
1005
  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
1006
  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
1007

    
1008

    
1009
def CreateBackup(file_name):
1010
  """Creates a backup of a file.
1011

1012
  @type file_name: str
1013
  @param file_name: file to be backed up
1014
  @rtype: str
1015
  @return: the path to the newly created backup
1016
  @raise errors.ProgrammerError: for invalid file names
1017

1018
  """
1019
  if not os.path.isfile(file_name):
1020
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
1021
                                file_name)
1022

    
1023
  prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
1024
  dir_name = os.path.dirname(file_name)
1025

    
1026
  fsrc = open(file_name, 'rb')
1027
  try:
1028
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
1029
    fdst = os.fdopen(fd, 'wb')
1030
    try:
1031
      shutil.copyfileobj(fsrc, fdst)
1032
    finally:
1033
      fdst.close()
1034
  finally:
1035
    fsrc.close()
1036

    
1037
  return backup_name
1038

    
1039

    
1040
def ShellQuote(value):
1041
  """Quotes shell argument according to POSIX.
1042

1043
  @type value: str
1044
  @param value: the argument to be quoted
1045
  @rtype: str
1046
  @return: the quoted value
1047

1048
  """
1049
  if _re_shell_unquoted.match(value):
1050
    return value
1051
  else:
1052
    return "'%s'" % value.replace("'", "'\\''")
1053

    
1054

    
1055
def ShellQuoteArgs(args):
1056
  """Quotes a list of shell arguments.
1057

1058
  @type args: list
1059
  @param args: list of arguments to be quoted
1060
  @rtype: str
1061
  @return: the quoted arguments concatenaned with spaces
1062

1063
  """
1064
  return ' '.join([ShellQuote(i) for i in args])
1065

    
1066

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

1070
  Check if the given IP is reachable by doing attempting a TCP connect
1071
  to it.
1072

1073
  @type target: str
1074
  @param target: the IP or hostname to ping
1075
  @type port: int
1076
  @param port: the port to connect to
1077
  @type timeout: int
1078
  @param timeout: the timeout on the connection attemp
1079
  @type live_port_needed: boolean
1080
  @param live_port_needed: whether a closed port will cause the
1081
      function to return failure, as if there was a timeout
1082
  @type source: str or None
1083
  @param source: if specified, will cause the connect to be made
1084
      from this specific source address; failures to bind other
1085
      than C{EADDRNOTAVAIL} will be ignored
1086

1087
  """
1088
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1089

    
1090
  success = False
1091

    
1092
  if source is not None:
1093
    try:
1094
      sock.bind((source, 0))
1095
    except socket.error, (errcode, errstring):
1096
      if errcode == errno.EADDRNOTAVAIL:
1097
        success = False
1098

    
1099
  sock.settimeout(timeout)
1100

    
1101
  try:
1102
    sock.connect((target, port))
1103
    sock.close()
1104
    success = True
1105
  except socket.timeout:
1106
    success = False
1107
  except socket.error, (errcode, errstring):
1108
    success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
1109

    
1110
  return success
1111

    
1112

    
1113
def OwnIpAddress(address):
1114
  """Check if the current host has the the given IP address.
1115

1116
  Currently this is done by TCP-pinging the address from the loopback
1117
  address.
1118

1119
  @type address: string
1120
  @param address: the addres to check
1121
  @rtype: bool
1122
  @return: True if we own the address
1123

1124
  """
1125
  return TcpPing(address, constants.DEFAULT_NODED_PORT,
1126
                 source=constants.LOCALHOST_IP_ADDRESS)
1127

    
1128

    
1129
def ListVisibleFiles(path):
1130
  """Returns a list of visible files in a directory.
1131

1132
  @type path: str
1133
  @param path: the directory to enumerate
1134
  @rtype: list
1135
  @return: the list of all files not starting with a dot
1136

1137
  """
1138
  files = [i for i in os.listdir(path) if not i.startswith(".")]
1139
  files.sort()
1140
  return files
1141

    
1142

    
1143
def GetHomeDir(user, default=None):
1144
  """Try to get the homedir of the given user.
1145

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

1150
  """
1151
  try:
1152
    if isinstance(user, basestring):
1153
      result = pwd.getpwnam(user)
1154
    elif isinstance(user, (int, long)):
1155
      result = pwd.getpwuid(user)
1156
    else:
1157
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
1158
                                   type(user))
1159
  except KeyError:
1160
    return default
1161
  return result.pw_dir
1162

    
1163

    
1164
def NewUUID():
1165
  """Returns a random UUID.
1166

1167
  @note: This is a Linux-specific method as it uses the /proc
1168
      filesystem.
1169
  @rtype: str
1170

1171
  """
1172
  f = open("/proc/sys/kernel/random/uuid", "r")
1173
  try:
1174
    return f.read(128).rstrip("\n")
1175
  finally:
1176
    f.close()
1177

    
1178

    
1179
def GenerateSecret():
1180
  """Generates a random secret.
1181

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

1185
  @rtype: str
1186
  @return: a sha1 hexdigest of a block of 64 random bytes
1187

1188
  """
1189
  return sha.new(os.urandom(64)).hexdigest()
1190

    
1191

    
1192
def EnsureDirs(dirs):
1193
  """Make required directories, if they don't exist.
1194

1195
  @param dirs: list of tuples (dir_name, dir_mode)
1196
  @type dirs: list of (string, integer)
1197

1198
  """
1199
  for dir_name, dir_mode in dirs:
1200
    try:
1201
      os.mkdir(dir_name, dir_mode)
1202
    except EnvironmentError, err:
1203
      if err.errno != errno.EEXIST:
1204
        raise errors.GenericError("Cannot create needed directory"
1205
                                  " '%s': %s" % (dir_name, err))
1206
    if not os.path.isdir(dir_name):
1207
      raise errors.GenericError("%s is not a directory" % dir_name)
1208

    
1209

    
1210
def ReadFile(file_name, size=None):
1211
  """Reads a file.
1212

1213
  @type size: None or int
1214
  @param size: Read at most size bytes
1215
  @rtype: str
1216
  @return: the (possibly partial) conent of the file
1217

1218
  """
1219
  f = open(file_name, "r")
1220
  try:
1221
    if size is None:
1222
      return f.read()
1223
    else:
1224
      return f.read(size)
1225
  finally:
1226
    f.close()
1227

    
1228

    
1229
def WriteFile(file_name, fn=None, data=None,
1230
              mode=None, uid=-1, gid=-1,
1231
              atime=None, mtime=None, close=True,
1232
              dry_run=False, backup=False,
1233
              prewrite=None, postwrite=None):
1234
  """(Over)write a file atomically.
1235

1236
  The file_name and either fn (a function taking one argument, the
1237
  file descriptor, and which should write the data to it) or data (the
1238
  contents of the file) must be passed. The other arguments are
1239
  optional and allow setting the file mode, owner and group, and the
1240
  mtime/atime of the file.
1241

1242
  If the function doesn't raise an exception, it has succeeded and the
1243
  target file has the new contents. If the function has raised an
1244
  exception, an existing target file should be unmodified and the
1245
  temporary file should be removed.
1246

1247
  @type file_name: str
1248
  @param file_name: the target filename
1249
  @type fn: callable
1250
  @param fn: content writing function, called with
1251
      file descriptor as parameter
1252
  @type data: str
1253
  @param data: contents of the file
1254
  @type mode: int
1255
  @param mode: file mode
1256
  @type uid: int
1257
  @param uid: the owner of the file
1258
  @type gid: int
1259
  @param gid: the group of the file
1260
  @type atime: int
1261
  @param atime: a custom access time to be set on the file
1262
  @type mtime: int
1263
  @param mtime: a custom modification time to be set on the file
1264
  @type close: boolean
1265
  @param close: whether to close file after writing it
1266
  @type prewrite: callable
1267
  @param prewrite: function to be called before writing content
1268
  @type postwrite: callable
1269
  @param postwrite: function to be called after writing content
1270

1271
  @rtype: None or int
1272
  @return: None if the 'close' parameter evaluates to True,
1273
      otherwise the file descriptor
1274

1275
  @raise errors.ProgrammerError: if any of the arguments are not valid
1276

1277
  """
1278
  if not os.path.isabs(file_name):
1279
    raise errors.ProgrammerError("Path passed to WriteFile is not"
1280
                                 " absolute: '%s'" % file_name)
1281

    
1282
  if [fn, data].count(None) != 1:
1283
    raise errors.ProgrammerError("fn or data required")
1284

    
1285
  if [atime, mtime].count(None) == 1:
1286
    raise errors.ProgrammerError("Both atime and mtime must be either"
1287
                                 " set or None")
1288

    
1289
  if backup and not dry_run and os.path.isfile(file_name):
1290
    CreateBackup(file_name)
1291

    
1292
  dir_name, base_name = os.path.split(file_name)
1293
  fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1294
  do_remove = True
1295
  # here we need to make sure we remove the temp file, if any error
1296
  # leaves it in place
1297
  try:
1298
    if uid != -1 or gid != -1:
1299
      os.chown(new_name, uid, gid)
1300
    if mode:
1301
      os.chmod(new_name, mode)
1302
    if callable(prewrite):
1303
      prewrite(fd)
1304
    if data is not None:
1305
      os.write(fd, data)
1306
    else:
1307
      fn(fd)
1308
    if callable(postwrite):
1309
      postwrite(fd)
1310
    os.fsync(fd)
1311
    if atime is not None and mtime is not None:
1312
      os.utime(new_name, (atime, mtime))
1313
    if not dry_run:
1314
      os.rename(new_name, file_name)
1315
      do_remove = False
1316
  finally:
1317
    if close:
1318
      os.close(fd)
1319
      result = None
1320
    else:
1321
      result = fd
1322
    if do_remove:
1323
      RemoveFile(new_name)
1324

    
1325
  return result
1326

    
1327

    
1328
def FirstFree(seq, base=0):
1329
  """Returns the first non-existing integer from seq.
1330

1331
  The seq argument should be a sorted list of positive integers. The
1332
  first time the index of an element is smaller than the element
1333
  value, the index will be returned.
1334

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

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

1340
  @type seq: sequence
1341
  @param seq: the sequence to be analyzed.
1342
  @type base: int
1343
  @param base: use this value as the base index of the sequence
1344
  @rtype: int
1345
  @return: the first non-used index in the sequence
1346

1347
  """
1348
  for idx, elem in enumerate(seq):
1349
    assert elem >= base, "Passed element is higher than base offset"
1350
    if elem > idx + base:
1351
      # idx is not used
1352
      return idx + base
1353
  return None
1354

    
1355

    
1356
def all(seq, pred=bool):
1357
  "Returns True if pred(x) is True for every element in the iterable"
1358
  for elem in itertools.ifilterfalse(pred, seq):
1359
    return False
1360
  return True
1361

    
1362

    
1363
def any(seq, pred=bool):
1364
  "Returns True if pred(x) is True for at least one element in the iterable"
1365
  for elem in itertools.ifilter(pred, seq):
1366
    return True
1367
  return False
1368

    
1369

    
1370
def UniqueSequence(seq):
1371
  """Returns a list with unique elements.
1372

1373
  Element order is preserved.
1374

1375
  @type seq: sequence
1376
  @param seq: the sequence with the source elementes
1377
  @rtype: list
1378
  @return: list of unique elements from seq
1379

1380
  """
1381
  seen = set()
1382
  return [i for i in seq if i not in seen and not seen.add(i)]
1383

    
1384

    
1385
def IsValidMac(mac):
1386
  """Predicate to check if a MAC address is valid.
1387

1388
  Checks wether the supplied MAC address is formally correct, only
1389
  accepts colon separated format.
1390

1391
  @type mac: str
1392
  @param mac: the MAC to be validated
1393
  @rtype: boolean
1394
  @return: True is the MAC seems valid
1395

1396
  """
1397
  mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
1398
  return mac_check.match(mac) is not None
1399

    
1400

    
1401
def TestDelay(duration):
1402
  """Sleep for a fixed amount of time.
1403

1404
  @type duration: float
1405
  @param duration: the sleep duration
1406
  @rtype: boolean
1407
  @return: False for negative value, True otherwise
1408

1409
  """
1410
  if duration < 0:
1411
    return False
1412
  time.sleep(duration)
1413
  return True
1414

    
1415

    
1416
def _CloseFDNoErr(fd, retries=5):
1417
  """Close a file descriptor ignoring errors.
1418

1419
  @type fd: int
1420
  @param fd: the file descriptor
1421
  @type retries: int
1422
  @param retries: how many retries to make, in case we get any
1423
      other error than EBADF
1424

1425
  """
1426
  try:
1427
    os.close(fd)
1428
  except OSError, err:
1429
    if err.errno != errno.EBADF:
1430
      if retries > 0:
1431
        _CloseFDNoErr(fd, retries - 1)
1432
    # else either it's closed already or we're out of retries, so we
1433
    # ignore this and go on
1434

    
1435

    
1436
def CloseFDs(noclose_fds=None):
1437
  """Close file descriptors.
1438

1439
  This closes all file descriptors above 2 (i.e. except
1440
  stdin/out/err).
1441

1442
  @type noclose_fds: list or None
1443
  @param noclose_fds: if given, it denotes a list of file descriptor
1444
      that should not be closed
1445

1446
  """
1447
  # Default maximum for the number of available file descriptors.
1448
  if 'SC_OPEN_MAX' in os.sysconf_names:
1449
    try:
1450
      MAXFD = os.sysconf('SC_OPEN_MAX')
1451
      if MAXFD < 0:
1452
        MAXFD = 1024
1453
    except OSError:
1454
      MAXFD = 1024
1455
  else:
1456
    MAXFD = 1024
1457
  maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1458
  if (maxfd == resource.RLIM_INFINITY):
1459
    maxfd = MAXFD
1460

    
1461
  # Iterate through and close all file descriptors (except the standard ones)
1462
  for fd in range(3, maxfd):
1463
    if noclose_fds and fd in noclose_fds:
1464
      continue
1465
    _CloseFDNoErr(fd)
1466

    
1467

    
1468
def Daemonize(logfile):
1469
  """Daemonize the current process.
1470

1471
  This detaches the current process from the controlling terminal and
1472
  runs it in the background as a daemon.
1473

1474
  @type logfile: str
1475
  @param logfile: the logfile to which we should redirect stdout/stderr
1476
  @rtype: int
1477
  @return: the value zero
1478

1479
  """
1480
  UMASK = 077
1481
  WORKDIR = "/"
1482

    
1483
  # this might fail
1484
  pid = os.fork()
1485
  if (pid == 0):  # The first child.
1486
    os.setsid()
1487
    # this might fail
1488
    pid = os.fork() # Fork a second child.
1489
    if (pid == 0):  # The second child.
1490
      os.chdir(WORKDIR)
1491
      os.umask(UMASK)
1492
    else:
1493
      # exit() or _exit()?  See below.
1494
      os._exit(0) # Exit parent (the first child) of the second child.
1495
  else:
1496
    os._exit(0) # Exit parent of the first child.
1497

    
1498
  for fd in range(3):
1499
    _CloseFDNoErr(fd)
1500
  i = os.open("/dev/null", os.O_RDONLY) # stdin
1501
  assert i == 0, "Can't close/reopen stdin"
1502
  i = os.open(logfile, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0600) # stdout
1503
  assert i == 1, "Can't close/reopen stdout"
1504
  # Duplicate standard output to standard error.
1505
  os.dup2(1, 2)
1506
  return 0
1507

    
1508

    
1509
def DaemonPidFileName(name):
1510
  """Compute a ganeti pid file absolute path
1511

1512
  @type name: str
1513
  @param name: the daemon name
1514
  @rtype: str
1515
  @return: the full path to the pidfile corresponding to the given
1516
      daemon name
1517

1518
  """
1519
  return os.path.join(constants.RUN_GANETI_DIR, "%s.pid" % name)
1520

    
1521

    
1522
def WritePidFile(name):
1523
  """Write the current process pidfile.
1524

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

1527
  @type name: str
1528
  @param name: the daemon name to use
1529
  @raise errors.GenericError: if the pid file already exists and
1530
      points to a live process
1531

1532
  """
1533
  pid = os.getpid()
1534
  pidfilename = DaemonPidFileName(name)
1535
  if IsProcessAlive(ReadPidFile(pidfilename)):
1536
    raise errors.GenericError("%s contains a live process" % pidfilename)
1537

    
1538
  WriteFile(pidfilename, data="%d\n" % pid)
1539

    
1540

    
1541
def RemovePidFile(name):
1542
  """Remove the current process pidfile.
1543

1544
  Any errors are ignored.
1545

1546
  @type name: str
1547
  @param name: the daemon name used to derive the pidfile name
1548

1549
  """
1550
  pid = os.getpid()
1551
  pidfilename = DaemonPidFileName(name)
1552
  # TODO: we could check here that the file contains our pid
1553
  try:
1554
    RemoveFile(pidfilename)
1555
  except:
1556
    pass
1557

    
1558

    
1559
def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1560
                waitpid=False):
1561
  """Kill a process given by its pid.
1562

1563
  @type pid: int
1564
  @param pid: The PID to terminate.
1565
  @type signal_: int
1566
  @param signal_: The signal to send, by default SIGTERM
1567
  @type timeout: int
1568
  @param timeout: The timeout after which, if the process is still alive,
1569
                  a SIGKILL will be sent. If not positive, no such checking
1570
                  will be done
1571
  @type waitpid: boolean
1572
  @param waitpid: If true, we should waitpid on this process after
1573
      sending signals, since it's our own child and otherwise it
1574
      would remain as zombie
1575

1576
  """
1577
  def _helper(pid, signal_, wait):
1578
    """Simple helper to encapsulate the kill/waitpid sequence"""
1579
    os.kill(pid, signal_)
1580
    if wait:
1581
      try:
1582
        os.waitpid(pid, os.WNOHANG)
1583
      except OSError:
1584
        pass
1585

    
1586
  if pid <= 0:
1587
    # kill with pid=0 == suicide
1588
    raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1589

    
1590
  if not IsProcessAlive(pid):
1591
    return
1592
  _helper(pid, signal_, waitpid)
1593
  if timeout <= 0:
1594
    return
1595

    
1596
  # Wait up to $timeout seconds
1597
  end = time.time() + timeout
1598
  wait = 0.01
1599
  while time.time() < end and IsProcessAlive(pid):
1600
    try:
1601
      (result_pid, _) = os.waitpid(pid, os.WNOHANG)
1602
      if result_pid > 0:
1603
        break
1604
    except OSError:
1605
      pass
1606
    time.sleep(wait)
1607
    # Make wait time longer for next try
1608
    if wait < 0.1:
1609
      wait *= 1.5
1610

    
1611
  if IsProcessAlive(pid):
1612
    # Kill process if it's still alive
1613
    _helper(pid, signal.SIGKILL, waitpid)
1614

    
1615

    
1616
def FindFile(name, search_path, test=os.path.exists):
1617
  """Look for a filesystem object in a given path.
1618

1619
  This is an abstract method to search for filesystem object (files,
1620
  dirs) under a given search path.
1621

1622
  @type name: str
1623
  @param name: the name to look for
1624
  @type search_path: str
1625
  @param search_path: location to start at
1626
  @type test: callable
1627
  @param test: a function taking one argument that should return True
1628
      if the a given object is valid; the default value is
1629
      os.path.exists, causing only existing files to be returned
1630
  @rtype: str or None
1631
  @return: full path to the object if found, None otherwise
1632

1633
  """
1634
  for dir_name in search_path:
1635
    item_name = os.path.sep.join([dir_name, name])
1636
    if test(item_name):
1637
      return item_name
1638
  return None
1639

    
1640

    
1641
def CheckVolumeGroupSize(vglist, vgname, minsize):
1642
  """Checks if the volume group list is valid.
1643

1644
  The function will check if a given volume group is in the list of
1645
  volume groups and has a minimum size.
1646

1647
  @type vglist: dict
1648
  @param vglist: dictionary of volume group names and their size
1649
  @type vgname: str
1650
  @param vgname: the volume group we should check
1651
  @type minsize: int
1652
  @param minsize: the minimum size we accept
1653
  @rtype: None or str
1654
  @return: None for success, otherwise the error message
1655

1656
  """
1657
  vgsize = vglist.get(vgname, None)
1658
  if vgsize is None:
1659
    return "volume group '%s' missing" % vgname
1660
  elif vgsize < minsize:
1661
    return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1662
            (vgname, minsize, vgsize))
1663
  return None
1664

    
1665

    
1666
def SplitTime(value):
1667
  """Splits time as floating point number into a tuple.
1668

1669
  @param value: Time in seconds
1670
  @type value: int or float
1671
  @return: Tuple containing (seconds, microseconds)
1672

1673
  """
1674
  (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
1675

    
1676
  assert 0 <= seconds, \
1677
    "Seconds must be larger than or equal to 0, but are %s" % seconds
1678
  assert 0 <= microseconds <= 999999, \
1679
    "Microseconds must be 0-999999, but are %s" % microseconds
1680

    
1681
  return (int(seconds), int(microseconds))
1682

    
1683

    
1684
def MergeTime(timetuple):
1685
  """Merges a tuple into time as a floating point number.
1686

1687
  @param timetuple: Time as tuple, (seconds, microseconds)
1688
  @type timetuple: tuple
1689
  @return: Time as a floating point number expressed in seconds
1690

1691
  """
1692
  (seconds, microseconds) = timetuple
1693

    
1694
  assert 0 <= seconds, \
1695
    "Seconds must be larger than or equal to 0, but are %s" % seconds
1696
  assert 0 <= microseconds <= 999999, \
1697
    "Microseconds must be 0-999999, but are %s" % microseconds
1698

    
1699
  return float(seconds) + (float(microseconds) * 0.000001)
1700

    
1701

    
1702
def GetNodeDaemonPort():
1703
  """Get the node daemon port for this cluster.
1704

1705
  Note that this routine does not read a ganeti-specific file, but
1706
  instead uses C{socket.getservbyname} to allow pre-customization of
1707
  this parameter outside of Ganeti.
1708

1709
  @rtype: int
1710

1711
  """
1712
  try:
1713
    port = socket.getservbyname("ganeti-noded", "tcp")
1714
  except socket.error:
1715
    port = constants.DEFAULT_NODED_PORT
1716

    
1717
  return port
1718

    
1719

    
1720
def SetupLogging(logfile, debug=False, stderr_logging=False, program="",
1721
                 multithreaded=False):
1722
  """Configures the logging module.
1723

1724
  @type logfile: str
1725
  @param logfile: the filename to which we should log
1726
  @type debug: boolean
1727
  @param debug: whether to enable debug messages too or
1728
      only those at C{INFO} and above level
1729
  @type stderr_logging: boolean
1730
  @param stderr_logging: whether we should also log to the standard error
1731
  @type program: str
1732
  @param program: the name under which we should log messages
1733
  @type multithreaded: boolean
1734
  @param multithreaded: if True, will add the thread name to the log file
1735
  @raise EnvironmentError: if we can't open the log file and
1736
      stderr logging is disabled
1737

1738
  """
1739
  fmt = "%(asctime)s: " + program + " pid=%(process)d"
1740
  if multithreaded:
1741
    fmt += "/%(threadName)s"
1742
  if debug:
1743
    fmt += " %(module)s:%(lineno)s"
1744
  fmt += " %(levelname)s %(message)s"
1745
  formatter = logging.Formatter(fmt)
1746

    
1747
  root_logger = logging.getLogger("")
1748
  root_logger.setLevel(logging.NOTSET)
1749

    
1750
  # Remove all previously setup handlers
1751
  for handler in root_logger.handlers:
1752
    handler.close()
1753
    root_logger.removeHandler(handler)
1754

    
1755
  if stderr_logging:
1756
    stderr_handler = logging.StreamHandler()
1757
    stderr_handler.setFormatter(formatter)
1758
    if debug:
1759
      stderr_handler.setLevel(logging.NOTSET)
1760
    else:
1761
      stderr_handler.setLevel(logging.CRITICAL)
1762
    root_logger.addHandler(stderr_handler)
1763

    
1764
  # this can fail, if the logging directories are not setup or we have
1765
  # a permisssion problem; in this case, it's best to log but ignore
1766
  # the error if stderr_logging is True, and if false we re-raise the
1767
  # exception since otherwise we could run but without any logs at all
1768
  try:
1769
    logfile_handler = logging.FileHandler(logfile)
1770
    logfile_handler.setFormatter(formatter)
1771
    if debug:
1772
      logfile_handler.setLevel(logging.DEBUG)
1773
    else:
1774
      logfile_handler.setLevel(logging.INFO)
1775
    root_logger.addHandler(logfile_handler)
1776
  except EnvironmentError:
1777
    if stderr_logging:
1778
      logging.exception("Failed to enable logging to file '%s'", logfile)
1779
    else:
1780
      # we need to re-raise the exception
1781
      raise
1782

    
1783

    
1784
def TailFile(fname, lines=20):
1785
  """Return the last lines from a file.
1786

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

1791
  @param fname: the file name
1792
  @type lines: int
1793
  @param lines: the (maximum) number of lines to return
1794

1795
  """
1796
  fd = open(fname, "r")
1797
  try:
1798
    fd.seek(0, 2)
1799
    pos = fd.tell()
1800
    pos = max(0, pos-4096)
1801
    fd.seek(pos, 0)
1802
    raw_data = fd.read()
1803
  finally:
1804
    fd.close()
1805

    
1806
  rows = raw_data.splitlines()
1807
  return rows[-lines:]
1808

    
1809

    
1810
def SafeEncode(text):
1811
  """Return a 'safe' version of a source string.
1812

1813
  This function mangles the input string and returns a version that
1814
  should be safe to disply/encode as ASCII. To this end, we first
1815
  convert it to ASCII using the 'backslashreplace' encoding which
1816
  should get rid of any non-ASCII chars, and then we again encode it
1817
  via 'string_escape' which converts '\n' into '\\n' so that log
1818
  messages remain one-line.
1819

1820
  @type text: str or unicode
1821
  @param text: input data
1822
  @rtype: str
1823
  @return: a safe version of text
1824

1825
  """
1826
  text = text.encode('ascii', 'backslashreplace')
1827
  text = text.encode('string_escape')
1828
  return text
1829

    
1830

    
1831
def LockedMethod(fn):
1832
  """Synchronized object access decorator.
1833

1834
  This decorator is intended to protect access to an object using the
1835
  object's own lock which is hardcoded to '_lock'.
1836

1837
  """
1838
  def _LockDebug(*args, **kwargs):
1839
    if debug_locks:
1840
      logging.debug(*args, **kwargs)
1841

    
1842
  def wrapper(self, *args, **kwargs):
1843
    assert hasattr(self, '_lock')
1844
    lock = self._lock
1845
    _LockDebug("Waiting for %s", lock)
1846
    lock.acquire()
1847
    try:
1848
      _LockDebug("Acquired %s", lock)
1849
      result = fn(self, *args, **kwargs)
1850
    finally:
1851
      _LockDebug("Releasing %s", lock)
1852
      lock.release()
1853
      _LockDebug("Released %s", lock)
1854
    return result
1855
  return wrapper
1856

    
1857

    
1858
def LockFile(fd):
1859
  """Locks a file using POSIX locks.
1860

1861
  @type fd: int
1862
  @param fd: the file descriptor we need to lock
1863

1864
  """
1865
  try:
1866
    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1867
  except IOError, err:
1868
    if err.errno == errno.EAGAIN:
1869
      raise errors.LockError("File already locked")
1870
    raise
1871

    
1872

    
1873
class FileLock(object):
1874
  """Utility class for file locks.
1875

1876
  """
1877
  def __init__(self, filename):
1878
    """Constructor for FileLock.
1879

1880
    This will open the file denoted by the I{filename} argument.
1881

1882
    @type filename: str
1883
    @param filename: path to the file to be locked
1884

1885
    """
1886
    self.filename = filename
1887
    self.fd = open(self.filename, "w")
1888

    
1889
  def __del__(self):
1890
    self.Close()
1891

    
1892
  def Close(self):
1893
    """Close the file and release the lock.
1894

1895
    """
1896
    if self.fd:
1897
      self.fd.close()
1898
      self.fd = None
1899

    
1900
  def _flock(self, flag, blocking, timeout, errmsg):
1901
    """Wrapper for fcntl.flock.
1902

1903
    @type flag: int
1904
    @param flag: operation flag
1905
    @type blocking: bool
1906
    @param blocking: whether the operation should be done in blocking mode.
1907
    @type timeout: None or float
1908
    @param timeout: for how long the operation should be retried (implies
1909
                    non-blocking mode).
1910
    @type errmsg: string
1911
    @param errmsg: error message in case operation fails.
1912

1913
    """
1914
    assert self.fd, "Lock was closed"
1915
    assert timeout is None or timeout >= 0, \
1916
      "If specified, timeout must be positive"
1917

    
1918
    if timeout is not None:
1919
      flag |= fcntl.LOCK_NB
1920
      timeout_end = time.time() + timeout
1921

    
1922
    # Blocking doesn't have effect with timeout
1923
    elif not blocking:
1924
      flag |= fcntl.LOCK_NB
1925
      timeout_end = None
1926

    
1927
    retry = True
1928
    while retry:
1929
      try:
1930
        fcntl.flock(self.fd, flag)
1931
        retry = False
1932
      except IOError, err:
1933
        if err.errno in (errno.EAGAIN, ):
1934
          if timeout_end is not None and time.time() < timeout_end:
1935
            # Wait before trying again
1936
            time.sleep(max(0.1, min(1.0, timeout)))
1937
          else:
1938
            raise errors.LockError(errmsg)
1939
        else:
1940
          logging.exception("fcntl.flock failed")
1941
          raise
1942

    
1943
  def Exclusive(self, blocking=False, timeout=None):
1944
    """Locks the file in exclusive mode.
1945

1946
    @type blocking: boolean
1947
    @param blocking: whether to block and wait until we
1948
        can lock the file or return immediately
1949
    @type timeout: int or None
1950
    @param timeout: if not None, the duration to wait for the lock
1951
        (in blocking mode)
1952

1953
    """
1954
    self._flock(fcntl.LOCK_EX, blocking, timeout,
1955
                "Failed to lock %s in exclusive mode" % self.filename)
1956

    
1957
  def Shared(self, blocking=False, timeout=None):
1958
    """Locks the file in shared mode.
1959

1960
    @type blocking: boolean
1961
    @param blocking: whether to block and wait until we
1962
        can lock the file or return immediately
1963
    @type timeout: int or None
1964
    @param timeout: if not None, the duration to wait for the lock
1965
        (in blocking mode)
1966

1967
    """
1968
    self._flock(fcntl.LOCK_SH, blocking, timeout,
1969
                "Failed to lock %s in shared mode" % self.filename)
1970

    
1971
  def Unlock(self, blocking=True, timeout=None):
1972
    """Unlocks the file.
1973

1974
    According to C{flock(2)}, unlocking can also be a nonblocking
1975
    operation::
1976

1977
      To make a non-blocking request, include LOCK_NB with any of the above
1978
      operations.
1979

1980
    @type blocking: boolean
1981
    @param blocking: whether to block and wait until we
1982
        can lock the file or return immediately
1983
    @type timeout: int or None
1984
    @param timeout: if not None, the duration to wait for the lock
1985
        (in blocking mode)
1986

1987
    """
1988
    self._flock(fcntl.LOCK_UN, blocking, timeout,
1989
                "Failed to unlock %s" % self.filename)
1990

    
1991

    
1992
class SignalHandler(object):
1993
  """Generic signal handler class.
1994

1995
  It automatically restores the original handler when deconstructed or
1996
  when L{Reset} is called. You can either pass your own handler
1997
  function in or query the L{called} attribute to detect whether the
1998
  signal was sent.
1999

2000
  @type signum: list
2001
  @ivar signum: the signals we handle
2002
  @type called: boolean
2003
  @ivar called: tracks whether any of the signals have been raised
2004

2005
  """
2006
  def __init__(self, signum):
2007
    """Constructs a new SignalHandler instance.
2008

2009
    @type signum: int or list of ints
2010
    @param signum: Single signal number or set of signal numbers
2011

2012
    """
2013
    if isinstance(signum, (int, long)):
2014
      self.signum = set([signum])
2015
    else:
2016
      self.signum = set(signum)
2017

    
2018
    self.called = False
2019

    
2020
    self._previous = {}
2021
    try:
2022
      for signum in self.signum:
2023
        # Setup handler
2024
        prev_handler = signal.signal(signum, self._HandleSignal)
2025
        try:
2026
          self._previous[signum] = prev_handler
2027
        except:
2028
          # Restore previous handler
2029
          signal.signal(signum, prev_handler)
2030
          raise
2031
    except:
2032
      # Reset all handlers
2033
      self.Reset()
2034
      # Here we have a race condition: a handler may have already been called,
2035
      # but there's not much we can do about it at this point.
2036
      raise
2037

    
2038
  def __del__(self):
2039
    self.Reset()
2040

    
2041
  def Reset(self):
2042
    """Restore previous handler.
2043

2044
    This will reset all the signals to their previous handlers.
2045

2046
    """
2047
    for signum, prev_handler in self._previous.items():
2048
      signal.signal(signum, prev_handler)
2049
      # If successful, remove from dict
2050
      del self._previous[signum]
2051

    
2052
  def Clear(self):
2053
    """Unsets the L{called} flag.
2054

2055
    This function can be used in case a signal may arrive several times.
2056

2057
    """
2058
    self.called = False
2059

    
2060
  def _HandleSignal(self, signum, frame):
2061
    """Actual signal handling function.
2062

2063
    """
2064
    # This is not nice and not absolutely atomic, but it appears to be the only
2065
    # solution in Python -- there are no atomic types.
2066
    self.called = True
2067

    
2068

    
2069
class FieldSet(object):
2070
  """A simple field set.
2071

2072
  Among the features are:
2073
    - checking if a string is among a list of static string or regex objects
2074
    - checking if a whole list of string matches
2075
    - returning the matching groups from a regex match
2076

2077
  Internally, all fields are held as regular expression objects.
2078

2079
  """
2080
  def __init__(self, *items):
2081
    self.items = [re.compile("^%s$" % value) for value in items]
2082

    
2083
  def Extend(self, other_set):
2084
    """Extend the field set with the items from another one"""
2085
    self.items.extend(other_set.items)
2086

    
2087
  def Matches(self, field):
2088
    """Checks if a field matches the current set
2089

2090
    @type field: str
2091
    @param field: the string to match
2092
    @return: either False or a regular expression match object
2093

2094
    """
2095
    for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
2096
      return m
2097
    return False
2098

    
2099
  def NonMatching(self, items):
2100
    """Returns the list of fields not matching the current set
2101

2102
    @type items: list
2103
    @param items: the list of fields to check
2104
    @rtype: list
2105
    @return: list of non-matching fields
2106

2107
    """
2108
    return [val for val in items if not self.Matches(val)]