Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ 3aecd2c7

History | View | Annotate | Download (25.5 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

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

    
40
from ganeti import logger
41
from ganeti import errors
42
from ganeti import constants
43

    
44

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

    
48
debug = False
49

    
50
class RunResult(object):
51
  """Simple class for holding the result of running external programs.
52

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

64
  """
65
  __slots__ = ["exit_code", "signal", "stdout", "stderr",
66
               "failed", "fail_reason", "cmd"]
67

    
68

    
69
  def __init__(self, exit_code, signal, stdout, stderr, cmd):
70
    self.cmd = cmd
71
    self.exit_code = exit_code
72
    self.signal = signal
73
    self.stdout = stdout
74
    self.stderr = stderr
75
    self.failed = (signal is not None or exit_code != 0)
76

    
77
    if self.signal is not None:
78
      self.fail_reason = "terminated by signal %s" % self.signal
79
    elif self.exit_code is not None:
80
      self.fail_reason = "exited with exit code %s" % self.exit_code
81
    else:
82
      self.fail_reason = "unable to determine termination reason"
83

    
84
    if debug and self.failed:
85
      logger.Debug("Command '%s' failed (%s); output: %s" %
86
                   (self.cmd, self.fail_reason, self.output))
87

    
88
  def _GetOutput(self):
89
    """Returns the combined stdout and stderr for easier usage.
90

91
    """
92
    return self.stdout + self.stderr
93

    
94
  output = property(_GetOutput, None, None, "Return full output")
95

    
96

    
97
def _GetLockFile(subsystem):
98
  """Compute the file name for a given lock name."""
99
  return "%s/ganeti_lock_%s" % (constants.LOCK_DIR, subsystem)
100

    
101

    
102
def Lock(name, max_retries=None, debug=False):
103
  """Lock a given subsystem.
104

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

108
  When the optional integer argument 'max_retries' is passed with a
109
  non-zero value, the function will sleep only for this number of
110
  times, and then it will will raise a LockError if the lock can't be
111
  acquired. Passing in a negative number will cause only one try to
112
  get the lock. Passing a positive number will make the function retry
113
  for approximately that number of seconds.
114

115
  """
116
  lockfile = _GetLockFile(name)
117

    
118
  if name in _locksheld:
119
    raise errors.LockError('Lock "%s" already held!' % (name,))
120

    
121
  errcount = 0
122

    
123
  retries = 0
124
  while True:
125
    try:
126
      fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
127
      break
128
    except OSError, creat_err:
129
      if creat_err.errno != errno.EEXIST:
130
        raise errors.LockError("Can't create the lock file. Error '%s'." %
131
                               str(creat_err))
132

    
133
      try:
134
        pf = open(lockfile, 'r')
135
      except IOError, open_err:
136
        errcount += 1
137
        if errcount >= 5:
138
          raise errors.LockError("Lock file exists but cannot be opened."
139
                                 " Error: '%s'." % str(open_err))
140
        time.sleep(1)
141
        continue
142

    
143
      try:
144
        pid = int(pf.read())
145
      except ValueError:
146
        raise errors.LockError("Invalid pid string in %s" %
147
                               (lockfile,))
148

    
149
      if not IsProcessAlive(pid):
150
        raise errors.LockError("Stale lockfile %s for pid %d?" %
151
                               (lockfile, pid))
152

    
153
      if max_retries and max_retries <= retries:
154
        raise errors.LockError("Can't acquire lock during the specified"
155
                               " time, aborting.")
156
      if retries == 5 and (debug or sys.stdin.isatty()):
157
        logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
158

    
159
      time.sleep(1)
160
      retries += 1
161
      continue
162

    
163
  os.write(fd, '%d\n' % (os.getpid(),))
164
  os.close(fd)
165

    
166
  _locksheld.append(name)
167

    
168

    
169
def Unlock(name):
170
  """Unlock a given subsystem.
171

172
  """
173
  lockfile = _GetLockFile(name)
174

    
175
  try:
176
    fd = os.open(lockfile, os.O_RDONLY)
177
  except OSError:
178
    raise errors.LockError('Lock "%s" not held.' % (name,))
179

    
180
  f = os.fdopen(fd, 'r')
181
  pid_str = f.read()
182

    
183
  try:
184
    pid = int(pid_str)
185
  except ValueError:
186
    raise errors.LockError('Unable to determine PID of locking process.')
187

    
188
  if pid != os.getpid():
189
    raise errors.LockError('Lock not held by me (%d != %d)' %
190
                           (os.getpid(), pid,))
191

    
192
  os.unlink(lockfile)
193
  _locksheld.remove(name)
194

    
195

    
196
def LockCleanup():
197
  """Remove all locks.
198

199
  """
200
  for lock in _locksheld:
201
    Unlock(lock)
202

    
203

    
204
def RunCmd(cmd):
205
  """Execute a (shell) command.
206

207
  The command should not read from its standard input, as it will be
208
  closed.
209

210
  Args:
211
    cmd: command to run. (str)
212

213
  Returns: `RunResult` instance
214

215
  """
216
  if isinstance(cmd, list):
217
    cmd = [str(val) for val in cmd]
218
    strcmd = " ".join(cmd)
219
    shell = False
220
  else:
221
    strcmd = cmd
222
    shell = True
223
  env = os.environ.copy()
224
  env["LC_ALL"] = "C"
225
  child = subprocess.Popen(cmd, shell=shell,
226
                           stderr=subprocess.PIPE,
227
                           stdout=subprocess.PIPE,
228
                           stdin=subprocess.PIPE,
229
                           close_fds=True, env=env)
230

    
231
  child.stdin.close()
232
  out = child.stdout.read()
233
  err = child.stderr.read()
234

    
235
  status = child.wait()
236
  if status >= 0:
237
    exitcode = status
238
    signal = None
239
  else:
240
    exitcode = None
241
    signal = -status
242

    
243
  return RunResult(exitcode, signal, out, err, strcmd)
244

    
245

    
246
def RunCmdUnlocked(cmd):
247
  """Execute a shell command without the 'cmd' lock.
248

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

253
  The argument and return values are the same as for the `RunCmd()`
254
  function.
255

256
  Args:
257
    cmd - command to run. (str)
258

259
  Returns:
260
    `RunResult`
261

262
  """
263
  Unlock('cmd')
264
  ret = RunCmd(cmd)
265
  Lock('cmd')
266

    
267
  return ret
268

    
269

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

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

276
  """
277
  try:
278
    os.unlink(filename)
279
  except OSError, err:
280
    if err.errno not in (errno.ENOENT, errno.EISDIR):
281
      raise
282

    
283

    
284
def _FingerprintFile(filename):
285
  """Compute the fingerprint of a file.
286

287
  If the file does not exist, a None will be returned
288
  instead.
289

290
  Args:
291
    filename - Filename (str)
292

293
  """
294
  if not (os.path.exists(filename) and os.path.isfile(filename)):
295
    return None
296

    
297
  f = open(filename)
298

    
299
  fp = sha.sha()
300
  while True:
301
    data = f.read(4096)
302
    if not data:
303
      break
304

    
305
    fp.update(data)
306

    
307
  return fp.hexdigest()
308

    
309

    
310
def FingerprintFiles(files):
311
  """Compute fingerprints for a list of files.
312

313
  Args:
314
    files - array of filenames.  ( [str, ...] )
315

316
  Return value:
317
    dictionary of filename: fingerprint for the files that exist
318

319
  """
320
  ret = {}
321

    
322
  for filename in files:
323
    cksum = _FingerprintFile(filename)
324
    if cksum:
325
      ret[filename] = cksum
326

    
327
  return ret
328

    
329

    
330
def CheckDict(target, template, logname=None):
331
  """Ensure a dictionary has a required set of keys.
332

333
  For the given dictionaries `target` and `template`, ensure target
334
  has all the keys from template. Missing keys are added with values
335
  from template.
336

337
  Args:
338
    target   - the dictionary to check
339
    template - template dictionary
340
    logname  - a caller-chosen string to identify the debug log
341
               entry; if None, no logging will be done
342

343
  Returns value:
344
    None
345

346
  """
347
  missing = []
348
  for k in template:
349
    if k not in target:
350
      missing.append(k)
351
      target[k] = template[k]
352

    
353
  if missing and logname:
354
    logger.Debug('%s missing keys %s' %
355
                 (logname, ', '.join(missing)))
356

    
357

    
358
def IsProcessAlive(pid):
359
  """Check if a given pid exists on the system.
360

361
  Returns: true or false, depending on if the pid exists or not
362

363
  Remarks: zombie processes treated as not alive
364

365
  """
366
  try:
367
    f = open("/proc/%d/status" % pid)
368
  except IOError, err:
369
    if err.errno in (errno.ENOENT, errno.ENOTDIR):
370
      return False
371

    
372
  alive = True
373
  try:
374
    data = f.readlines()
375
    if len(data) > 1:
376
      state = data[1].split()
377
      if len(state) > 1 and state[1] == "Z":
378
        alive = False
379
  finally:
380
    f.close()
381

    
382
  return alive
383

    
384

    
385
def MatchNameComponent(key, name_list):
386
  """Try to match a name against a list.
387

388
  This function will try to match a name like test1 against a list
389
  like ['test1.example.com', 'test2.example.com', ...]. Against this
390
  list, 'test1' as well as 'test1.example' will match, but not
391
  'test1.ex'. A multiple match will be considered as no match at all
392
  (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
393

394
  Args:
395
    key: the name to be searched
396
    name_list: the list of strings against which to search the key
397

398
  Returns:
399
    None if there is no match *or* if there are multiple matches
400
    otherwise the element from the list which matches
401

402
  """
403
  mo = re.compile("^%s(\..*)?$" % re.escape(key))
404
  names_filtered = [name for name in name_list if mo.match(name) is not None]
405
  if len(names_filtered) != 1:
406
    return None
407
  return names_filtered[0]
408

    
409

    
410
class HostInfo:
411
  """Class implementing resolver and hostname functionality
412

413
  """
414
  def __init__(self, name=None):
415
    """Initialize the host name object.
416

417
    If the name argument is not passed, it will use this system's
418
    name.
419

420
    """
421
    if name is None:
422
      name = self.SysName()
423

    
424
    self.query = name
425
    self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
426
    self.ip = self.ipaddrs[0]
427

    
428
  def ShortName(self):
429
    """Returns the hostname without domain.
430

431
    """
432
    return self.name.split('.')[0]
433

    
434
  @staticmethod
435
  def SysName():
436
    """Return the current system's name.
437

438
    This is simply a wrapper over socket.gethostname()
439

440
    """
441
    return socket.gethostname()
442

    
443
  @staticmethod
444
  def LookupHostname(hostname):
445
    """Look up hostname
446

447
    Args:
448
      hostname: hostname to look up
449

450
    Returns:
451
      a tuple (name, aliases, ipaddrs) as returned by socket.gethostbyname_ex
452
      in case of errors in resolving, we raise a ResolverError
453

454
    """
455
    try:
456
      result = socket.gethostbyname_ex(hostname)
457
    except socket.gaierror, err:
458
      # hostname not found in DNS
459
      raise errors.ResolverError(hostname, err.args[0], err.args[1])
460

    
461
    return result
462

    
463

    
464
def ListVolumeGroups():
465
  """List volume groups and their size
466

467
  Returns:
468
     Dictionary with keys volume name and values the size of the volume
469

470
  """
471
  command = "vgs --noheadings --units m --nosuffix -o name,size"
472
  result = RunCmd(command)
473
  retval = {}
474
  if result.failed:
475
    return retval
476

    
477
  for line in result.stdout.splitlines():
478
    try:
479
      name, size = line.split()
480
      size = int(float(size))
481
    except (IndexError, ValueError), err:
482
      logger.Error("Invalid output from vgs (%s): %s" % (err, line))
483
      continue
484

    
485
    retval[name] = size
486

    
487
  return retval
488

    
489

    
490
def BridgeExists(bridge):
491
  """Check whether the given bridge exists in the system
492

493
  Returns:
494
     True if it does, false otherwise.
495

496
  """
497
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
498

    
499

    
500
def NiceSort(name_list):
501
  """Sort a list of strings based on digit and non-digit groupings.
502

503
  Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
504
  sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
505

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

510
  Return value
511
    - a copy of the list sorted according to our algorithm
512

513
  """
514
  _SORTER_BASE = "(\D+|\d+)"
515
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
516
                                                  _SORTER_BASE, _SORTER_BASE,
517
                                                  _SORTER_BASE, _SORTER_BASE,
518
                                                  _SORTER_BASE, _SORTER_BASE)
519
  _SORTER_RE = re.compile(_SORTER_FULL)
520
  _SORTER_NODIGIT = re.compile("^\D*$")
521
  def _TryInt(val):
522
    """Attempts to convert a variable to integer."""
523
    if val is None or _SORTER_NODIGIT.match(val):
524
      return val
525
    rval = int(val)
526
    return rval
527

    
528
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
529
             for name in name_list]
530
  to_sort.sort()
531
  return [tup[1] for tup in to_sort]
532

    
533

    
534
def CheckDaemonAlive(pid_file, process_string):
535
  """Check wether the specified daemon is alive.
536

537
  Args:
538
   - pid_file: file to read the daemon pid from, the file is
539
               expected to contain only a single line containing
540
               only the PID
541
   - process_string: a substring that we expect to find in
542
                     the command line of the daemon process
543

544
  Returns:
545
   - True if the daemon is judged to be alive (that is:
546
      - the PID file exists, is readable and contains a number
547
      - a process of the specified PID is running
548
      - that process contains the specified string in its
549
        command line
550
      - the process is not in state Z (zombie))
551
   - False otherwise
552

553
  """
554
  try:
555
    pid_file = file(pid_file, 'r')
556
    try:
557
      pid = int(pid_file.readline())
558
    finally:
559
      pid_file.close()
560

    
561
    cmdline_file_path = "/proc/%s/cmdline" % (pid)
562
    cmdline_file = open(cmdline_file_path, 'r')
563
    try:
564
      cmdline = cmdline_file.readline()
565
    finally:
566
      cmdline_file.close()
567

    
568
    if not process_string in cmdline:
569
      return False
570

    
571
    stat_file_path =  "/proc/%s/stat" % (pid)
572
    stat_file = open(stat_file_path, 'r')
573
    try:
574
      process_state = stat_file.readline().split()[2]
575
    finally:
576
      stat_file.close()
577

    
578
    if process_state == 'Z':
579
      return False
580

    
581
  except (IndexError, IOError, ValueError):
582
    return False
583

    
584
  return True
585

    
586

    
587
def TryConvert(fn, val):
588
  """Try to convert a value ignoring errors.
589

590
  This function tries to apply function `fn` to `val`. If no
591
  ValueError or TypeError exceptions are raised, it will return the
592
  result, else it will return the original value. Any other exceptions
593
  are propagated to the caller.
594

595
  """
596
  try:
597
    nv = fn(val)
598
  except (ValueError, TypeError), err:
599
    nv = val
600
  return nv
601

    
602

    
603
def IsValidIP(ip):
604
  """Verifies the syntax of an IP address.
605

606
  This function checks if the ip address passes is valid or not based
607
  on syntax (not ip range, class calculations or anything).
608

609
  """
610
  unit = "(0|[1-9]\d{0,2})"
611
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
612

    
613

    
614
def IsValidShellParam(word):
615
  """Verifies is the given word is safe from the shell's p.o.v.
616

617
  This means that we can pass this to a command via the shell and be
618
  sure that it doesn't alter the command line and is passed as such to
619
  the actual command.
620

621
  Note that we are overly restrictive here, in order to be on the safe
622
  side.
623

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

    
627

    
628
def BuildShellCmd(template, *args):
629
  """Build a safe shell command line from the given arguments.
630

631
  This function will check all arguments in the args list so that they
632
  are valid shell parameters (i.e. they don't contain shell
633
  metacharaters). If everything is ok, it will return the result of
634
  template % args.
635

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

    
643

    
644
def FormatUnit(value):
645
  """Formats an incoming number of MiB with the appropriate unit.
646

647
  Value needs to be passed as a numeric type. Return value is always a string.
648

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

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

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

    
659

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

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

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

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

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

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

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

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

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

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

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

    
701
  return value
702

    
703

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

707
  Args:
708
    file_name: Path to authorized_keys file
709
    key: String containing key
710
  """
711
  key_fields = key.split()
712

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

    
730

    
731
def RemoveAuthorizedKey(file_name, key):
732
  """Removes an SSH public key from an authorized_keys file.
733

734
  Args:
735
    file_name: Path to authorized_keys file
736
    key: String containing key
737
  """
738
  key_fields = key.split()
739

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

    
751
        out.flush()
752
        os.rename(tmpname, file_name)
753
      finally:
754
        f.close()
755
    finally:
756
      out.close()
757
  except:
758
    RemoveFile(tmpname)
759
    raise
760

    
761

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

765
  """
766
  # Ensure aliases are unique
767
  aliases = UniqueSequence([hostname] + aliases)[1:]
768

    
769
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
770
  try:
771
    out = os.fdopen(fd, 'w')
772
    try:
773
      f = open(file_name, 'r')
774
      try:
775
        written = False
776
        for line in f:
777
          fields = line.split()
778
          if fields and not fields[0].startswith('#') and ip == fields[0]:
779
            continue
780
          out.write(line)
781

    
782
        out.write("%s\t%s" % (ip, hostname))
783
        if aliases:
784
          out.write(" %s" % ' '.join(aliases))
785
        out.write('\n')
786

    
787
        out.flush()
788
        os.fsync(out)
789
        os.rename(tmpname, file_name)
790
      finally:
791
        f.close()
792
    finally:
793
      out.close()
794
  except:
795
    RemoveFile(tmpname)
796
    raise
797

    
798

    
799
def RemoveEtcHostsEntry(file_name, hostname):
800
  """Removes a hostname from /etc/hosts.
801

802
  IP addresses without names are removed from the file.
803
  """
804
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
805
  try:
806
    out = os.fdopen(fd, 'w')
807
    try:
808
      f = open(file_name, 'r')
809
      try:
810
        for line in f:
811
          fields = line.split()
812
          if len(fields) > 1 and not fields[0].startswith('#'):
813
            names = fields[1:]
814
            if hostname in names:
815
              while hostname in names:
816
                names.remove(hostname)
817
              if names:
818
                out.write("%s %s\n" % (fields[0], ' '.join(names)))
819
              continue
820

    
821
          out.write(line)
822

    
823
        out.flush()
824
        os.fsync(out)
825
        os.rename(tmpname, file_name)
826
      finally:
827
        f.close()
828
    finally:
829
      out.close()
830
  except:
831
    RemoveFile(tmpname)
832
    raise
833

    
834

    
835
def CreateBackup(file_name):
836
  """Creates a backup of a file.
837

838
  Returns: the path to the newly created backup file.
839

840
  """
841
  if not os.path.isfile(file_name):
842
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
843
                                file_name)
844

    
845
  prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
846
  dir_name = os.path.dirname(file_name)
847

    
848
  fsrc = open(file_name, 'rb')
849
  try:
850
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
851
    fdst = os.fdopen(fd, 'wb')
852
    try:
853
      shutil.copyfileobj(fsrc, fdst)
854
    finally:
855
      fdst.close()
856
  finally:
857
    fsrc.close()
858

    
859
  return backup_name
860

    
861

    
862
def ShellQuote(value):
863
  """Quotes shell argument according to POSIX.
864

865
  """
866
  if _re_shell_unquoted.match(value):
867
    return value
868
  else:
869
    return "'%s'" % value.replace("'", "'\\''")
870

    
871

    
872
def ShellQuoteArgs(args):
873
  """Quotes all given shell arguments and concatenates using spaces.
874

875
  """
876
  return ' '.join([ShellQuote(i) for i in args])
877

    
878

    
879

    
880
def TcpPing(source, target, port, timeout=10, live_port_needed=False):
881
  """Simple ping implementation using TCP connect(2).
882

883
  Try to do a TCP connect(2) from the specified source IP to the specified
884
  target IP and the specified target port. If live_port_needed is set to true,
885
  requires the remote end to accept the connection. The timeout is specified
886
  in seconds and defaults to 10 seconds
887

888
  """
889
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
890

    
891
  sucess = False
892

    
893
  try:
894
    sock.bind((source, 0))
895
  except socket.error, (errcode, errstring):
896
    if errcode == errno.EADDRNOTAVAIL:
897
      success = False
898

    
899
  sock.settimeout(timeout)
900

    
901
  try:
902
    sock.connect((target, port))
903
    sock.close()
904
    success = True
905
  except socket.timeout:
906
    success = False
907
  except socket.error, (errcode, errstring):
908
    success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
909

    
910
  return success
911

    
912

    
913
def ListVisibleFiles(path):
914
  """Returns a list of all visible files in a directory.
915

916
  """
917
  files = [i for i in os.listdir(path) if not i.startswith(".")]
918
  files.sort()
919
  return files
920

    
921

    
922
def GetHomeDir(user, default=None):
923
  """Try to get the homedir of the given user.
924

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

929
  """
930
  try:
931
    if isinstance(user, basestring):
932
      result = pwd.getpwnam(user)
933
    elif isinstance(user, (int, long)):
934
      result = pwd.getpwuid(user)
935
    else:
936
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
937
                                   type(user))
