Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ 57de31c0

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

    
42
def ReadFile(file_name, size=-1, preread=None):
43
  """Reads a file.
44

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

52
  """
53
  f = open(file_name, "r")
54
  try:
55
    if preread:
56
      preread(f)
57

    
58
    return f.read(size)
59
  finally:
60
    f.close()
61

    
62

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

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

76
  If the function doesn't raise an exception, it has succeeded and the
77
  target file has the new contents. If the function has raised an
78
  exception, an existing target file should be unmodified and the
79
  temporary file should be removed.
80

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

105
  @rtype: None or int
106
  @return: None if the 'close' parameter evaluates to True,
107
      otherwise the file descriptor
108

109
  @raise errors.ProgrammerError: if any of the arguments are not valid
110

111
  """
112
  if not os.path.isabs(file_name):
113
    raise errors.ProgrammerError("Path passed to WriteFile is not"
114
                                 " absolute: '%s'" % file_name)
115

    
116
  if [fn, data].count(None) != 1:
117
    raise errors.ProgrammerError("fn or data required")
118

    
119
  if [atime, mtime].count(None) == 1:
120
    raise errors.ProgrammerError("Both atime and mtime must be either"
121
                                 " set or None")
122

    
123
  if backup and not dry_run and os.path.isfile(file_name):
124
    CreateBackup(file_name)
125

    
126
  # Whether temporary file needs to be removed (e.g. if any error occurs)
127
  do_remove = True
128

    
129
  # Function result
130
  result = None
131

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

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

    
178
  return result
179

    
180

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

184
  Either the path to the file or the fd must be given.
185

186
  @param path: the file path
187
  @param fd: a file descriptor
188
  @return: a tuple of (device number, inode number, mtime)
189

190
  """
191
  if [path, fd].count(None) != 1:
192
    raise errors.ProgrammerError("One and only one of fd/path must be given")
193

    
194
  if fd is None:
195
    st = os.stat(path)
196
  else:
197
    st = os.fstat(fd)
198

    
199
  return (st.st_dev, st.st_ino, st.st_mtime)
200

    
201

    
202
def VerifyFileID(fi_disk, fi_ours):
203
  """Verifies that two file IDs are matching.
204

205
  Differences in the inode/device are not accepted, but and older
206
  timestamp for fi_disk is accepted.
207

208
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
209
      file data
210
  @param fi_ours: tuple (dev, inode, mtime) representing the last
211
      written file data
212
  @rtype: boolean
213

214
  """
215
  (d1, i1, m1) = fi_disk
216
  (d2, i2, m2) = fi_ours
217

    
218
  return (d1, i1) == (d2, i2) and m1 <= m2
219

    
220

    
221
def SafeWriteFile(file_name, file_id, **kwargs):
222
  """Wraper over L{WriteFile} that locks the target file.
223

224
  By keeping the target file locked during WriteFile, we ensure that
225
  cooperating writers will safely serialise access to the file.
226

227
  @type file_name: str
228
  @param file_name: the target filename
229
  @type file_id: tuple
230
  @param file_id: a result from L{GetFileID}
231

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

    
245

    
246
def ReadOneLineFile(file_name, strict=False):
247
  """Return the first non-empty line from a file.
248

249
  @type strict: boolean
250
  @param strict: if True, abort if the file has more than one
251
      non-empty line
252

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

    
263

    
264
def RemoveFile(filename):
265
  """Remove a file ignoring some errors.
266

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

270
  @type filename: str
271
  @param filename: the file to be removed
272

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

    
280

    
281
def RemoveDir(dirname):
282
  """Remove an empty directory.
283

284
  Remove a directory, ignoring non-existing ones.
285
  Other errors are passed. This includes the case,
286
  where the directory is not empty, so it can't be removed.
287

288
  @type dirname: str
289
  @param dirname: the empty directory to be removed
290

291
  """
292
  try:
293
    os.rmdir(dirname)
294
  except OSError, err:
295
    if err.errno != errno.ENOENT:
296
      raise
297

    
298

    
299
def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
300
               dir_gid=None):
301
  """Renames a file.
302

303
  This just creates the very least directory if it does not exist and C{mkdir}
304
  is set to true.
305

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

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

    
331
      return os.rename(old, new)
332

    
333
    raise
334

    
335

    
336
def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
337
                      _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
338
  """Enforces that given path has given permissions.
339

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

348
  """
349
  logging.debug("Checking %s", path)
350

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

    
358
  try:
359
    st = _stat_fn(path)
360

    
361
    fmode = stat.S_IMODE(st[stat.ST_MODE])
362
    if fmode != mode:
363
      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
364
      _chmod_fn(path, mode)
365

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

    
381

    
382
def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
383
                    _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
