Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ c47eddb8

History | View | Annotate | Download (26.8 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
# Possible values for keep_perms in WriteFile()
42
KP_NEVER = 0
43
KP_ALWAYS = 1
44
KP_IF_EXISTS = 2
45

    
46
KEEP_PERMS_VALUES = [
47
  KP_NEVER,
48
  KP_ALWAYS,
49
  KP_IF_EXISTS,
50
  ]
51

    
52

    
53
def ErrnoOrStr(err):
54
  """Format an EnvironmentError exception.
55

56
  If the L{err} argument has an errno attribute, it will be looked up
57
  and converted into a textual C{E...} description. Otherwise the
58
  string representation of the error will be returned.
59

60
  @type err: L{EnvironmentError}
61
  @param err: the exception to format
62

63
  """
64
  if hasattr(err, "errno"):
65
    detail = errno.errorcode[err.errno]
66
  else:
67
    detail = str(err)
68
  return detail
69

    
70

    
71
def ReadFile(file_name, size=-1, preread=None):
72
  """Reads a file.
73

74
  @type size: int
75
  @param size: Read at most size bytes (if negative, entire file)
76
  @type preread: callable receiving file handle as single parameter
77
  @param preread: Function called before file is read
78
  @rtype: str
79
  @return: the (possibly partial) content of the file
80

81
  """
82
  f = open(file_name, "r")
83
  try:
84
    if preread:
85
      preread(f)
86

    
87
    return f.read(size)
88
  finally:
89
    f.close()
90

    
91

    
92
def WriteFile(file_name, fn=None, data=None,
93
              mode=None, uid=-1, gid=-1,
94
              atime=None, mtime=None, close=True,
95
              dry_run=False, backup=False,
96
              prewrite=None, postwrite=None, keep_perms=KP_NEVER):
97
  """(Over)write a file atomically.
98

99
  The file_name and either fn (a function taking one argument, the
100
  file descriptor, and which should write the data to it) or data (the
101
  contents of the file) must be passed. The other arguments are
102
  optional and allow setting the file mode, owner and group, and the
103
  mtime/atime of the file.
104

105
  If the function doesn't raise an exception, it has succeeded and the
106
  target file has the new contents. If the function has raised an
107
  exception, an existing target file should be unmodified and the
108
  temporary file should be removed.
109

110
  @type file_name: str
111
  @param file_name: the target filename
112
  @type fn: callable
113
  @param fn: content writing function, called with
114
      file descriptor as parameter
115
  @type data: str
116
  @param data: contents of the file
117
  @type mode: int
118
  @param mode: file mode
119
  @type uid: int
120
  @param uid: the owner of the file
121
  @type gid: int
122
  @param gid: the group of the file
123
  @type atime: int
124
  @param atime: a custom access time to be set on the file
125
  @type mtime: int
126
  @param mtime: a custom modification time to be set on the file
127
  @type close: boolean
128
  @param close: whether to close file after writing it
129
  @type prewrite: callable
130
  @param prewrite: function to be called before writing content
131
  @type postwrite: callable
132
  @param postwrite: function to be called after writing content
133
  @type keep_perms: members of L{KEEP_PERMS_VALUES}
134
  @param keep_perms: if L{KP_NEVER} (default), owner, group, and mode are
135
      taken from the other parameters; if L{KP_ALWAYS}, owner, group, and
136
      mode are copied from the existing file; if L{KP_IF_EXISTS}, owner,
137
      group, and mode are taken from the file, and if the file doesn't
138
      exist, they are taken from the other parameters. It is an error to
139
      pass L{KP_ALWAYS} when the file doesn't exist or when C{uid}, C{gid},
140
      or C{mode} are set to non-default values.
141

142
  @rtype: None or int
143
  @return: None if the 'close' parameter evaluates to True,
144
      otherwise the file descriptor
145

146
  @raise errors.ProgrammerError: if any of the arguments are not valid
147

148
  """
149
  if not os.path.isabs(file_name):
150
    raise errors.ProgrammerError("Path passed to WriteFile is not"
151
                                 " absolute: '%s'" % file_name)
152

    
153
  if [fn, data].count(None) != 1:
154
    raise errors.ProgrammerError("fn or data required")
155

    
156
  if [atime, mtime].count(None) == 1:
157
    raise errors.ProgrammerError("Both atime and mtime must be either"
158
                                 " set or None")
159

    
160
  if not keep_perms in KEEP_PERMS_VALUES:
161
    raise errors.ProgrammerError("Invalid value for keep_perms: %s" %
162
                                 keep_perms)
163
  if keep_perms == KP_ALWAYS and (uid != -1 or gid != -1 or mode is not None):
164
    raise errors.ProgrammerError("When keep_perms==KP_ALWAYS, 'uid', 'gid',"
165
                                 " and 'mode' cannot be set")
166

    
167
  if backup and not dry_run and os.path.isfile(file_name):
168
    CreateBackup(file_name)
169

    
170
  if keep_perms == KP_ALWAYS or keep_perms == KP_IF_EXISTS:
171
    # os.stat() raises an exception if the file doesn't exist
172
    try:
173
      file_stat = os.stat(file_name)
174
      mode = stat.S_IMODE(file_stat.st_mode)
175
      uid = file_stat.st_uid
176
      gid = file_stat.st_gid
177
    except OSError:
178
      if keep_perms == KP_ALWAYS:
179
        raise
180
      # else: if keeep_perms == KP_IF_EXISTS it's ok if the file doesn't exist
181

    
182
  # Whether temporary file needs to be removed (e.g. if any error occurs)
183
  do_remove = True
184

    
185
  # Function result
186
  result = None
187

    
188
  (dir_name, base_name) = os.path.split(file_name)
189
  (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
190
                                    dir=dir_name)
191
  try:
192
    try:
193
      if uid != -1 or gid != -1:
194
        os.chown(new_name, uid, gid)
195
      if mode:
196
        os.chmod(new_name, mode)
197
      if callable(prewrite):
198
        prewrite(fd)
199
      if data is not None:
200
        if isinstance(data, unicode):
201
          data = data.encode()
202
        assert isinstance(data, str)
203
        to_write = len(data)
204
        offset = 0
205
        while offset < to_write:
206
          written = os.write(fd, buffer(data, offset))
207
          assert written >= 0
208
          assert written <= to_write - offset
209
          offset += written
210
        assert offset == to_write
211
      else:
212
        fn(fd)
213
      if callable(postwrite):
214
        postwrite(fd)
215
      os.fsync(fd)
216
      if atime is not None and mtime is not None:
217
        os.utime(new_name, (atime, mtime))
218
    finally:
219
      # Close file unless the file descriptor should be returned
220
      if close:
221
        os.close(fd)
222
      else:
223
        result = fd
224

    
225
    # Rename file to destination name
226
    if not dry_run:
227
      os.rename(new_name, file_name)
228
      # Successful, no need to remove anymore
229
      do_remove = False
230
  finally:
231
    if do_remove:
232
      RemoveFile(new_name)
233

    
234
  return result
235

    
236

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

240
  Either the path to the file or the fd must be given.
241

242
  @param path: the file path
243
  @param fd: a file descriptor
244
  @return: a tuple of (device number, inode number, mtime)
245

246
  """
247
  if [path, fd].count(None) != 1:
248
    raise errors.ProgrammerError("One and only one of fd/path must be given")
249

    
250
  if fd is None:
251
    st = os.stat(path)
252
  else:
253
    st = os.fstat(fd)
254

    
255
  return (st.st_dev, st.st_ino, st.st_mtime)
256

    
257

    
258
def VerifyFileID(fi_disk, fi_ours):
259
  """Verifies that two file IDs are matching.
260

261
  Differences in the inode/device are not accepted, but and older
262
  timestamp for fi_disk is accepted.
263

264
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
265
      file data
266
  @param fi_ours: tuple (dev, inode, mtime) representing the last
267
      written file data
268
  @rtype: boolean
269

270
  """
271
  (d1, i1, m1) = fi_disk
272
  (d2, i2, m2) = fi_ours
273

    
274
  return (d1, i1) == (d2, i2) and m1 <= m2
275

    
276

    
277
def SafeWriteFile(file_name, file_id, **kwargs):
278
  """Wraper over L{WriteFile} that locks the target file.
279

280
  By keeping the target file locked during WriteFile, we ensure that
281
  cooperating writers will safely serialise access to the file.
282

283
  @type file_name: str
284
  @param file_name: the target filename
285
  @type file_id: tuple
286
  @param file_id: a result from L{GetFileID}
287

288
  """
289
  fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
290
  try:
291
    filelock.LockFile(fd)
292
    if file_id is not None:
293
      disk_id = GetFileID(fd=fd)
294
      if not VerifyFileID(disk_id, file_id):
295
        raise errors.LockError("Cannot overwrite file %s, it has been modified"
296
                               " since last written" % file_name)
297
    return WriteFile(file_name, **kwargs)
298
  finally:
299
    os.close(fd)
300

    
301

    
302
def ReadOneLineFile(file_name, strict=False):
303
  """Return the first non-empty line from a file.
304

305
  @type strict: boolean
306
  @param strict: if True, abort if the file has more than one
307
      non-empty line
308

309
  """
310
  file_lines = ReadFile(file_name).splitlines()
311
  full_lines = filter(bool, file_lines)
312
  if not file_lines or not full_lines:
313
    raise errors.GenericError("No data in one-liner file %s" % file_name)
314
  elif strict and len(full_lines) > 1:
315
    raise errors.GenericError("Too many lines in one-liner file %s" %
316
                              file_name)
317
  return full_lines[0]
318

    
319

    
320
def RemoveFile(filename):
321
  """Remove a file ignoring some errors.
322

323
  Remove a file, ignoring non-existing ones or directories. Other
324
  errors are passed.
325

326
  @type filename: str
327
  @param filename: the file to be removed
328

329
  """
330
  try:
331
    os.unlink(filename)
332
  except OSError, err:
333
    if err.errno not in (errno.ENOENT, errno.EISDIR):
334
      raise
335

    
336

    
337
def RemoveDir(dirname):
338
  """Remove an empty directory.
339

340
  Remove a directory, ignoring non-existing ones.
341
  Other errors are passed. This includes the case,
342
  where the directory is not empty, so it can't be removed.
343

344
  @type dirname: str
345
  @param dirname: the empty directory to be removed
346

347
  """
348
  try:
349
    os.rmdir(dirname)
350
  except OSError, err:
351
    if err.errno != errno.ENOENT:
352
      raise
353

    
354

    
355
def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
356
               dir_gid=None):
357
  """Renames a file.
358

359
  This just creates the very least directory if it does not exist and C{mkdir}
360
  is set to true.
361

362
  @type old: string
363
  @param old: Original path
364
  @type new: string
365
  @param new: New path
366
  @type mkdir: bool
367
  @param mkdir: Whether to create target directory if it doesn't exist
368
  @type mkdir_mode: int
369
  @param mkdir_mode: Mode for newly created directories
370
  @type dir_uid: int
371
  @param dir_uid: The uid for the (if fresh created) dir
372
  @type dir_gid: int
373
  @param dir_gid: The gid for the (if fresh created) dir
374

375
  """
376
  try:
377
    return os.rename(old, new)
378
  except OSError, err:
379
    # In at least one use case of this function, the job queue, directory
380
    # creation is very rare. Checking for the directory before renaming is not
381
    # as efficient.
382
    if mkdir and err.errno == errno.ENOENT:
383
      # Create directory and try again
384
      dir_path = os.path.dirname(new)
385
      MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
386

    
387
      return os.rename(old, new)
388

    
389
    raise
390

    
391

    
392
def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
393
                      _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
394
  """Enforces that given path has given permissions.
395

396
  @param path: The path to the file
397
  @param mode: The mode of the file
398
  @param uid: The uid of the owner of this file
399
  @param gid: The gid of the owner of this file
400
  @param must_exist: Specifies if non-existance of path will be an error
401
  @param _chmod_fn: chmod function to use (unittest only)
402
  @param _chown_fn: chown function to use (unittest only)
403

404
  """
405
  logging.debug("Checking %s", path)
406

    
407
  # chown takes -1 if you want to keep one part of the ownership, however
408
  # None is Python standard for that. So we remap them here.
409
  if uid is None:
410
    uid = -1
411
  if gid is None:
412
    gid = -1
413

    
414
  try:
415
    st = _stat_fn(path)
416

    
417
    fmode = stat.S_IMODE(st[stat.ST_MODE])
418
    if fmode != mode:
419
      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
420
      _chmod_fn(path, mode)
421

    
422
    if max(uid, gid) > -1:
423
      fuid = st[stat.ST_UID]
424
      fgid = st[stat.ST_GID]
425
      if fuid != uid or fgid != gid:
426
        logging.debug("Changing owner of %s from UID %s/GID %s to"
427
                      " UID %s/GID %s", path, fuid, fgid, uid, gid)
428
        _chown_fn(path, uid, gid)
429
  except EnvironmentError, err:
430
    if err.errno == errno.ENOENT:
431
      if must_exist:
432
        raise errors.GenericError("Path %s should exist, but does not" % path)
433
    else:
434
      raise errors.GenericError("Error while changing permissions on %s: %s" %
435
                                (path, err))
436

    
437

    
438
def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
439
                    _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
440
  """Enforces that given path is a dir and has given mode, uid and gid set.
441

442
  @param path: The path to the file
443
  @param mode: The mode of the file
444
  @param uid: The uid of the owner of this file
445
  @param gid: The gid of the owner of this file
446
  @param _lstat_fn: Stat function to use (unittest only)
447
  @param _mkdir_fn: mkdir function to use (unittest only)
448
  @param _perm_fn: permission setter function to use (unittest only)
449

450
  """
451
  logging.debug("Checking directory %s", path)
452
  try:
453
    # We don't want to follow symlinks
454
    st = _lstat_fn(path)
455
  except EnvironmentError, err:
456
    if err.errno != errno.ENOENT:
457
      raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
458
    _mkdir_fn(path)
459
  else:
460
    if not stat.S_ISDIR(st[stat.ST_MODE]):
461
      raise errors.GenericError(("Path %s is expected to be a directory, but "
462
                                 "isn't") % path)
463

    
464
  _perm_fn(path, mode, uid=uid, gid=gid)
465

    
466

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

470
  This is a wrapper around C{os.makedirs} adding error handling not implemented
471
  before Python 2.5.
472

473
  """
474
  try:
475
    os.makedirs(path, mode)
476
  except OSError, err:
477
    # Ignore EEXIST. This is only handled in os.makedirs as included in
478
    # Python 2.5 and above.
479
    if err.errno != errno.EEXIST or not os.path.exists(path):
480
      raise
481

    
482

    
483
def TimestampForFilename():
484
  """Returns the current time formatted for filenames.
485

486
  The format doesn't contain colons as some shells and applications treat them
487
  as separators. Uses the local timezone.
488

489
  """
490
  return time.strftime("%Y-%m-%d_%H_%M_%S")
491

    
492

    
493
def CreateBackup(file_name):
494
  """Creates a backup of a file.
495

496
  @type file_name: str
497
  @param file_name: file to be backed up
498
  @rtype: str
499
  @return: the path to the newly created backup
500
  @raise errors.ProgrammerError: for invalid file names
501

502
  """
503
  if not os.path.isfile(file_name):
504
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
505
                                file_name)