938
  except KeyError:
939
    return default
940
  return result.pw_dir
941

    
942

    
943
def NewUUID():
944
  """Returns a random UUID.
945

946
  """
947
  f = open("/proc/sys/kernel/random/uuid", "r")
948
  try:
949
    return f.read(128).rstrip("\n")
950
  finally:
951
    f.close()
952

    
953

    
954
def WriteFile(file_name, fn=None, data=None,
955
              mode=None, uid=-1, gid=-1,
956
              atime=None, mtime=None):
957
  """(Over)write a file atomically.
958

959
  The file_name and either fn (a function taking one argument, the
960
  file descriptor, and which should write the data to it) or data (the
961
  contents of the file) must be passed. The other arguments are
962
  optional and allow setting the file mode, owner and group, and the
963
  mtime/atime of the file.
964

965
  If the function doesn't raise an exception, it has succeeded and the
966
  target file has the new contents. If the file has raised an
967
  exception, an existing target file should be unmodified and the
968
  temporary file should be removed.
969

970
  """
971
  if not os.path.isabs(file_name):
972
    raise errors.ProgrammerError("Path passed to WriteFile is not"
973
                                 " absolute: '%s'" % file_name)
974

    
975
  if [fn, data].count(None) != 1:
976
    raise errors.ProgrammerError("fn or data required")