384
  """Enforces that given path is a dir and has given mode, uid and gid set.
385

386
  @param path: The path to the file
387
  @param mode: The mode of the file
388
  @param uid: The uid of the owner of this file
389
  @param gid: The gid of the owner of this file
390
  @param _lstat_fn: Stat function to use (unittest only)
391
  @param _mkdir_fn: mkdir function to use (unittest only)
392
  @param _perm_fn: permission setter function to use (unittest only)
393

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

    
408
  _perm_fn(path, mode, uid=uid, gid=gid)
409

    
410

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

414
  This is a wrapper around C{os.makedirs} adding error handling not implemented
415
  before Python 2.5.
416

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

    
426

    
427
def TimestampForFilename():
428
  """Returns the current time formatted for filenames.
429

430
  The format doesn't contain colons as some shells and applications treat them
431
  as separators. Uses the local timezone.
432

433
  """
434
  return time.strftime("%Y-%m-%d_%H_%M_%S")
435

    
436

    
437
def CreateBackup(file_name):
438
  """Creates a backup of a file.
439

440
  @type file_name: str
441
  @param file_name: file to be backed up
442
  @rtype: str
443
  @return: the path to the newly created backup
444
  @raise errors.ProgrammerError: for invalid file names
445

446
  """
447
  if not os.path.isfile(file_name):
448
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
449
                                file_name)
450

    
451
  prefix = ("%s.backup-%s." %
452
            (os.path.basename(file_name), TimestampForFilename()))
453
  dir_name = os.path.dirname(file_name)
454

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

    
467
  return backup_name
468

    
469

    
470
def ListVisibleFiles(path):
471
  """Returns a list of visible files in a directory.
472

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

479
  """
480
  if not IsNormAbsPath(path):
481
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
482
                                 " absolute/normalized: '%s'" % path)
483
  files = [i for i in os.listdir(path) if not i.startswith(".")]
484
  return files
485

    
486

    
487
def EnsureDirs(dirs):
488
  """Make required directories, if they don't exist.
489

490
  @param dirs: list of tuples (dir_name, dir_mode)
491
  @type dirs: list of (string, integer)
492

493
  """
494
  for dir_name, dir_mode in dirs:
495
    try:
496
      os.mkdir(dir_name, dir_mode)
497
    except EnvironmentError, err:
498
      if err.errno != errno.EEXIST:
499
        raise errors.GenericError("Cannot create needed directory"
500
                                  " '%s': %s" % (dir_name, err))
501
    try:
502
      os.chmod(dir_name, dir_mode)
503
    except EnvironmentError, err:
504
      raise errors.GenericError("Cannot change directory permissions on"
505
                                " '%s': %s" % (dir_name, err))
506
    if not os.path.isdir(dir_name):
507
      raise errors.GenericError("%s is not a directory" % dir_name)
508

    
509

    
510
def FindFile(name, search_path, test=os.path.exists):
511
  """Look for a filesystem object in a given path.
512

513
  This is an abstract method to search for filesystem object (files,
514
  dirs) under a given search path.
515

516
  @type name: str
517
  @param name: the name to look for
518
  @type search_path: str
519
  @param search_path: location to start at
520
  @type test: callable
521
  @param test: a function taking one argument that should return True
522
      if the a given object is valid; the default value is
523
      os.path.exists, causing only existing files to be returned
524
  @rtype: str or None
525
  @return: full path to the object if found, None otherwise
526

527
  """
528
  # validate the filename mask
529
  if constants.EXT_PLUGIN_MASK.match(name) is None:
530
    logging.critical("Invalid value passed for external script name: '%s'",
531
                     name)
532
    return None
533

    
534
  for dir_name in search_path:
535
    # FIXME: investigate switch to PathJoin
536
    item_name = os.path.sep.join([dir_name, name])
537
    # check the user test and that we're indeed resolving to the given
538
    # basename
539
    if test(item_name) and os.path.basename(item_name) == name:
540
      return item_name
541
  return None
542

    
543

    
544
def IsNormAbsPath(path):
545
  """Check whether a path is absolute and also normalized
546

547
  This avoids things like /dir/../../other/path to be valid.
548

549
  """
550
  return os.path.normpath(path) == path and os.path.isabs(path)
551

    
552

    
553
def IsBelowDir(root, other_path):
554
  """Check whether a path is below a root dir.
555

556
  This works around the nasty byte-byte comparisation of commonprefix.
557

558
  """
559
  if not (os.path.isabs(root) and os.path.isabs(other_path)):
560
    raise ValueError("Provided paths '%s' and '%s' are not absolute" %
561
                     (root, other_path))
562
  prepared_root = "%s%s" % (os.path.normpath(root), os.sep)
563
  return os.path.commonprefix([prepared_root,
564
                               os.path.normpath(other_path)]) == prepared_root
565

    
566

    
567
def PathJoin(*args):
568
  """Safe-join a list of path components.