506

    
507
  prefix = ("%s.backup-%s." %
508
            (os.path.basename(file_name), TimestampForFilename()))
509
  dir_name = os.path.dirname(file_name)
510

    
511
  fsrc = open(file_name, "rb")
512
  try:
513
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
514
    fdst = os.fdopen(fd, "wb")
515
    try:
516
      logging.debug("Backing up %s at %s", file_name, backup_name)
517
      shutil.copyfileobj(fsrc, fdst)
518
    finally:
519
      fdst.close()
520
  finally:
521
    fsrc.close()
522

    
523
  return backup_name
524

    
525

    
526
def ListVisibleFiles(path):
527
  """Returns a list of visible files in a directory.
528

529
  @type path: str
530
  @param path: the directory to enumerate
531
  @rtype: list
532
  @return: the list of all files not starting with a dot
533
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
534

535
  """
536
  if not IsNormAbsPath(path):
537
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
538
                                 " absolute/normalized: '%s'" % path)
539
  files = [i for i in os.listdir(path) if not i.startswith(".")]
540
  return files
541

    
542

    
543
def EnsureDirs(dirs):
544
  """Make required directories, if they don't exist.
545

546
  @param dirs: list of tuples (dir_name, dir_mode)
547
  @type dirs: list of (string, integer)
548

549
  """
