Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ eb93b673

History | View | Annotate | Download (25.3 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 ErrnoOrStr(err):
43
  """Format an EnvironmentError exception.
44

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

49
  @type err: L{EnvironmentError}
50
  @param err: the exception to format
51

52
  """
53
  if hasattr(err, "errno"):
54
    detail = errno.errorcode[err.errno]
55
  else:
56
    detail = str(err)
57
  return detail
58

    
59

    
60
def ReadFile(file_name, size=-1, preread=None):
61
  """Reads a file.
62

63
  @type size: int
64
  @param size: Read at most size bytes (if negative, entire file)
65
  @type preread: callable receiving file handle as single parameter
66
  @param preread: Function called before file is read
67
  @rtype: str
68
  @return: the (possibly partial) content of the file
69

70
  """
71
  f = open(file_name, "r")
72
  try:
73
    if preread:
74
      preread(f)
75

    
76
    return f.read(size)
77
  finally:
78
    f.close()
79

    
80

    
81
def WriteFile(file_name, fn=None, data=None,
82
              mode=None, uid=-1, gid=-1,
83
              atime=None, mtime=None, close=True,
84
              dry_run=False, backup=False,
85
              prewrite=None, postwrite=None):
86
  """(Over)write a file atomically.
87

88
  The file_name and either fn (a function taking one argument, the
89
  file descriptor, and which should write the data to it) or data (the
90
  contents of the file) must be passed. The other arguments are
91
  optional and allow setting the file mode, owner and group, and the
92
  mtime/atime of the file.
93

94
  If the function doesn't raise an exception, it has succeeded and the
95
  target file has the new contents. If the function has raised an
96
  exception, an existing target file should be unmodified and the
97
  temporary file should be removed.
98

99
  @type file_name: str
100
  @param file_name: the target filename
101
  @type fn: callable
102
  @param fn: content writing function, called with
103
      file descriptor as parameter
104
  @type data: str
105
  @param data: contents of the file
106
  @type mode: int
107
  @param mode: file mode
108
  @type uid: int
109
  @param uid: the owner of the file
110
  @type gid: int
111
  @param gid: the group of the file
112
  @type atime: int
113
  @param atime: a custom access time to be set on the file
114
  @type mtime: int
115
  @param mtime: a custom modification time to be set on the file
116
  @type close: boolean
117
  @param close: whether to close file after writing it
118
  @type prewrite: callable
119
  @param prewrite: function to be called before writing content
120
  @type postwrite: callable
121
  @param postwrite: function to be called after writing content
122

123
  @rtype: None or int
124
  @return: None if the 'close' parameter evaluates to True,
125
      otherwise the file descriptor
126

127
  @raise errors.ProgrammerError: if any of the arguments are not valid
128

129
  """
130
  if not os.path.isabs(file_name):
131
    raise errors.ProgrammerError("Path passed to WriteFile is not"
132
                                 " absolute: '%s'" % file_name)
133

    
134
  if [fn, data].count(None) != 1:
135
    raise errors.ProgrammerError("fn or data required")
136

    
137
  if [atime, mtime].count(None) == 1:
138
    raise errors.ProgrammerError("Both atime and mtime must be either"
139
                                 " set or None")
140

    
141
  if backup and not dry_run and os.path.isfile(file_name):
142
    CreateBackup(file_name)
143

    
144
  # Whether temporary file needs to be removed (e.g. if any error occurs)
145
  do_remove = True
146

    
147
  # Function result
148
  result = None
149

    
150
  (dir_name, base_name) = os.path.split(file_name)
151
  (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
152
                                    dir=dir_name)
153
  try:
154
    try:
155
      if uid != -1 or gid != -1:
156
        os.chown(new_name, uid, gid)
157
      if mode:
158
        os.chmod(new_name, mode)
159
      if callable(prewrite):
160
        prewrite(fd)
161
      if data is not None:
162
        if isinstance(data, unicode):
163
          data = data.encode()
164
        assert isinstance(data, str)
165
        to_write = len(data)
166
        offset = 0
167
        while offset < to_write:
168
          written = os.write(fd, buffer(data, offset))
169
          assert written >= 0
170
          assert written <= to_write - offset
171
          offset += written
172
        assert offset == to_write
173
      else:
174
        fn(fd)
175
      if callable(postwrite):
176
        postwrite(fd)
177
      os.fsync(fd)
178
      if atime is not None and mtime is not None:
179
        os.utime(new_name, (atime, mtime))
180
    finally:
181
      # Close file unless the file descriptor should be returned
182
      if close:
183
        os.close(fd)
184
      else:
185
        result = fd
186

    
187
    # Rename file to destination name
188
    if not dry_run:
189
      os.rename(new_name, file_name)
190
      # Successful, no need to remove anymore
191
      do_remove = False
192
  finally:
193
    if do_remove:
194
      RemoveFile(new_name)
195

    
196
  return result
197

    
198

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

202
  Either the path to the file or the fd must be given.
203

204
  @param path: the file path
205
  @param fd: a file descriptor
206
  @return: a tuple of (device number, inode number, mtime)
207

208
  """
209
  if [path, fd].count(None) != 1:
210
    raise errors.ProgrammerError("One and only one of fd/path must be given")
211

    
212
  if fd is None:
213
    st = os.stat(path)
214
  else:
215
    st = os.fstat(fd)
216

    
217
  return (st.st_dev, st.st_ino, st.st_mtime)
218

    
219

    
220
def VerifyFileID(fi_disk, fi_ours):
221
  """Verifies that two file IDs are matching.
222

223
  Differences in the inode/device are not accepted, but and older
224
  timestamp for fi_disk is accepted.
225

226
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
227
      file data
228
  @param fi_ours: tuple (dev, inode, mtime) representing the last
229
      written file data
230
  @rtype: boolean
231

232
  """
233
  (d1, i1, m1) = fi_disk
234
  (d2, i2, m2) = fi_ours
235

    
236
  return (d1, i1) == (d2, i2) and m1 <= m2
237

    
238

    
239
def SafeWriteFile(file_name, file_id, **kwargs):
240
  """Wraper over L{WriteFile} that locks the target file.
241

242
  By keeping the target file locked during WriteFile, we ensure that
243
  cooperating writers will safely serialise access to the file.
244

245
  @type file_name: str
246
  @param file_name: the target filename
247
  @type file_id: tuple
248
  @param file_id: a result from L{GetFileID}
249

250
  """
251
  fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
252
  try:
253
    filelock.LockFile(fd)
254
    if file_id is not None:
255
      disk_id = GetFileID(fd=fd)
256
      if not VerifyFileID(disk_id, file_id):
257
        raise errors.LockError("Cannot overwrite file %s, it has been modified"
258
                               " since last written" % file_name)
259
    return WriteFile(file_name, **kwargs)
260
  finally:
261
    os.close(fd)
262

    
263

    
264
def ReadOneLineFile(file_name, strict=False):
265
  """Return the first non-empty line from a file.
266

267
  @type strict: boolean
268
  @param strict: if True, abort if the file has more than one
269
      non-empty line
270

271
  """
272
  file_lines = ReadFile(file_name).splitlines()
273
  full_lines = filter(bool, file_lines)
274
  if not file_lines or not full_lines:
275
    raise errors.GenericError("No data in one-liner file %s" % file_name)
276
  elif strict and len(full_lines) > 1:
277
    raise errors.GenericError("Too many lines in one-liner file %s" %
278
                              file_name)
279
  return full_lines[0]
280

    
281

    
282
def RemoveFile(filename):
283
  """Remove a file ignoring some errors.
284

285
  Remove a file, ignoring non-existing ones or directories. Other
286
  errors are passed.
287

288
  @type filename: str
289
  @param filename: the file to be removed
290

291
  """
292
  try:
293
    os.unlink(filename)
294
  except OSError, err:
295
    if err.errno not in (errno.ENOENT, errno.EISDIR):
296
      raise
297

    
298

    
299
def RemoveDir(dirname):
300
  """Remove an empty directory.
301

302
  Remove a directory, ignoring non-existing ones.
303
  Other errors are passed. This includes the case,
304
  where the directory is not empty, so it can't be removed.
305

306
  @type dirname: str
307
  @param dirname: the empty directory to be removed
308

309
  """
310
  try:
311
    os.rmdir(dirname)
312
  except OSError, err:
313
    if err.errno != errno.ENOENT:
314
      raise
315

    
316

    
317
def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
318
               dir_gid=None):
319
  """Renames a file.
320

321
  This just creates the very least directory if it does not exist and C{mkdir}
322
  is set to true.
323

324
  @type old: string
325
  @param old: Original path
326
  @type new: string
327
  @param new: New path
328
  @type mkdir: bool
329
  @param mkdir: Whether to create target directory if it doesn't exist
330
  @type mkdir_mode: int
331
  @param mkdir_mode: Mode for newly created directories
332
  @type dir_uid: int
333
  @param dir_uid: The uid for the (if fresh created) dir
334
  @type dir_gid: int
335
  @param dir_gid: The gid for the (if fresh created) dir
336

337
  """
338
  try:
339
    return os.rename(old, new)
340
  except OSError, err:
341
    # In at least one use case of this function, the job queue, directory
342
    # creation is very rare. Checking for the directory before renaming is not
343
    # as efficient.
344
    if mkdir and err.errno == errno.ENOENT:
345
      # Create directory and try again
346
      dir_path = os.path.dirname(new)
347
      MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
348

    
349
      return os.rename(old, new)
350

    
351
    raise
352

    
353

    
354
def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
355
                      _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
356
  """Enforces that given path has given permissions.
357

358
  @param path: The path to the file
359
  @param mode: The mode of the file
360
  @param uid: The uid of the owner of this file
361
  @param gid: The gid of the owner of this file
362
  @param must_exist: Specifies if non-existance of path will be an error
363
  @param _chmod_fn: chmod function to use (unittest only)
364
  @param _chown_fn: chown function to use (unittest only)
365

366
  """
367
  logging.debug("Checking %s", path)
368

    
369
  # chown takes -1 if you want to keep one part of the ownership, however
370
  # None is Python standard for that. So we remap them here.
371
  if uid is None:
372
    uid = -1
373
  if gid is None:
374
    gid = -1
375

    
376
  try:
377
    st = _stat_fn(path)
378

    
379
    fmode = stat.S_IMODE(st[stat.ST_MODE])
380
    if fmode != mode:
381
      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
382
      _chmod_fn(path, mode)
383

    
384
    if max(uid, gid) > -1:
385
      fuid = st[stat.ST_UID]
386
      fgid = st[stat.ST_GID]
387
      if fuid != uid or fgid != gid:
388
        logging.debug("Changing owner of %s from UID %s/GID %s to"
389
                      " UID %s/GID %s", path, fuid, fgid, uid, gid)
390
        _chown_fn(path, uid, gid)
391
  except EnvironmentError, err:
392
    if err.errno == errno.ENOENT:
393
      if must_exist:
394
        raise errors.GenericError("Path %s should exist, but does not" % path)
395
    else:
396
      raise errors.GenericError("Error while changing permissions on %s: %s" %
397
                                (path, err))
398

    
399

    
400
def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
401
                    _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
402
  """Enforces that given path is a dir and has given mode, uid and gid set.
403

404
  @param path: The path to the file
405
  @param mode: The mode of the file
406
  @param uid: The uid of the owner of this file
407
  @param gid: The gid of the owner of this file
408
  @param _lstat_fn: Stat function to use (unittest only)
409
  @param _mkdir_fn: mkdir function to use (unittest only)
410
  @param _perm_fn: permission setter function to use (unittest only)
411

412
  """
413
  logging.debug("Checking directory %s", path)
414
  try:
415
    # We don't want to follow symlinks
416
    st = _lstat_fn(path)
417
  except EnvironmentError, err:
418
    if err.errno != errno.ENOENT:
419
      raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
420
    _mkdir_fn(path)
421
  else:
422
    if not stat.S_ISDIR(st[stat.ST_MODE]):
423
      raise errors.GenericError(("Path %s is expected to be a directory, but "
424
                                 "isn't") % path)
425

    
426
  _perm_fn(path, mode, uid=uid, gid=gid)
427

    
428

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

432
  This is a wrapper around C{os.makedirs} adding error handling not implemented
433
  before Python 2.5.
434

435
  """
436
  try:
437
    os.makedirs(path, mode)
438
  except OSError, err:
439
    # Ignore EEXIST. This is only handled in os.makedirs as included in
440
    # Python 2.5 and above.
441
    if err.errno != errno.EEXIST or not os.path.exists(path):
442
      raise
443

    
444

    
445
def TimestampForFilename():
446
  """Returns the current time formatted for filenames.
447

448
  The format doesn't contain colons as some shells and applications treat them
449
  as separators. Uses the local timezone.
450

451
  """
452
  return time.strftime("%Y-%m-%d_%H_%M_%S")
453

    
454

    
455
def CreateBackup(file_name):
456
  """Creates a backup of a file.
457

458
  @type file_name: str
459
  @param file_name: file to be backed up
460
  @rtype: str
461
  @return: the path to the newly created backup
462
  @raise errors.ProgrammerError: for invalid file names
463

464
  """
465
  if not os.path.isfile(file_name):
466
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
467
                                file_name)
