Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ 2dbc6857

History | View | Annotate | Download (24.6 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
  try:
705
    pid = int(raw_data)
706
  except (TypeError, ValueError), err:
707
    logging.info("Can't parse pid file contents", exc_info=True)
708
    return 0
709

    
710
  return pid
711

    
712

    
713
def ReadLockedPidFile(path):
714
  """Reads a locked PID file.
715

716
  This can be used together with L{utils.process.StartDaemon}.
717

718
  @type path: string
719
  @param path: Path to PID file
720
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
721

722
  """
723
  try:
724
    fd = os.open(path, os.O_RDONLY)
725
  except EnvironmentError, err:
726
    if err.errno == errno.ENOENT:
727
      # PID file doesn't exist
728
      return None
729
    raise
730

    
731
  try:
732
    try:
733
      # Try to acquire lock
734
      filelock.LockFile(fd)
735
    except errors.LockError:
736
      # Couldn't lock, daemon is running
737
      return int(os.read(fd, 100))
738
  finally:
739
    os.close(fd)
740

    
741
  return None
742

    
743

    
744
def AddAuthorizedKey(file_obj, key):
745
  """Adds an SSH public key to an authorized_keys file.
746

747
  @type file_obj: str or file handle
748
  @param file_obj: path to authorized_keys file
749
  @type key: str
750
  @param key: string containing key
751

752
  """
753
  key_fields = key.split()
754

    
755
  if isinstance(file_obj, basestring):
756
    f = open(file_obj, "a+")
757
  else:
758
    f = file_obj
759

    
760
  try:
761
    nl = True
762
    for line in f:
763
      # Ignore whitespace changes
764
      if line.split() == key_fields:
765
        break
766
      nl = line.endswith("\n")
767
    else:
768
      if not nl:
769
        f.write("\n")
770
      f.write(key.rstrip("\r\n"))
771
      f.write("\n")
772
      f.flush()
773
  finally:
774
    f.close()
775

    
776

    
777
def RemoveAuthorizedKey(file_name, key):
778
  """Removes an SSH public key from an authorized_keys file.
779

780
  @type file_name: str
781
  @param file_name: path to authorized_keys file
782
  @type key: str
783
  @param key: string containing key
784

785
  """
786
  key_fields = key.split()
787

    
788
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
789
  try:
790
    out = os.fdopen(fd, "w")
791
    try:
792
      f = open(file_name, "r")
793
      try:
794
        for line in f:
795
          # Ignore whitespace changes while comparing lines
796
          if line.split() != key_fields:
797
            out.write(line)
798

    
799
        out.flush()
800
        os.rename(tmpname, file_name)
801
      finally:
802
        f.close()
803
    finally:
804
      out.close()
805
  except:
806
    RemoveFile(tmpname)
807
    raise
808

    
809

    
810
def DaemonPidFileName(name):
811
  """Compute a ganeti pid file absolute path
812

813
  @type name: str
814
  @param name: the daemon name
815
  @rtype: str
816
  @return: the full path to the pidfile corresponding to the given
817
      daemon name
818

819
  """
820
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
821

    
822

    
823
def WritePidFile(pidfile):
824
  """Write the current process pidfile.
825

826
  @type pidfile: string
827
  @param pidfile: the path to the file to be written
828
  @raise errors.LockError: if the pid file already exists and
829
      points to a live process
830
  @rtype: int
831
  @return: the file descriptor of the lock file; do not close this unless
832
      you want to unlock the pid file
833

834
  """
835
  # We don't rename nor truncate the file to not drop locks under
836
  # existing processes
837
  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
838

    
839
  # Lock the PID file (and fail if not possible to do so). Any code
840
  # wanting to send a signal to the daemon should try to lock the PID
841
  # file before reading it. If acquiring the lock succeeds, the daemon is
842
  # no longer running and the signal should not be sent.
843
  filelock.LockFile(fd_pidfile)
844

    
845
  os.write(fd_pidfile, "%d\n" % os.getpid())
846

    
847
  return fd_pidfile
848

    
849

    
850
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
851
  """Reads the watcher pause file.
852

853
  @type filename: string
854
  @param filename: Path to watcher pause file
855
  @type now: None, float or int
856
  @param now: Current time as Unix timestamp
857
  @type remove_after: int
858
  @param remove_after: Remove watcher pause file after specified amount of
859
    seconds past the pause end time
860

861
  """
862
  if now is None:
863
    now = time.time()
864

    
865
  try:
866
    value = ReadFile(filename)
867
  except IOError, err:
868
    if err.errno != errno.ENOENT:
869
      raise
870
    value = None
871

    
872
  if value is not None:
873
    try:
874
      value = int(value)
875
    except ValueError:
876
      logging.warning(("Watcher pause file (%s) contains invalid value,"
877
                       " removing it"), filename)
878
      RemoveFile(filename)
879
      value = None
880

    
881
    if value is not None:
882
      # Remove file if it's outdated
883
      if now > (value + remove_after):
884
        RemoveFile(filename)
885
        value = None
886

    
887
      elif now > value:
888
        value = None
889

    
890
  return value
891

    
892

    
893
def NewUUID():
894
  """Returns a random UUID.
895

896
  @note: This is a Linux-specific method as it uses the /proc
897
      filesystem.
898
  @rtype: str
899

900
  """
901
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")