550
  for dir_name, dir_mode in dirs:
551
    try:
552
      os.mkdir(dir_name, dir_mode)
553
    except EnvironmentError, err:
554
      if err.errno != errno.EEXIST:
555
        raise errors.GenericError("Cannot create needed directory"
556
                                  " '%s': %s" % (dir_name, err))
557
    try:
558
      os.chmod(dir_name, dir_mode)
559
    except EnvironmentError, err:
560
      raise errors.GenericError("Cannot change directory permissions on"
561
                                " '%s': %s" % (dir_name, err))
562
    if not os.path.isdir(dir_name):
563
      raise errors.GenericError("%s is not a directory" % dir_name)
564

    
565

    
566
def FindFile(name, search_path, test=os.path.exists):
567
  """Look for a filesystem object in a given path.
568

569
  This is an abstract method to search for filesystem object (files,
570
  dirs) under a given search path.
571

572
  @type name: str
573
  @param name: the name to look for
574
  @type search_path: str
575
  @param search_path: location to start at
576
  @type test: callable
577
  @param test: a function taking one argument that should return True
578
      if the a given object is valid; the default value is
579
      os.path.exists, causing only existing files to be returned
580
  @rtype: str or None
581
  @return: full path to the object if found, None otherwise
582

583
  """
584
  # validate the filename mask