569

570
  Requirements:
571
      - the first argument must be an absolute path
572
      - no component in the path must have backtracking (e.g. /../),
573
        since we check for normalization at the end
574

575
  @param args: the path components to be joined
576
  @raise ValueError: for invalid paths
577

578
  """
579
  # ensure we're having at least one path passed in
580
  assert args
581
  # ensure the first component is an absolute and normalized path name
582
  root = args[0]
583
  if not IsNormAbsPath(root):
584
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
585
  result = os.path.join(*args)
586
  # ensure that the whole path is normalized
587
  if not IsNormAbsPath(result):
588
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
589
  # check that we're still under the original prefix
590
  if not IsBelowDir(root, result):
591
    raise ValueError("Error: path joining resulted in different prefix"
592
                     " (%s != %s)" % (result, root))
593
  return result
594

    
595

    
596
def TailFile(fname, lines=20):
597
  """Return the last lines from a file.
598

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

603
  @param fname: the file name
604
  @type lines: int
605
  @param lines: the (maximum) number of lines to return
606

607
  """
608
  fd = open(fname, "r")
609
  try:
610
    fd.seek(0, 2)
611
    pos = fd.tell()
612
    pos = max(0, pos - 4096)
613
    fd.seek(pos, 0)
614
    raw_data = fd.read()
615
  finally:
616
    fd.close()
617

    
618
  rows = raw_data.splitlines()
619
  return rows[-lines:]
620

    
621

    
622
def BytesToMebibyte(value):
623
  """Converts bytes to mebibytes.
624

625
  @type value: int
626
  @param value: Value in bytes
627
  @rtype: int
628
  @return: Value in mebibytes
629

630
  """
631
  return int(round(value / (1024.0 * 1024.0), 0))
632

    
633

    
634
def CalculateDirectorySize(path):
635
  """Calculates the size of a directory recursively.
636

637
  @type path: string
638
  @param path: Path to directory
639
  @rtype: int
640
  @return: Size in mebibytes
641

642
  """
643
  size = 0
644

    
645
  for (curpath, _, files) in os.walk(path):
646
    for filename in files:
647
      st = os.lstat(PathJoin(curpath, filename))
648
      size += st.st_size
649

    
650
  return BytesToMebibyte(size)
651

    
652

    
653
def GetFilesystemStats(path):
654
  """Returns the total and free space on a filesystem.
655

656
  @type path: string
657
  @param path: Path on filesystem to be examined
658
  @rtype: int
659
  @return: tuple of (Total space, Free space) in mebibytes
660

661
  """
662
  st = os.statvfs(path)
663

    
664
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
665
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
666
  return (tsize, fsize)
667

    
668

    
669
def ReadPidFile(pidfile):
670
  """Read a pid from a file.
671

672
  @type  pidfile: string
673
  @param pidfile: path to the file containing the pid
674
  @rtype: int
675
  @return: The process id, if the file exists and contains a valid PID,
676
           otherwise 0
677

678
  """
679
  try:
680
    raw_data = ReadOneLineFile(pidfile)
681
  except EnvironmentError, err:
682
    if err.errno != errno.ENOENT:
683
      logging.exception("Can't read pid file")
684
    return 0
685

    
686
  try:
687
    pid = int(raw_data)
688
  except (TypeError, ValueError), err:
689
    logging.info("Can't parse pid file contents", exc_info=True)
690
    return 0
691

    
692
  return pid
693

    
694

    
695
def ReadLockedPidFile(path):
696
  """Reads a locked PID file.
697

698
  This can be used together with L{utils.process.StartDaemon}.
699

700
  @type path: string
701
  @param path: Path to PID file
702
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
703

704
  """
705
  try:
706
    fd = os.open(path, os.O_RDONLY)
707
  except EnvironmentError, err:
708
    if err.errno == errno.ENOENT:
709
      # PID file doesn't exist
710
      return None
711
    raise
712

    
713
  try:
714
    try:
715
      # Try to acquire lock
716
      filelock.LockFile(fd)
717
    except errors.LockError:
718
      # Couldn't lock, daemon is running
719
      return int(os.read(fd, 100))
720
  finally:
721
    os.close(fd)
722

    
723
  return None
724

    
725

    
726
def AddAuthorizedKey(file_obj, key):
727
  """Adds an SSH public key to an authorized_keys file.
728

729
  @type file_obj: str or file handle
730
  @param file_obj: path to authorized_keys file
731
  @type key: str
732
  @param key: string containing key
733