468

    
469
  prefix = ("%s.backup-%s." %
470
            (os.path.basename(file_name), TimestampForFilename()))
471
  dir_name = os.path.dirname(file_name)
472

    
473
  fsrc = open(file_name, "rb")
474
  try:
475
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
476
    fdst = os.fdopen(fd, "wb")
477
    try:
478
      logging.debug("Backing up %s at %s", file_name, backup_name)
479
      shutil.copyfileobj(fsrc, fdst)
480
    finally:
481
      fdst.close()
482
  finally:
483
    fsrc.close()
484

    
485
  return backup_name
486

    
487

    
488
def ListVisibleFiles(path):
489
  """Returns a list of visible files in a directory.
490

491
  @type path: str
492
  @param path: the directory to enumerate
493
  @rtype: list
494
  @return: the list of all files not starting with a dot
495
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
496

497
  """
498
  if not IsNormAbsPath(path):
499
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
500
                                 " absolute/normalized: '%s'" % path)
501
  files = [i for i in os.listdir(path) if not i.startswith(".")]
502
  return files
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")
902

    
903

    
904
class TemporaryFileManager(object):
905
  """Stores the list of files to be deleted and removes them on demand.
906

907
  """
908

    
909
  def __init__(self):
910
    self._files = []
911

    
912
  def __del__(self):
913
    self.Cleanup()
914

    
915
  def Add(self, filename):
916
    """Add file to list of files to be deleted.
917

918
    @type filename: string
919
    @param filename: path to filename to be added
920

921
    """
922
    self._files.append(filename)
923

    
924
  def Remove(self, filename):
925
    """Remove file from list of files to be deleted.
926

927
    @type filename: string
928
    @param filename: path to filename to be deleted
929

930
    """
931
    self._files.remove(filename)
932

    
933
  def Cleanup(self):
934
    """Delete all files marked for deletion
935

936
    """
937
    while self._files:
938
      RemoveFile(self._files.pop())