585
  if constants.EXT_PLUGIN_MASK.match(name) is None:
586
    logging.critical("Invalid value passed for external script name: '%s'",
587
                     name)
588
    return None
589

    
590
  for dir_name in search_path:
591
    # FIXME: investigate switch to PathJoin
592
    item_name = os.path.sep.join([dir_name, name])
593
    # check the user test and that we're indeed resolving to the given
594
    # basename
595
    if test(item_name) and os.path.basename(item_name) == name:
596
      return item_name
597
  return None
598

    
599

    
600
def IsNormAbsPath(path):
601
  """Check whether a path is absolute and also normalized
602

603
  This avoids things like /dir/../../other/path to be valid.
604

605
  """
606
  return os.path.normpath(path) == path and os.path.isabs(path)
607

    
608

    
609
def IsBelowDir(root, other_path):
610
  """Check whether a path is below a root dir.
611

612
  This works around the nasty byte-byte comparisation of commonprefix.
613

614
  """
615
  if not (os.path.isabs(root) and os.path.isabs(other_path)):
616
    raise ValueError("Provided paths '%s' and '%s' are not absolute" %
617
                     (root, other_path))
618
  prepared_root = "%s%s" % (os.path.normpath(root), os.sep)
619
  return os.path.commonprefix([prepared_root,
620
                               os.path.normpath(other_path)]) == prepared_root