734
  """
735
  key_fields = key.split()
736

    
737
  if isinstance(file_obj, basestring):
738
    f = open(file_obj, "a+")
739
  else:
740
    f = file_obj
741

    
742
  try:
743
    nl = True
744
    for line in f:
745
      # Ignore whitespace changes
746
      if line.split() == key_fields:
747
        break
748
      nl = line.endswith("\n")
749
    else:
750
      if not nl:
751
        f.write("\n")
752
      f.write(key.rstrip("\r\n"))
753
      f.write("\n")
754
      f.flush()
755
  finally:
756
    f.close()
757

    
758

    
759
def RemoveAuthorizedKey(file_name, key):
760
  """Removes an SSH public key from an authorized_keys file.
761

762
  @type file_name: str
763
  @param file_name: path to authorized_keys file
764
  @type key: str
765
  @param key: string containing key
766

767
  """
768
  key_fields = key.split()
769

    
770
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
771
  try:
772
    out = os.fdopen(fd, "w")
773
    try:
774
      f = open(file_name, "r")
775
      try:
776
        for line in f:
777
          # Ignore whitespace changes while comparing lines
778
          if line.split() != key_fields:
779
            out.write(line)
780

    
781
        out.flush()
782
        os.rename(tmpname, file_name)
783
      finally:
784
        f.close()
785
    finally:
786
      out.close()
787
  except:
788
    RemoveFile(tmpname)
789
    raise
790

    
791

    
792
def DaemonPidFileName(name):
793
  """Compute a ganeti pid file absolute path
794

795
  @type name: str
796
  @param name: the daemon name
797
  @rtype: str
798
  @return: the full path to the pidfile corresponding to the given
799
      daemon name
800

801
  """
802
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
803

    
804

    
805
def WritePidFile(pidfile):
806
  """Write the current process pidfile.
807

808
  @type pidfile: string
809
  @param pidfile: the path to the file to be written
810
  @raise errors.LockError: if the pid file already exists and
811
      points to a live process
812
  @rtype: int
813
  @return: the file descriptor of the lock file; do not close this unless
814
      you want to unlock the pid file
815

816
  """
817
  # We don't rename nor truncate the file to not drop locks under
818
  # existing processes
819
  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
820

    
821
  # Lock the PID file (and fail if not possible to do so). Any code
822
  # wanting to send a signal to the daemon should try to lock the PID
823
  # file before reading it. If acquiring the lock succeeds, the daemon is
824
  # no longer running and the signal should not be sent.
825
  filelock.LockFile(fd_pidfile)
826

    
827
  os.write(fd_pidfile, "%d\n" % os.getpid())
828

    
829
  return fd_pidfile
830

    
831

    
832
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
833
  """Reads the watcher pause file.
834

835
  @type filename: string
836
  @param filename: Path to watcher pause file
837
  @type now: None, float or int
838
  @param now: Current time as Unix timestamp
839
  @type remove_after: int
840
  @param remove_after: Remove watcher pause file after specified amount of
841
    seconds past the pause end time
842

843
  """
844
  if now is None:
845
    now = time.time()
846

    
847
  try:
848
    value = ReadFile(filename)
849
  except IOError, err:
850
    if err.errno != errno.ENOENT:
851
      raise
852
    value = None
853

    
854
  if value is not None:
855
    try:
856
      value = int(value)
857
    except ValueError:
858
      logging.warning(("Watcher pause file (%s) contains invalid value,"
859
                       " removing it"), filename)
860
      RemoveFile(filename)
861
      value = None
862

    
863
    if value is not None:
864
      # Remove file if it's outdated
865
      if now > (value + remove_after):
866
        RemoveFile(filename)
867
        value = None
868

    
869
      elif now > value:
870
        value = None
871

    
872
  return value
873

    
874

    
875
def NewUUID():
876
  """Returns a random UUID.
877

878
  @note: This is a Linux-specific method as it uses the /proc
879
      filesystem.
880
  @rtype: str
881

882
  """
883
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
884

    
885

    
886
class TemporaryFileManager(object):
887
  """Stores the list of files to be deleted and removes them on demand.
888

889
  """
890

    
891
  def __init__(self):
892
    self._files = []
893

    
894
  def __del__(self):
895
    self.Cleanup()
896

    
897
  def Add(self, filename):
898
    """Add file to list of files to be deleted.
899

900
    @type filename: string
901
    @param filename: path to filename to be added
902

903
    """
904
    self._files.append(filename)
905

    
906
  def Remove(self, filename):
907
    """Remove file from list of files to be deleted.
908

909
    @type filename: string
910
    @param filename: path to filename to be deleted
911

912
    """
913
    self._files.remove(filename)
914

    
915
  def Cleanup(self):
916
    """Delete all files marked for deletion
917

918
    """
919
    while self._files:
920
      RemoveFile(self._files.pop())