977

    
978
  if [atime, mtime].count(None) == 1:
979
    raise errors.ProgrammerError("Both atime and mtime must be either"
980
                                 " set or None")
981

    
982

    
983
  dir_name, base_name = os.path.split(file_name)
984
  fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
985
  # here we need to make sure we remove the temp file, if any error
986
  # leaves it in place
987
  try:
988
    if uid != -1 or gid != -1:
989
      os.chown(new_name, uid, gid)
990
    if mode:
991
      os.chmod(new_name, mode)
992
    if data is not None:
993
      os.write(fd, data)
994
    else:
995
      fn(fd)
996
    os.fsync(fd)
997
    if atime is not None and mtime is not None:
998
      os.utime(new_name, (atime, mtime))
999
    os.rename(new_name, file_name)
1000
  finally:
1001
    os.close(fd)
1002
    RemoveFile(new_name)
1003

    
1004

    
1005
def all(seq, pred=bool):
1006
  "Returns True if pred(x) is True for every element in the iterable"
1007
  for elem in itertools.ifilterfalse(pred, seq):
1008
    return False
1009
  return True
1010

    
1011

    
1012
def any(seq, pred=bool):
1013
  "Returns True if pred(x) is True for at least one element in the iterable"
1014
  for elem in itertools.ifilter(pred, seq):
1015
    return True
1016
  return False
1017

    
1018

    
1019
def UniqueSequence(seq):
1020
  """Returns a list with unique elements.
1021

1022
  Element order is preserved.
1023
  """
1024
  seen = set()
1025
  return [i for i in seq if i not in seen and not seen.add(i)]