621

    
622

    
623
def PathJoin(*args):
624
  """Safe-join a list of path components.
625

626
  Requirements:
627
      - the first argument must be an absolute path
628
      - no component in the path must have backtracking (e.g. /../),
629
        since we check for normalization at the end
630

631
  @param args: the path components to be joined
632
  @raise ValueError: for invalid paths
633

634
  """
635
  # ensure we're having at least one path passed in
636
  assert args
637
  # ensure the first component is an absolute and normalized path name
638
  root = args[0]
639
  if not IsNormAbsPath(root):
640
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
641
  result = os.path.join(*args)
642
  # ensure that the whole path is normalized
643
  if not IsNormAbsPath(result):
644
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
645
  # check that we're still under the original prefix
646
  if not IsBelowDir(root, result):
647
    raise ValueError("Error: path joining resulted in different prefix"
648
                     " (%s != %s)" % (result, root))
649
  return result
650

    
651

    
652
def TailFile(fname, lines=20):
653
  """Return the last lines from a file.
654

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

659
  @param fname: the file name
660
  @type lines: int
661
  @param lines: the (maximum) number of lines to return
662

663
  """
664
  fd = open(fname, "r")
665
  try:
666
    fd.seek(0, 2)
667
    pos = fd.tell()
668
    pos = max(0, pos - 4096)
