Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ 59f82e3f

History | View | Annotate | Download (23.2 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 small utilities
23
"""
24

    
25

    
26
import sys
27
import os
28
import sha
29
import time
30
import subprocess
31
import re
32
import socket
33
import tempfile
34
import shutil
35
import errno
36
import pwd
37
import itertools
38

    
39
from ganeti import logger
40
from ganeti import errors
41

    
42

    
43
_locksheld = []
44
_re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
45

    
46
class RunResult(object):
47
  """Simple class for holding the result of running external programs.
48

49
  Instance variables:
50
    exit_code: the exit code of the program, or None (if the program
51
               didn't exit())
52
    signal: numeric signal that caused the program to finish, or None
53
            (if the program wasn't terminated by a signal)
54
    stdout: the standard output of the program
55
    stderr: the standard error of the program
56
    failed: a Boolean value which is True in case the program was
57
            terminated by a signal or exited with a non-zero exit code
58
    fail_reason: a string detailing the termination reason
59

60
  """
61
  __slots__ = ["exit_code", "signal", "stdout", "stderr",
62
               "failed", "fail_reason", "cmd"]
63

    
64

    
65
  def __init__(self, exit_code, signal, stdout, stderr, cmd):
66
    self.cmd = cmd
67
    self.exit_code = exit_code
68
    self.signal = signal
69
    self.stdout = stdout
70
    self.stderr = stderr
71
    self.failed = (signal is not None or exit_code != 0)
72

    
73
    if self.signal is not None:
74
      self.fail_reason = "terminated by signal %s" % self.signal
75
    elif self.exit_code is not None:
76
      self.fail_reason = "exited with exit code %s" % self.exit_code
77
    else:
78
      self.fail_reason = "unable to determine termination reason"
79

    
80
  def _GetOutput(self):
81
    """Returns the combined stdout and stderr for easier usage.
82

83
    """
84
    return self.stdout + self.stderr
85

    
86
  output = property(_GetOutput, None, None, "Return full output")
87

    
88

    
89
def _GetLockFile(subsystem):
90
  """Compute the file name for a given lock name."""
91
  return "/var/lock/ganeti_lock_%s" % subsystem
92

    
93

    
94
def Lock(name, max_retries=None, debug=False):
95
  """Lock a given subsystem.
96

97
  In case the lock is already held by an alive process, the function
98
  will sleep indefintely and poll with a one second interval.
99

100
  When the optional integer argument 'max_retries' is passed with a
101
  non-zero value, the function will sleep only for this number of
102
  times, and then it will will raise a LockError if the lock can't be
103
  acquired. Passing in a negative number will cause only one try to
104
  get the lock. Passing a positive number will make the function retry
105
  for approximately that number of seconds.
106

107
  """
108
  lockfile = _GetLockFile(name)
109

    
110
  if name in _locksheld:
111
    raise errors.LockError('Lock "%s" already held!' % (name,))
112

    
113
  errcount = 0
114

    
115
  retries = 0
116
  while True:
117
    try:
118
      fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
119
      break
120
    except OSError, creat_err:
121
      if creat_err.errno != errno.EEXIST:
122
        raise errors.LockError("Can't create the lock file. Error '%s'." %
123
                               str(creat_err))
124

    
125
      try:
126
        pf = open(lockfile, 'r')
127
      except IOError, open_err:
128
        errcount += 1
129
        if errcount >= 5:
130
          raise errors.LockError("Lock file exists but cannot be opened."
131
                                 " Error: '%s'." % str(open_err))
132
        time.sleep(1)
133
        continue
134

    
135
      try:
136
        pid = int(pf.read())
137
      except ValueError:
138
        raise errors.LockError("Invalid pid string in %s" %
139
                               (lockfile,))
140

    
141
      if not IsProcessAlive(pid):
142
        raise errors.LockError("Stale lockfile %s for pid %d?" %
143
                               (lockfile, pid))
144

    
145
      if max_retries and max_retries <= retries:
146
        raise errors.LockError("Can't acquire lock during the specified"
147
                               " time, aborting.")
148
      if retries == 5 and (debug or sys.stdin.isatty()):
149
        logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
150

    
151
      time.sleep(1)
152
      retries += 1
153
      continue
154

    
155
  os.write(fd, '%d\n' % (os.getpid(),))
156
  os.close(fd)
157

    
158
  _locksheld.append(name)
159

    
160

    
161
def Unlock(name):
162
  """Unlock a given subsystem.
163

164
  """
165
  lockfile = _GetLockFile(name)
166

    
167
  try:
168
    fd = os.open(lockfile, os.O_RDONLY)
169
  except OSError:
170
    raise errors.LockError('Lock "%s" not held.' % (name,))
171

    
172
  f = os.fdopen(fd, 'r')
173
  pid_str = f.read()
174

    
175
  try:
176
    pid = int(pid_str)
177
  except ValueError:
178
    raise errors.LockError('Unable to determine PID of locking process.')
179

    
180
  if pid != os.getpid():
181
    raise errors.LockError('Lock not held by me (%d != %d)' %
182
                           (os.getpid(), pid,))
183

    
184
  os.unlink(lockfile)
185
  _locksheld.remove(name)
186

    
187

    
188
def LockCleanup():
189
  """Remove all locks.
190

191
  """
192
  for lock in _locksheld:
193
    Unlock(lock)
194

    
195

    
196
def RunCmd(cmd):
197
  """Execute a (shell) command.
198

199
  The command should not read from its standard input, as it will be
200
  closed.
201

202
  Args:
203
    cmd: command to run. (str)
204

205
  Returns: `RunResult` instance
206

207
  """
208
  if isinstance(cmd, list):
209
    cmd = [str(val) for val in cmd]
210
    strcmd = " ".join(cmd)
211
    shell = False
212
  else:
213
    strcmd = cmd
214
    shell = True
215
  env = os.environ.copy()
216
  env["LC_ALL"] = "C"
217
  child = subprocess.Popen(cmd, shell=shell,
218
                           stderr=subprocess.PIPE,
219
                           stdout=subprocess.PIPE,
220
                           stdin=subprocess.PIPE,
221
                           close_fds=True, env=env)
222

    
223
  child.stdin.close()
224
  out = child.stdout.read()
225
  err = child.stderr.read()
226

    
227
  status = child.wait()
228
  if status >= 0:
229
    exitcode = status
230
    signal = None
231
  else:
232
    exitcode = None
233
    signal = -status
234

    
235
  return RunResult(exitcode, signal, out, err, strcmd)
236

    
237

    
238
def RunCmdUnlocked(cmd):
239
  """Execute a shell command without the 'cmd' lock.
240

241
  This variant of `RunCmd()` drops the 'cmd' lock before running the
242
  command and re-aquires it afterwards, thus it can be used to call
243
  other ganeti commands.
244

245
  The argument and return values are the same as for the `RunCmd()`
246
  function.
247

248
  Args:
249
    cmd - command to run. (str)
250

251
  Returns:
252
    `RunResult`
253

254
  """
255
  Unlock('cmd')
256
  ret = RunCmd(cmd)
257
  Lock('cmd')
258

    
259
  return ret
260

    
261

    
262
def RemoveFile(filename):
263
  """Remove a file ignoring some errors.
264

265
  Remove a file, ignoring non-existing ones or directories. Other
266
  errors are passed.
267

268
  """
269
  try:
270
    os.unlink(filename)
271
  except OSError, err:
272
    if err.errno not in (errno.ENOENT, errno.EISDIR):
273
      raise
274

    
275

    
276
def _FingerprintFile(filename):
277
  """Compute the fingerprint of a file.
278

279
  If the file does not exist, a None will be returned
280
  instead.
281

282
  Args:
283
    filename - Filename (str)
284

285
  """
286
  if not (os.path.exists(filename) and os.path.isfile(filename)):
287
    return None
288

    
289
  f = open(filename)
290

    
291
  fp = sha.sha()
292
  while True:
293
    data = f.read(4096)
294
    if not data:
295
      break
296

    
297
    fp.update(data)
298

    
299
  return fp.hexdigest()
300

    
301

    
302
def FingerprintFiles(files):
303
  """Compute fingerprints for a list of files.
304

305
  Args:
306
    files - array of filenames.  ( [str, ...] )
307

308
  Return value:
309
    dictionary of filename: fingerprint for the files that exist
310

311
  """
312
  ret = {}
313

    
314
  for filename in files:
315
    cksum = _FingerprintFile(filename)
316
    if cksum:
317
      ret[filename] = cksum
318

    
319
  return ret
320

    
321

    
322
def CheckDict(target, template, logname=None):
323
  """Ensure a dictionary has a required set of keys.
324

325
  For the given dictionaries `target` and `template`, ensure target
326
  has all the keys from template. Missing keys are added with values
327
  from template.
328

329
  Args:
330
    target   - the dictionary to check
331
    template - template dictionary
332
    logname  - a caller-chosen string to identify the debug log
333
               entry; if None, no logging will be done
334

335
  Returns value:
336
    None
337

338
  """
339
  missing = []
340
  for k in template:
341
    if k not in target:
342
      missing.append(k)
343
      target[k] = template[k]
344

    
345
  if missing and logname:
346
    logger.Debug('%s missing keys %s' %
347
                 (logname, ', '.join(missing)))
348

    
349

    
350
def IsProcessAlive(pid):
351
  """Check if a given pid exists on the system.
352

353
  Returns: true or false, depending on if the pid exists or not
354

355
  Remarks: zombie processes treated as not alive
356

357
  """
358
  try:
359
    f = open("/proc/%d/status" % pid)
360
  except IOError, err:
361
    if err.errno in (errno.ENOENT, errno.ENOTDIR):
362
      return False
363

    
364
  alive = True
365
  try:
366
    data = f.readlines()
367
    if len(data) > 1:
368
      state = data[1].split()
369
      if len(state) > 1 and state[1] == "Z":
370
        alive = False
371
  finally:
372
    f.close()
373

    
374
  return alive
375

    
376

    
377
def MatchNameComponent(key, name_list):
378
  """Try to match a name against a list.
379

380
  This function will try to match a name like test1 against a list
381
  like ['test1.example.com', 'test2.example.com', ...]. Against this
382
  list, 'test1' as well as 'test1.example' will match, but not
383
  'test1.ex'. A multiple match will be considered as no match at all
384
  (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
385

386
  Args:
387
    key: the name to be searched
388
    name_list: the list of strings against which to search the key
389

390
  Returns:
391
    None if there is no match *or* if there are multiple matches
392
    otherwise the element from the list which matches
393

394
  """
395
  mo = re.compile("^%s(\..*)?$" % re.escape(key))
396
  names_filtered = [name for name in name_list if mo.match(name) is not None]
397
  if len(names_filtered) != 1:
398
    return None
399
  return names_filtered[0]
400

    
401

    
402
class HostInfo:
403
  """Class implementing resolver and hostname functionality
404

405
  """
406
  def __init__(self, name=None):
407
    """Initialize the host name object.
408

409
    If the name argument is not passed, it will use this system's
410
    name.
411

412
    """
413
    if name is None:
414
      name = self.SysName()
415

    
416
    self.query = name
417
    self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
418
    self.ip = self.ipaddrs[0]
419

    
420
  @staticmethod
421
  def SysName():
422
    """Return the current system's name.
423

424
    This is simply a wrapper over socket.gethostname()
425

426
    """
427
    return socket.gethostname()
428

    
429
  @staticmethod
430
  def LookupHostname(hostname):
431
    """Look up hostname
432

433
    Args:
434
      hostname: hostname to look up
435

436
    Returns:
437
      a tuple (name, aliases, ipaddrs) as returned by socket.gethostbyname_ex
438
      in case of errors in resolving, we raise a ResolverError
439

440
    """
441
    try:
442
      result = socket.gethostbyname_ex(hostname)
443
    except socket.gaierror, err:
444
      # hostname not found in DNS
445
      raise errors.ResolverError(hostname, err.args[0], err.args[1])
446

    
447
    return result
448

    
449

    
450
def ListVolumeGroups():
451
  """List volume groups and their size
452

453
  Returns:
454
     Dictionary with keys volume name and values the size of the volume
455

456
  """
457
  command = "vgs --noheadings --units m --nosuffix -o name,size"
458
  result = RunCmd(command)
459
  retval = {}
460
  if result.failed:
461
    return retval
462

    
463
  for line in result.stdout.splitlines():
464
    try:
465
      name, size = line.split()
466
      size = int(float(size))
467
    except (IndexError, ValueError), err:
468
      logger.Error("Invalid output from vgs (%s): %s" % (err, line))
469
      continue
470

    
471
    retval[name] = size
472

    
473
  return retval
474

    
475

    
476
def BridgeExists(bridge):
477
  """Check whether the given bridge exists in the system
478

479
  Returns:
480
     True if it does, false otherwise.
481

482
  """
483
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
484

    
485

    
486
def NiceSort(name_list):
487
  """Sort a list of strings based on digit and non-digit groupings.
488

489
  Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
490
  sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
491

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

496
  Return value
497
    - a copy of the list sorted according to our algorithm
498

499
  """
500
  _SORTER_BASE = "(\D+|\d+)"
501
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
502
                                                  _SORTER_BASE, _SORTER_BASE,
503
                                                  _SORTER_BASE, _SORTER_BASE,
504
                                                  _SORTER_BASE, _SORTER_BASE)
505
  _SORTER_RE = re.compile(_SORTER_FULL)
506
  _SORTER_NODIGIT = re.compile("^\D*$")
507
  def _TryInt(val):
508
    """Attempts to convert a variable to integer."""
509
    if val is None or _SORTER_NODIGIT.match(val):
510
      return val
511
    rval = int(val)
512
    return rval
513

    
514
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
515
             for name in name_list]
516
  to_sort.sort()
517
  return [tup[1] for tup in to_sort]
518

    
519

    
520
def CheckDaemonAlive(pid_file, process_string):
521
  """Check wether the specified daemon is alive.
522

523
  Args:
524
   - pid_file: file to read the daemon pid from, the file is
525
               expected to contain only a single line containing
526
               only the PID
527
   - process_string: a substring that we expect to find in
528
                     the command line of the daemon process
529

530
  Returns:
531
   - True if the daemon is judged to be alive (that is:
532
      - the PID file exists, is readable and contains a number
533
      - a process of the specified PID is running
534
      - that process contains the specified string in its
535
        command line
536
      - the process is not in state Z (zombie))
537
   - False otherwise
538

539
  """
540
  try:
541
    pid_file = file(pid_file, 'r')
542
    try:
543
      pid = int(pid_file.readline())
544
    finally:
545
      pid_file.close()
546

    
547
    cmdline_file_path = "/proc/%s/cmdline" % (pid)
548
    cmdline_file = open(cmdline_file_path, 'r')
549
    try:
550
      cmdline = cmdline_file.readline()
551
    finally:
552
      cmdline_file.close()
553

    
554
    if not process_string in cmdline:
555
      return False
556

    
557
    stat_file_path =  "/proc/%s/stat" % (pid)
558
    stat_file = open(stat_file_path, 'r')
559
    try:
560
      process_state = stat_file.readline().split()[2]
561
    finally:
562
      stat_file.close()
563

    
564
    if process_state == 'Z':
565
      return False
566

    
567
  except (IndexError, IOError, ValueError):
568
    return False
569

    
570
  return True
571

    
572

    
573
def TryConvert(fn, val):
574
  """Try to convert a value ignoring errors.
575

576
  This function tries to apply function `fn` to `val`. If no
577
  ValueError or TypeError exceptions are raised, it will return the
578
  result, else it will return the original value. Any other exceptions
579
  are propagated to the caller.
580

581
  """
582
  try:
583
    nv = fn(val)
584
  except (ValueError, TypeError), err:
585
    nv = val
586
  return nv
587

    
588

    
589
def IsValidIP(ip):
590
  """Verifies the syntax of an IP address.
591

592
  This function checks if the ip address passes is valid or not based
593
  on syntax (not ip range, class calculations or anything).
594

595
  """
596
  unit = "(0|[1-9]\d{0,2})"
597
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
598

    
599

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

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

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

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

    
613

    
614
def BuildShellCmd(template, *args):
615
  """Build a safe shell command line from the given arguments.
616

617
  This function will check all arguments in the args list so that they
618
  are valid shell parameters (i.e. they don't contain shell
619
  metacharaters). If everything is ok, it will return the result of
620
  template % args.
621

622
  """
623
  for word in args:
624
    if not IsValidShellParam(word):
625
      raise errors.ProgrammerError("Shell argument '%s' contains"
626
                                   " invalid characters" % word)
627
  return template % args
628

    
629

    
630
def FormatUnit(value):
631
  """Formats an incoming number of MiB with the appropriate unit.
632

633
  Value needs to be passed as a numeric type. Return value is always a string.
634

635
  """
636
  if value < 1024:
637
    return "%dM" % round(value, 0)
638

    
639
  elif value < (1024 * 1024):
640
    return "%0.1fG" % round(float(value) / 1024, 1)
641

    
642
  else:
643
    return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
644

    
645

    
646
def ParseUnit(input_string):
647
  """Tries to extract number and scale from the given string.
648

649
  Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
650
  is specified, it defaults to MiB. Return value is always an int in MiB.
651

652
  """
653
  m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
654
  if not m:
655
    raise errors.UnitParseError("Invalid format")
656

    
657
  value = float(m.groups()[0])
658

    
659
  unit = m.groups()[1]
660
  if unit:
661
    lcunit = unit.lower()
662
  else:
663
    lcunit = 'm'
664

    
665
  if lcunit in ('m', 'mb', 'mib'):
666
    # Value already in MiB
667
    pass
668

    
669
  elif lcunit in ('g', 'gb', 'gib'):
670
    value *= 1024
671

    
672
  elif lcunit in ('t', 'tb', 'tib'):
673
    value *= 1024 * 1024
674

    
675
  else:
676
    raise errors.UnitParseError("Unknown unit: %s" % unit)
677

    
678
  # Make sure we round up
679
  if int(value) < value:
680
    value += 1
681

    
682
  # Round up to the next multiple of 4
683
  value = int(value)
684
  if value % 4:
685
    value += 4 - value % 4
686

    
687
  return value
688

    
689

    
690
def AddAuthorizedKey(file_name, key):
691
  """Adds an SSH public key to an authorized_keys file.
692

693
  Args:
694
    file_name: Path to authorized_keys file
695
    key: String containing key
696
  """
697
  key_fields = key.split()
698

    
699
  f = open(file_name, 'a+')
700
  try:
701
    nl = True
702
    for line in f:
703
      # Ignore whitespace changes
704
      if line.split() == key_fields:
705
        break
706
      nl = line.endswith('\n')
707
    else:
708
      if not nl:
709
        f.write("\n")
710
      f.write(key.rstrip('\r\n'))
711
      f.write("\n")
712
      f.flush()
713
  finally:
714
    f.close()
715

    
716

    
717
def RemoveAuthorizedKey(file_name, key):
718
  """Removes an SSH public key from an authorized_keys file.
719

720
  Args:
721
    file_name: Path to authorized_keys file
722
    key: String containing key
723
  """
724
  key_fields = key.split()
725

    
726
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
727
  try:
728
    out = os.fdopen(fd, 'w')
729
    try:
730
      f = open(file_name, 'r')
731
      try:
732
        for line in f:
733
          # Ignore whitespace changes while comparing lines
734
          if line.split() != key_fields:
735
            out.write(line)
736

    
737
        out.flush()
738
        os.rename(tmpname, file_name)
739
      finally:
740
        f.close()
741
    finally:
742
      out.close()
743
  except:
744
    RemoveFile(tmpname)
745
    raise
746

    
747

    
748
def CreateBackup(file_name):
749
  """Creates a backup of a file.
750

751
  Returns: the path to the newly created backup file.
752

753
  """
754
  if not os.path.isfile(file_name):
755
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
756
                                file_name)
757

    
758
  prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
759
  dir_name = os.path.dirname(file_name)
760

    
761
  fsrc = open(file_name, 'rb')
762
  try:
763
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
764
    fdst = os.fdopen(fd, 'wb')
765
    try:
766
      shutil.copyfileobj(fsrc, fdst)
767
    finally:
768
      fdst.close()
769
  finally:
770
    fsrc.close()
771

    
772
  return backup_name
773

    
774

    
775
def ShellQuote(value):
776
  """Quotes shell argument according to POSIX.
777

778
  """
779
  if _re_shell_unquoted.match(value):
780
    return value
781
  else:
782
    return "'%s'" % value.replace("'", "'\\''")
783

    
784

    
785
def ShellQuoteArgs(args):
786
  """Quotes all given shell arguments and concatenates using spaces.
787

788
  """
789
  return ' '.join([ShellQuote(i) for i in args])
790

    
791

    
792

    
793
def TcpPing(source, target, port, timeout=10, live_port_needed=False):
794
  """Simple ping implementation using TCP connect(2).
795

796
  Try to do a TCP connect(2) from the specified source IP to the specified
797
  target IP and the specified target port. If live_port_needed is set to true,
798
  requires the remote end to accept the connection. The timeout is specified
799
  in seconds and defaults to 10 seconds
800

801
  """
802
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
803

    
804
  sucess = False
805

    
806
  try:
807
    sock.bind((source, 0))
808
  except socket.error, (errcode, errstring):
809
    if errcode == errno.EADDRNOTAVAIL:
810
      success = False
811

    
812
  sock.settimeout(timeout)
813

    
814
  try:
815
    sock.connect((target, port))
816
    sock.close()
817
    success = True
818
  except socket.timeout:
819
    success = False
820
  except socket.error, (errcode, errstring):
821
    success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
822

    
823
  return success
824

    
825

    
826
def ListVisibleFiles(path):
827
  """Returns a list of all visible files in a directory.
828

829
  """
830
  return [i for i in os.listdir(path) if not i.startswith(".")]
831

    
832

    
833
def GetHomeDir(user, default=None):
834
  """Try to get the homedir of the given user.
835

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

840
  """
841
  try:
842
    if isinstance(user, basestring):
843
      result = pwd.getpwnam(user)
844
    elif isinstance(user, (int, long)):
845
      result = pwd.getpwuid(user)
846
    else:
847
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
848
                                   type(user))
849
  except KeyError:
850
    return default
851
  return result.pw_dir
852

    
853

    
854
def NewUUID():
855
  """Returns a random UUID.
856

857
  """
858
  f = open("/proc/sys/kernel/random/uuid", "r")
859
  try:
860
    return f.read(128).rstrip("\n")
861
  finally:
862
    f.close()
863

    
864

    
865
def WriteFile(file_name, fn=None, data=None,
866
              mode=None, uid=-1, gid=-1,
867
              atime=None, mtime=None):
868
  """(Over)write a file atomically.
869

870
  The file_name and either fn (a function taking one argument, the
871
  file descriptor, and which should write the data to it) or data (the
872
  contents of the file) must be passed. The other arguments are
873
  optional and allow setting the file mode, owner and group, and the
874
  mtime/atime of the file.
875

876
  If the function doesn't raise an exception, it has succeeded and the
877
  target file has the new contents. If the file has raised an
878
  exception, an existing target file should be unmodified and the
879
  temporary file should be removed.
880

881
  """
882
  if not os.path.isabs(file_name):
883
    raise errors.ProgrammerError("Path passed to WriteFile is not"
884
                                 " absolute: '%s'" % file_name)
885

    
886
  if [fn, data].count(None) != 1:
887
    raise errors.ProgrammerError("fn or data required")
888

    
889
  if [atime, mtime].count(None) == 1:
890
    raise errors.ProgrammerError("Both atime and mtime must be either"
891
                                 " set or None")
892

    
893

    
894
  dir_name, base_name = os.path.split(file_name)
895
  fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
896
  # here we need to make sure we remove the temp file, if any error
897
  # leaves it in place
898
  try:
899
    if uid != -1 or gid != -1:
900
      os.chown(new_name, uid, gid)
901
    if mode:
902
      os.chmod(new_name, mode)
903
    if data is not None:
904
      os.write(fd, data)
905
    else:
906
      fn(fd)
907
    os.fsync(fd)
908
    if atime is not None and mtime is not None:
909
      os.utime(new_name, (atime, mtime))
910
    os.rename(new_name, file_name)
911
  finally:
912
    os.close(fd)
913
    RemoveFile(new_name)
914

    
915

    
916
def all(seq, pred=bool):
917
  "Returns True if pred(x) is True for every element in the iterable"
918
  for elem in itertools.ifilterfalse(pred, seq):
919
    return False
920
  return True
921

    
922

    
923
def any(seq, pred=bool):
924
  "Returns True if pred(x) is True for at least one element in the iterable"
925
  for elem in itertools.ifilter(pred, seq):
926
    return True
927
  return False