Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ b6522276

History | View | Annotate | Download (25.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 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
"""Utility functions for I/O.
22

23
"""
24

    
25
import os
26
import logging
27
import shutil
28
import tempfile
29
import errno
30
import time
31
import stat
32

    
33
from ganeti import errors
34
from ganeti import constants
35
from ganeti.utils import filelock
36

    
37

    
38
#: Path generating random UUID
39
_RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
40

    
41
#: Directory used by fsck(8) to store recovered data, usually at a file
42
#: system's root directory
43
_LOST_AND_FOUND = "lost+found"
44

    
45

    
46
def ReadFile(file_name, size=-1, preread=None):
47
  """Reads a file.
48

49
  @type size: int
50
  @param size: Read at most size bytes (if negative, entire file)
51
  @type preread: callable receiving file handle as single parameter
52
  @param preread: Function called before file is read
53
  @rtype: str
54
  @return: the (possibly partial) content of the file
55

56
  """
57
  f = open(file_name, "r")
58
  try:
59
    if preread:
60
      preread(f)
61

    
62
    return f.read(size)
63
  finally:
64
    f.close()
65

    
66

    
67
def WriteFile(file_name, fn=None, data=None,
68
              mode=None, uid=-1, gid=-1,
69
              atime=None, mtime=None, close=True,
70
              dry_run=False, backup=False,
71
              prewrite=None, postwrite=None):
72
  """(Over)write a file atomically.
73

74
  The file_name and either fn (a function taking one argument, the
75
  file descriptor, and which should write the data to it) or data (the
76
  contents of the file) must be passed. The other arguments are
77
  optional and allow setting the file mode, owner and group, and the
78
  mtime/atime of the file.
79

80
  If the function doesn't raise an exception, it has succeeded and the
81
  target file has the new contents. If the function has raised an
82
  exception, an existing target file should be unmodified and the
83
  temporary file should be removed.
84

85
  @type file_name: str
86
  @param file_name: the target filename
87
  @type fn: callable
88
  @param fn: content writing function, called with
89
      file descriptor as parameter
90
  @type data: str
91
  @param data: contents of the file
92
  @type mode: int
93
  @param mode: file mode
94
  @type uid: int
95
  @param uid: the owner of the file
96
  @type gid: int
97
  @param gid: the group of the file
98
  @type atime: int
99
  @param atime: a custom access time to be set on the file
100
  @type mtime: int
101
  @param mtime: a custom modification time to be set on the file
102
  @type close: boolean
103
  @param close: whether to close file after writing it
104
  @type prewrite: callable
105
  @param prewrite: function to be called before writing content
106
  @type postwrite: callable
107
  @param postwrite: function to be called after writing content
108

109
  @rtype: None or int
110
  @return: None if the 'close' parameter evaluates to True,
111
      otherwise the file descriptor
112

113
  @raise errors.ProgrammerError: if any of the arguments are not valid
114

115
  """
116
  if not os.path.isabs(file_name):
117
    raise errors.ProgrammerError("Path passed to WriteFile is not"
118
                                 " absolute: '%s'" % file_name)
119

    
120
  if [fn, data].count(None) != 1:
121
    raise errors.ProgrammerError("fn or data required")
122

    
123
  if [atime, mtime].count(None) == 1:
124
    raise errors.ProgrammerError("Both atime and mtime must be either"
125
                                 " set or None")
126

    
127
  if backup and not dry_run and os.path.isfile(file_name):
128
    CreateBackup(file_name)
129

    
130
  # Whether temporary file needs to be removed (e.g. if any error occurs)
131
  do_remove = True
132

    
133
  # Function result
134
  result = None
135

    
136
  (dir_name, base_name) = os.path.split(file_name)
137
  (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
138
                                    dir=dir_name)
139
  try:
140
    try:
141
      if uid != -1 or gid != -1:
142
        os.chown(new_name, uid, gid)
143
      if mode:
144
        os.chmod(new_name, mode)
145
      if callable(prewrite):
146
        prewrite(fd)
147
      if data is not None:
148
        if isinstance(data, unicode):
149
          data = data.encode()
150
        assert isinstance(data, str)
151
        to_write = len(data)
152
        offset = 0
153
        while offset < to_write:
154
          written = os.write(fd, buffer(data, offset))
155
          assert written >= 0
156
          assert written <= to_write - offset
157
          offset += written
158
        assert offset == to_write
159
      else:
160
        fn(fd)
161
      if callable(postwrite):
162
        postwrite(fd)
163
      os.fsync(fd)
164
      if atime is not None and mtime is not None:
165
        os.utime(new_name, (atime, mtime))
166
    finally:
167
      # Close file unless the file descriptor should be returned
168
      if close:
169
        os.close(fd)
170
      else:
171
        result = fd
172

    
173
    # Rename file to destination name
174
    if not dry_run:
175
      os.rename(new_name, file_name)
176
      # Successful, no need to remove anymore
177
      do_remove = False
178
  finally:
179
    if do_remove:
180
      RemoveFile(new_name)
181

    
182
  return result
183

    
184

    
185
def GetFileID(path=None, fd=None):
186
  """Returns the file 'id', i.e. the dev/inode and mtime information.
187

188
  Either the path to the file or the fd must be given.
189

190
  @param path: the file path
191
  @param fd: a file descriptor
192
  @return: a tuple of (device number, inode number, mtime)
193

194
  """
195
  if [path, fd].count(None) != 1:
196
    raise errors.ProgrammerError("One and only one of fd/path must be given")
197

    
198
  if fd is None:
199
    st = os.stat(path)
200
  else:
201
    st = os.fstat(fd)
202

    
203
  return (st.st_dev, st.st_ino, st.st_mtime)
204

    
205

    
206
def VerifyFileID(fi_disk, fi_ours):
207
  """Verifies that two file IDs are matching.
208

209
  Differences in the inode/device are not accepted, but and older
210
  timestamp for fi_disk is accepted.
211

212
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
213
      file data
214
  @param fi_ours: tuple (dev, inode, mtime) representing the last
215
      written file data
216
  @rtype: boolean
217

218
  """
219
  (d1, i1, m1) = fi_disk
220
  (d2, i2, m2) = fi_ours
221

    
222
  return (d1, i1) == (d2, i2) and m1 <= m2
223

    
224

    
225
def SafeWriteFile(file_name, file_id, **kwargs):
226
  """Wraper over L{WriteFile} that locks the target file.
227

228
  By keeping the target file locked during WriteFile, we ensure that
229
  cooperating writers will safely serialise access to the file.
230

231
  @type file_name: str
232
  @param file_name: the target filename
233
  @type file_id: tuple
234
  @param file_id: a result from L{GetFileID}
235

236
  """
237
  fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
238
  try:
239
    filelock.LockFile(fd)
240
    if file_id is not None:
241
      disk_id = GetFileID(fd=fd)
242
      if not VerifyFileID(disk_id, file_id):
243
        raise errors.LockError("Cannot overwrite file %s, it has been modified"
244
                               " since last written" % file_name)
245
    return WriteFile(file_name, **kwargs)
246
  finally:
247
    os.close(fd)
248

    
249

    
250
def ReadOneLineFile(file_name, strict=False):
251
  """Return the first non-empty line from a file.
252

253
  @type strict: boolean
254
  @param strict: if True, abort if the file has more than one
255
      non-empty line
256

257
  """
258
  file_lines = ReadFile(file_name).splitlines()
259
  full_lines = filter(bool, file_lines)
260
  if not file_lines or not full_lines:
261
    raise errors.GenericError("No data in one-liner file %s" % file_name)
262
  elif strict and len(full_lines) > 1:
263
    raise errors.GenericError("Too many lines in one-liner file %s" %
264
                              file_name)
265
  return full_lines[0]
266

    
267

    
268
def RemoveFile(filename):
269
  """Remove a file ignoring some errors.
270

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

274
  @type filename: str
275
  @param filename: the file to be removed
276

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

    
284

    
285
def RemoveDir(dirname):
286
  """Remove an empty directory.
287

288
  Remove a directory, ignoring non-existing ones.
289
  Other errors are passed. This includes the case,
290
  where the directory is not empty, so it can't be removed.
291

292
  @type dirname: str
293
  @param dirname: the empty directory to be removed
294

295
  """
296
  try:
297
    os.rmdir(dirname)
298
  except OSError, err:
299
    if err.errno != errno.ENOENT:
300
      raise
301

    
302

    
303
def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
304
               dir_gid=None):
305
  """Renames a file.
306

307
  This just creates the very least directory if it does not exist and C{mkdir}
308
  is set to true.
309

310
  @type old: string
311
  @param old: Original path
312
  @type new: string
313
  @param new: New path
314
  @type mkdir: bool
315
  @param mkdir: Whether to create target directory if it doesn't exist
316
  @type mkdir_mode: int
317
  @param mkdir_mode: Mode for newly created directories
318
  @type dir_uid: int
319
  @param dir_uid: The uid for the (if fresh created) dir
320
  @type dir_gid: int
321
  @param dir_gid: The gid for the (if fresh created) dir
322

323
  """
324
  try:
325
    return os.rename(old, new)
326
  except OSError, err:
327
    # In at least one use case of this function, the job queue, directory
328
    # creation is very rare. Checking for the directory before renaming is not
329
    # as efficient.
330
    if mkdir and err.errno == errno.ENOENT:
331
      # Create directory and try again
332
      dir_path = os.path.dirname(new)
333
      MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
334

    
335
      return os.rename(old, new)
336

    
337
    raise
338

    
339

    
340
def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
341
                      _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
342
  """Enforces that given path has given permissions.
343

344
  @param path: The path to the file
345
  @param mode: The mode of the file
346
  @param uid: The uid of the owner of this file
347
  @param gid: The gid of the owner of this file
348
  @param must_exist: Specifies if non-existance of path will be an error
349
  @param _chmod_fn: chmod function to use (unittest only)
350
  @param _chown_fn: chown function to use (unittest only)
351

352
  """
353
  logging.debug("Checking %s", path)
354

    
355
  # chown takes -1 if you want to keep one part of the ownership, however
356
  # None is Python standard for that. So we remap them here.
357
  if uid is None:
358
    uid = -1
359
  if gid is None:
360
    gid = -1
361

    
362
  try:
363
    st = _stat_fn(path)
364

    
365
    fmode = stat.S_IMODE(st[stat.ST_MODE])
366
    if fmode != mode:
367
      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
368
      _chmod_fn(path, mode)
369

    
370
    if max(uid, gid) > -1:
371
      fuid = st[stat.ST_UID]
372
      fgid = st[stat.ST_GID]
373
      if fuid != uid or fgid != gid:
374
        logging.debug("Changing owner of %s from UID %s/GID %s to"
375
                      " UID %s/GID %s", path, fuid, fgid, uid, gid)
376
        _chown_fn(path, uid, gid)
377
  except EnvironmentError, err:
378
    if err.errno == errno.ENOENT:
379
      if must_exist:
380
        raise errors.GenericError("Path %s should exist, but does not" % path)
381
    else:
382
      raise errors.GenericError("Error while changing permissions on %s: %s" %
383
                                (path, err))
384

    
385

    
386
def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
387
                    _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
388
  """Enforces that given path is a dir and has given mode, uid and gid set.
389

390
  @param path: The path to the file
391
  @param mode: The mode of the file
392
  @param uid: The uid of the owner of this file
393
  @param gid: The gid of the owner of this file
394
  @param _lstat_fn: Stat function to use (unittest only)
395
  @param _mkdir_fn: mkdir function to use (unittest only)
396
  @param _perm_fn: permission setter function to use (unittest only)
397

398
  """
399
  logging.debug("Checking directory %s", path)
400
  try:
401
    # We don't want to follow symlinks
402
    st = _lstat_fn(path)
403
  except EnvironmentError, err:
404
    if err.errno != errno.ENOENT:
405
      raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
406
    _mkdir_fn(path)
407
  else:
408
    if not stat.S_ISDIR(st[stat.ST_MODE]):
409
      raise errors.GenericError(("Path %s is expected to be a directory, but "
410
                                 "isn't") % path)
411

    
412
  _perm_fn(path, mode, uid=uid, gid=gid)
413

    
414

    
415
def Makedirs(path, mode=0750):
416
  """Super-mkdir; create a leaf directory and all intermediate ones.
417

418
  This is a wrapper around C{os.makedirs} adding error handling not implemented
419
  before Python 2.5.
420

421
  """
422
  try:
423
    os.makedirs(path, mode)
424
  except OSError, err:
425
    # Ignore EEXIST. This is only handled in os.makedirs as included in
426
    # Python 2.5 and above.
427
    if err.errno != errno.EEXIST or not os.path.exists(path):
428
      raise
429

    
430

    
431
def TimestampForFilename():
432
  """Returns the current time formatted for filenames.
433

434
  The format doesn't contain colons as some shells and applications treat them
435
  as separators. Uses the local timezone.
436

437
  """
438
  return time.strftime("%Y-%m-%d_%H_%M_%S")
439

    
440

    
441
def CreateBackup(file_name):
442
  """Creates a backup of a file.
443

444
  @type file_name: str
445
  @param file_name: file to be backed up
446
  @rtype: str
447
  @return: the path to the newly created backup
448
  @raise errors.ProgrammerError: for invalid file names
449

450
  """
451
  if not os.path.isfile(file_name):
452
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
453
                                file_name)
454

    
455
  prefix = ("%s.backup-%s." %
456
            (os.path.basename(file_name), TimestampForFilename()))
457
  dir_name = os.path.dirname(file_name)
458

    
459
  fsrc = open(file_name, "rb")
460
  try:
461
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
462
    fdst = os.fdopen(fd, "wb")
463
    try:
464
      logging.debug("Backing up %s at %s", file_name, backup_name)
465
      shutil.copyfileobj(fsrc, fdst)
466
    finally:
467
      fdst.close()
468
  finally:
469
    fsrc.close()
470

    
471
  return backup_name
472

    
473

    
474
def ListVisibleFiles(path, _is_mountpoint=os.path.ismount):
475
  """Returns a list of visible files in a directory.
476

477
  @type path: str
478
  @param path: the directory to enumerate
479
  @rtype: list
480
  @return: the list of all files not starting with a dot
481
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
482

483
  """
484
  if not IsNormAbsPath(path):
485
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
486
                                 " absolute/normalized: '%s'" % path)
487

    
488
  mountpoint = _is_mountpoint(path)
489

    
490
  def fn(name):
491
    """File name filter.
492

493
    Ignores files starting with a dot (".") as by Unix convention they're
494
    considered hidden. The "lost+found" directory found at the root of some
495
    filesystems is also hidden.
496

497
    """
498
    return not (name.startswith(".") or
499
                (mountpoint and name == _LOST_AND_FOUND and
500
                 os.path.isdir(os.path.join(path, name))))
501

    
502
  return filter(fn, os.listdir(path))
503

    
504

    
505
def EnsureDirs(dirs):
506
  """Make required directories, if they don't exist.
507

508
  @param dirs: list of tuples (dir_name, dir_mode)
509
  @type dirs: list of (string, integer)
510

511
  """
512
  for dir_name, dir_mode in dirs:
513
    try:
514
      os.mkdir(dir_name, dir_mode)
515
    except EnvironmentError, err:
516
      if err.errno != errno.EEXIST:
517
        raise errors.GenericError("Cannot create needed directory"
518
                                  " '%s': %s" % (dir_name, err))
519
    try:
520
      os.chmod(dir_name, dir_mode)
521
    except EnvironmentError, err:
522
      raise errors.GenericError("Cannot change directory permissions on"
523
                                " '%s': %s" % (dir_name, err))
524
    if not os.path.isdir(dir_name):
525
      raise errors.GenericError("%s is not a directory" % dir_name)
526

    
527

    
528
def FindFile(name, search_path, test=os.path.exists):
529
  """Look for a filesystem object in a given path.
530

531
  This is an abstract method to search for filesystem object (files,
532
  dirs) under a given search path.
533

534
  @type name: str
535
  @param name: the name to look for
536
  @type search_path: str
537
  @param search_path: location to start at
538
  @type test: callable
539
  @param test: a function taking one argument that should return True
540
      if the a given object is valid; the default value is
541
      os.path.exists, causing only existing files to be returned
542
  @rtype: str or None
543
  @return: full path to the object if found, None otherwise
544

545
  """
546
  # validate the filename mask
547
  if constants.EXT_PLUGIN_MASK.match(name) is None:
548
    logging.critical("Invalid value passed for external script name: '%s'",
549
                     name)
550
    return None
551

    
552
  for dir_name in search_path:
553
    # FIXME: investigate switch to PathJoin
554
    item_name = os.path.sep.join([dir_name, name])
555
    # check the user test and that we're indeed resolving to the given
556
    # basename
557
    if test(item_name) and os.path.basename(item_name) == name:
558
      return item_name
559
  return None
560

    
561

    
562
def IsNormAbsPath(path):
563
  """Check whether a path is absolute and also normalized
564

565
  This avoids things like /dir/../../other/path to be valid.
566

567
  """
568
  return os.path.normpath(path) == path and os.path.isabs(path)
569

    
570

    
571
def IsBelowDir(root, other_path):
572
  """Check whether a path is below a root dir.
573

574
  This works around the nasty byte-byte comparisation of commonprefix.
575

576
  """
577
  if not (os.path.isabs(root) and os.path.isabs(other_path)):
578
    raise ValueError("Provided paths '%s' and '%s' are not absolute" %
579
                     (root, other_path))
580
  prepared_root = "%s%s" % (os.path.normpath(root), os.sep)
581
  return os.path.commonprefix([prepared_root,
582
                               os.path.normpath(other_path)]) == prepared_root
583

    
584

    
585
def PathJoin(*args):
586
  """Safe-join a list of path components.
587

588
  Requirements:
589
      - the first argument must be an absolute path
590
      - no component in the path must have backtracking (e.g. /../),
591
        since we check for normalization at the end
592

593
  @param args: the path components to be joined
594
  @raise ValueError: for invalid paths
595

596
  """
597
  # ensure we're having at least one path passed in
598
  assert args
599
  # ensure the first component is an absolute and normalized path name
600
  root = args[0]
601
  if not IsNormAbsPath(root):
602
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
603
  result = os.path.join(*args)
604
  # ensure that the whole path is normalized
605
  if not IsNormAbsPath(result):
606
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
607
  # check that we're still under the original prefix
608
  if not IsBelowDir(root, result):
609
    raise ValueError("Error: path joining resulted in different prefix"
610
                     " (%s != %s)" % (result, root))
611
  return result
612

    
613

    
614
def TailFile(fname, lines=20):
615
  """Return the last lines from a file.
616

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

621
  @param fname: the file name
622
  @type lines: int
623
  @param lines: the (maximum) number of lines to return
624

625
  """
626
  fd = open(fname, "r")
627
  try:
628
    fd.seek(0, 2)
629
    pos = fd.tell()
630
    pos = max(0, pos - 4096)
631
    fd.seek(pos, 0)
632
    raw_data = fd.read()
633
  finally:
634
    fd.close()
635

    
636
  rows = raw_data.splitlines()
637
  return rows[-lines:]
638

    
639

    
640
def BytesToMebibyte(value):
641
  """Converts bytes to mebibytes.
642

643
  @type value: int
644
  @param value: Value in bytes
645
  @rtype: int
646
  @return: Value in mebibytes
647

648
  """
649
  return int(round(value / (1024.0 * 1024.0), 0))
650

    
651

    
652
def CalculateDirectorySize(path):
653
  """Calculates the size of a directory recursively.
654

655
  @type path: string
656
  @param path: Path to directory
657
  @rtype: int
658
  @return: Size in mebibytes
659

660
  """
661
  size = 0
662

    
663
  for (curpath, _, files) in os.walk(path):
664
    for filename in files:
665
      st = os.lstat(PathJoin(curpath, filename))
666
      size += st.st_size
667

    
668
  return BytesToMebibyte(size)
669

    
670

    
671
def GetFilesystemStats(path):
672
  """Returns the total and free space on a filesystem.
673

674
  @type path: string
675
  @param path: Path on filesystem to be examined
676
  @rtype: int
677
  @return: tuple of (Total space, Free space) in mebibytes
678

679
  """
680
  st = os.statvfs(path)
681

    
682
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
683
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
684
  return (tsize, fsize)
685

    
686

    
687
def ReadPidFile(pidfile):
688
  """Read a pid from a file.
689

690
  @type  pidfile: string
691
  @param pidfile: path to the file containing the pid
692
  @rtype: int
693
  @return: The process id, if the file exists and contains a valid PID,
694
           otherwise 0
695

696
  """
697
  try:
698
    raw_data = ReadOneLineFile(pidfile)
699
  except EnvironmentError, err:
700
    if err.errno != errno.ENOENT:
701
      logging.exception("Can't read pid file")
702
    return 0
703

    
704
  return _ParsePidFileContents(raw_data)
705

    
706

    
707
def _ParsePidFileContents(data):
708
  """Tries to extract a process ID from a PID file's content.
709

710
  @type data: string
711
  @rtype: int
712
  @return: Zero if nothing could be read, PID otherwise
713

714
  """
715
  try:
716
    pid = int(data)
717
  except (TypeError, ValueError):
718
    logging.info("Can't parse pid file contents", exc_info=True)
719
    return 0
720
  else:
721
    return pid
722

    
723

    
724
def ReadLockedPidFile(path):
725
  """Reads a locked PID file.
726

727
  This can be used together with L{utils.process.StartDaemon}.
728

729
  @type path: string
730
  @param path: Path to PID file
731
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
732

733
  """
734
  try:
735
    fd = os.open(path, os.O_RDONLY)
736
  except EnvironmentError, err:
737
    if err.errno == errno.ENOENT:
738
      # PID file doesn't exist
739
      return None
740
    raise
741

    
742
  try:
743
    try:
744
      # Try to acquire lock
745
      filelock.LockFile(fd)
746
    except errors.LockError:
747
      # Couldn't lock, daemon is running
748
      return int(os.read(fd, 100))
749
  finally:
750
    os.close(fd)
751

    
752
  return None
753

    
754

    
755
def AddAuthorizedKey(file_obj, key):
756
  """Adds an SSH public key to an authorized_keys file.
757

758
  @type file_obj: str or file handle
759
  @param file_obj: path to authorized_keys file
760
  @type key: str
761
  @param key: string containing key
762

763
  """
764
  key_fields = key.split()
765

    
766
  if isinstance(file_obj, basestring):
767
    f = open(file_obj, "a+")
768
  else:
769
    f = file_obj
770

    
771
  try:
772
    nl = True
773
    for line in f:
774
      # Ignore whitespace changes
775
      if line.split() == key_fields:
776
        break
777
      nl = line.endswith("\n")
778
    else:
779
      if not nl:
780
        f.write("\n")
781
      f.write(key.rstrip("\r\n"))
782
      f.write("\n")
783
      f.flush()
784
  finally:
785
    f.close()
786

    
787

    
788
def RemoveAuthorizedKey(file_name, key):
789
  """Removes an SSH public key from an authorized_keys file.
790

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

796
  """
797
  key_fields = key.split()
798

    
799
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
800
  try:
801
    out = os.fdopen(fd, "w")
802
    try:
803
      f = open(file_name, "r")
804
      try:
805
        for line in f:
806
          # Ignore whitespace changes while comparing lines
807
          if line.split() != key_fields:
808
            out.write(line)
809

    
810
        out.flush()
811
        os.rename(tmpname, file_name)
812
      finally:
813
        f.close()
814
    finally:
815
      out.close()
816
  except:
817
    RemoveFile(tmpname)
818
    raise
819

    
820

    
821
def DaemonPidFileName(name):
822
  """Compute a ganeti pid file absolute path
823

824
  @type name: str
825
  @param name: the daemon name
826
  @rtype: str
827
  @return: the full path to the pidfile corresponding to the given
828
      daemon name
829

830
  """
831
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
832

    
833

    
834
def WritePidFile(pidfile):
835
  """Write the current process pidfile.
836

837
  @type pidfile: string
838
  @param pidfile: the path to the file to be written
839
  @raise errors.LockError: if the pid file already exists and
840
      points to a live process
841
  @rtype: int
842
  @return: the file descriptor of the lock file; do not close this unless
843
      you want to unlock the pid file
844

845
  """
846
  # We don't rename nor truncate the file to not drop locks under
847
  # existing processes
848
  fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600)
849

    
850
  # Lock the PID file (and fail if not possible to do so). Any code
851
  # wanting to send a signal to the daemon should try to lock the PID
852
  # file before reading it. If acquiring the lock succeeds, the daemon is
853
  # no longer running and the signal should not be sent.
854
  try:
855
    filelock.LockFile(fd_pidfile)
856
  except errors.LockError:
857
    msg = ["PID file '%s' is already locked by another process" % pidfile]
858
    # Try to read PID file
859
    pid = _ParsePidFileContents(os.read(fd_pidfile, 100))
860
    if pid > 0:
861
      msg.append(", PID read from file is %s" % pid)
862
    raise errors.PidFileLockError("".join(msg))
863

    
864
  os.write(fd_pidfile, "%d\n" % os.getpid())
865

    
866
  return fd_pidfile
867

    
868

    
869
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
870
  """Reads the watcher pause file.
871

872
  @type filename: string
873
  @param filename: Path to watcher pause file
874
  @type now: None, float or int
875
  @param now: Current time as Unix timestamp
876
  @type remove_after: int
877
  @param remove_after: Remove watcher pause file after specified amount of
878
    seconds past the pause end time
879

880
  """
881
  if now is None:
882
    now = time.time()
883

    
884
  try:
885
    value = ReadFile(filename)
886
  except IOError, err:
887
    if err.errno != errno.ENOENT:
888
      raise
889
    value = None
890

    
891
  if value is not None:
892
    try:
893
      value = int(value)
894
    except ValueError:
895
      logging.warning(("Watcher pause file (%s) contains invalid value,"
896
                       " removing it"), filename)
897
      RemoveFile(filename)
898
      value = None
899

    
900
    if value is not None:
901
      # Remove file if it's outdated
902
      if now > (value + remove_after):
903
        RemoveFile(filename)
904
        value = None
905

    
906
      elif now > value:
907
        value = None
908

    
909
  return value
910

    
911

    
912
def NewUUID():
913
  """Returns a random UUID.
914

915
  @note: This is a Linux-specific method as it uses the /proc
916
      filesystem.
917
  @rtype: str
918

919
  """
920
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")