669
    fd.seek(pos, 0)
670
    raw_data = fd.read()
671
  finally:
672
    fd.close()
673

    
674
  rows = raw_data.splitlines()
675
  return rows[-lines:]
676

    
677

    
678
def BytesToMebibyte(value):
679
  """Converts bytes to mebibytes.
680

681
  @type value: int
682
  @param value: Value in bytes
683
  @rtype: int
684
  @return: Value in mebibytes
685

686
  """
687
  return int(round(value / (1024.0 * 1024.0), 0))
688

    
689

    
690
def CalculateDirectorySize(path):
691
  """Calculates the size of a directory recursively.
692

693
  @type path: string
694
  @param path: Path to directory
695
  @rtype: int
696
  @return: Size in mebibytes
697

698
  """
699
  size = 0
700

    
701
  for (curpath, _, files) in os.walk(path):
702
    for filename in files:
703
      st = os.lstat(PathJoin(curpath, filename))
704
      size += st.st_size
705

    
706
  return BytesToMebibyte(size)
707

    
708

    
709
def GetFilesystemStats(path):
710
  """Returns the total and free space on a filesystem.
711

712
  @type path: string
713
  @param path: Path on filesystem to be examined
714
  @rtype: int
715
  @return: tuple of (Total space, Free space) in mebibytes
716

717
  """
718
  st = os.statvfs(path)
719

    
720
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
721
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
722
  return (tsize, fsize)
723

    
724

    
725
def ReadPidFile(pidfile):
726
  """Read a pid from a file.
727

728
  @type  pidfile: string
729
  @param pidfile: path to the file containing the pid
730
  @rtype: int
731
  @return: The process id, if the file exists and contains a valid PID,
732
           otherwise 0
733

734
  """
735
  try:
736
    raw_data = ReadOneLineFile(pidfile)
737
  except EnvironmentError, err:
738
    if err.errno != errno.ENOENT:
739
      logging.exception("Can't read pid file")
740
    return 0
741

    
742
  try:
743
    pid = int(raw_data)
744
  except (TypeError, ValueError), err:
745
    logging.info("Can't parse pid file contents", exc_info=True)
746
    return 0
747

    
748
  return pid
749

    
750

    
751
def ReadLockedPidFile(path):
752
  """Reads a locked PID file.
753

754
  This can be used together with L{utils.process.StartDaemon}.
755

756
  @type path: string
757
  @param path: Path to PID file
758
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
759

760
  """
761
  try:
762
    fd = os.open(path, os.O_RDONLY)
763
  except EnvironmentError, err:
764
    if err.errno == errno.ENOENT:
765
      # PID file doesn't exist
766
      return None
767
    raise
768

    
769
  try:
770
    try:
771
      # Try to acquire lock
772
      filelock.LockFile(fd)
773
    except errors.LockError:
774
      # Couldn't lock, daemon is running
775
      return int(os.read(fd, 100))
776
  finally:
777
    os.close(fd)
778

    
779
  return None
780

    
781

    
782
def AddAuthorizedKey(file_obj, key):
783
  """Adds an SSH public key to an authorized_keys file.
784

785
  @type file_obj: str or file handle
786
  @param file_obj: path to authorized_keys file
787
  @type key: str
788
  @param key: string containing key
789

790
  """
791
  key_fields = key.split()
792

    
793
  if isinstance(file_obj, basestring):
794
    f = open(file_obj, "a+")
795
  else:
796
    f = file_obj
797

    
798
  try:
799
    nl = True
800
    for line in f:
801
      # Ignore whitespace changes
802
      if line.split() == key_fields:
803
        break
804
      nl = line.endswith("\n")
805
    else:
806
      if not nl:
807
        f.write("\n")
808
      f.write(key.rstrip("\r\n"))
809
      f.write("\n")
810
      f.flush()
811
  finally:
812
    f.close()
813

    
814

    
815
def RemoveAuthorizedKey(file_name, key):
816
  """Removes an SSH public key from an authorized_keys file.
817

818
  @type file_name: str
819
  @param file_name: path to authorized_keys file
820
  @type key: str
821
  @param key: string containing key
822

823
  """
824
  key_fields = key.split()
825

    
826
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
827
  try:
828
    out = os.fdopen(fd, "w")
829
    try:
830
      f = open(file_name, "r")
831
      try:
832
        for line in f:
833
          # Ignore whitespace changes while comparing lines
834
          if line.split() != key_fields:
835
            out.write(line)
836

    
837
        out.flush()
838
        os.rename(tmpname, file_name)
839
      finally:
840
        f.close()
841
    finally:
842
      out.close()
843
  except:
844
    RemoveFile(tmpname)
845
    raise
846

    
847

    
848
def DaemonPidFileName(name):
849
  """Compute a ganeti pid file absolute path
850

851
  @type name: str
852
  @param name: the daemon name
853
  @rtype: str
854
  @return: the full path to the pidfile corresponding to the given
855
      daemon name
856

857
  """
858
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
859

    
860

    
861
def WritePidFile(pidfile):
862
  """Write the current process pidfile.
863

864
  @type pidfile: string
865
  @param pidfile: the path to the file to be written
866
  @raise errors.LockError: if the pid file already exists and
867
      points to a live process
868
  @rtype: int
869
  @return: the file descriptor of the lock file; do not close this unless
870
      you want to unlock the pid file
871

872
  """
873
  # We don't rename nor truncate the file to not drop locks under
874
  # existing processes
875
  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
876

    
877
  # Lock the PID file (and fail if not possible to do so). Any code
878
  # wanting to send a signal to the daemon should try to lock the PID
879
  # file before reading it. If acquiring the lock succeeds, the daemon is
880
  # no longer running and the signal should not be sent.
881
  filelock.LockFile(fd_pidfile)
882

    
883
  os.write(fd_pidfile, "%d\n" % os.getpid())
884

    
885
  return fd_pidfile
886

    
887

    
888
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
889
  """Reads the watcher pause file.
890

891
  @type filename: string
892
  @param filename: Path to watcher pause file
893
  @type now: None, float or int
894
  @param now: Current time as Unix timestamp
895
  @type remove_after: int
896
  @param remove_after: Remove watcher pause file after specified amount of
897
    seconds past the pause end time
898

899
  """
900
  if now is None:
901
    now = time.time()
902

    
903
  try:
904
    value = ReadFile(filename)
905
  except IOError, err:
906
    if err.errno != errno.ENOENT:
907
      raise
908
    value = None
909

    
910
  if value is not None:
911
    try:
912
      value = int(value)
913
    except ValueError:
914
      logging.warning(("Watcher pause file (%s) contains invalid value,"
915
                       " removing it"), filename)
916
      RemoveFile(filename)
917
      value = None
918

    
919
    if value is not None:
920
      # Remove file if it's outdated
921
      if now > (value + remove_after):
922
        RemoveFile(filename)
923
        value = None
924

    
925
      elif now > value:
926
        value = None
927

    
928
  return value
929

    
930

    
931
def NewUUID():
932
  """Returns a random UUID.
933

934
  @note: This is a Linux-specific method as it uses the /proc
935
      filesystem.
936
  @rtype: str
937

938
  """
939
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
940

    
941

    
942
class TemporaryFileManager(object):
943
  """Stores the list of files to be deleted and removes them on demand.
944

945
  """
946

    
947
  def __init__(self):
948
    self._files = []
949

    
950
  def __del__(self):
951
    self.Cleanup()
952

    
953
  def Add(self, filename):
954
    """Add file to list of files to be deleted.
955

956
    @type filename: string
957
    @param filename: path to filename to be added
958

959
    """
960
    self._files.append(filename)
961

    
962
  def Remove(self, filename):
963
    """Remove file from list of files to be deleted.
964

965
    @type filename: string
966
    @param filename: path to filename to be deleted
967

968
    """
969
    self._files.remove(filename)
970

    
971
  def Cleanup(self):
972
    """Delete all files marked for deletion
973

974
    """
975
    while self._files:
976
      RemoveFile(self._files.pop())