Better specify what packages to install
[ganeti-local] / lib / utils / io.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011, 2012 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 import pathutils
36 from ganeti.utils import filelock
37
38 #: Directory used by fsck(8) to store recovered data, usually at a file
39 #: system's root directory
40 _LOST_AND_FOUND = "lost+found"
41
42 # Possible values for keep_perms in WriteFile()
43 KP_NEVER = 0
44 KP_ALWAYS = 1
45 KP_IF_EXISTS = 2
46
47 KEEP_PERMS_VALUES = [
48   KP_NEVER,
49   KP_ALWAYS,
50   KP_IF_EXISTS,
51   ]
52
53
54 def ErrnoOrStr(err):
55   """Format an EnvironmentError exception.
56
57   If the L{err} argument has an errno attribute, it will be looked up
58   and converted into a textual C{E...} description. Otherwise the
59   string representation of the error will be returned.
60
61   @type err: L{EnvironmentError}
62   @param err: the exception to format
63
64   """
65   if hasattr(err, "errno"):
66     detail = errno.errorcode[err.errno]
67   else:
68     detail = str(err)
69   return detail
70
71
72 class FileStatHelper:
73   """Helper to store file handle's C{fstat}.
74
75   Useful in combination with L{ReadFile}'s C{preread} parameter.
76
77   """
78   def __init__(self):
79     """Initializes this class.
80
81     """
82     self.st = None
83
84   def __call__(self, fh):
85     """Calls C{fstat} on file handle.
86
87     """
88     self.st = os.fstat(fh.fileno())
89
90
91 def ReadFile(file_name, size=-1, preread=None):
92   """Reads a file.
93
94   @type size: int
95   @param size: Read at most size bytes (if negative, entire file)
96   @type preread: callable receiving file handle as single parameter
97   @param preread: Function called before file is read
98   @rtype: str
99   @return: the (possibly partial) content of the file
100
101   """
102   f = open(file_name, "r")
103   try:
104     if preread:
105       preread(f)
106
107     return f.read(size)
108   finally:
109     f.close()
110
111
112 def WriteFile(file_name, fn=None, data=None,
113               mode=None, uid=-1, gid=-1,
114               atime=None, mtime=None, close=True,
115               dry_run=False, backup=False,
116               prewrite=None, postwrite=None, keep_perms=KP_NEVER):
117   """(Over)write a file atomically.
118
119   The file_name and either fn (a function taking one argument, the
120   file descriptor, and which should write the data to it) or data (the
121   contents of the file) must be passed. The other arguments are
122   optional and allow setting the file mode, owner and group, and the
123   mtime/atime of the file.
124
125   If the function doesn't raise an exception, it has succeeded and the
126   target file has the new contents. If the function has raised an
127   exception, an existing target file should be unmodified and the
128   temporary file should be removed.
129
130   @type file_name: str
131   @param file_name: the target filename
132   @type fn: callable
133   @param fn: content writing function, called with
134       file descriptor as parameter
135   @type data: str
136   @param data: contents of the file
137   @type mode: int
138   @param mode: file mode
139   @type uid: int
140   @param uid: the owner of the file
141   @type gid: int
142   @param gid: the group of the file
143   @type atime: int
144   @param atime: a custom access time to be set on the file
145   @type mtime: int
146   @param mtime: a custom modification time to be set on the file
147   @type close: boolean
148   @param close: whether to close file after writing it
149   @type prewrite: callable
150   @param prewrite: function to be called before writing content
151   @type postwrite: callable
152   @param postwrite: function to be called after writing content
153   @type keep_perms: members of L{KEEP_PERMS_VALUES}
154   @param keep_perms: if L{KP_NEVER} (default), owner, group, and mode are
155       taken from the other parameters; if L{KP_ALWAYS}, owner, group, and
156       mode are copied from the existing file; if L{KP_IF_EXISTS}, owner,
157       group, and mode are taken from the file, and if the file doesn't
158       exist, they are taken from the other parameters. It is an error to
159       pass L{KP_ALWAYS} when the file doesn't exist or when C{uid}, C{gid},
160       or C{mode} are set to non-default values.
161
162   @rtype: None or int
163   @return: None if the 'close' parameter evaluates to True,
164       otherwise the file descriptor
165
166   @raise errors.ProgrammerError: if any of the arguments are not valid
167
168   """
169   if not os.path.isabs(file_name):
170     raise errors.ProgrammerError("Path passed to WriteFile is not"
171                                  " absolute: '%s'" % file_name)
172
173   if [fn, data].count(None) != 1:
174     raise errors.ProgrammerError("fn or data required")
175
176   if [atime, mtime].count(None) == 1:
177     raise errors.ProgrammerError("Both atime and mtime must be either"
178                                  " set or None")
179
180   if not keep_perms in KEEP_PERMS_VALUES:
181     raise errors.ProgrammerError("Invalid value for keep_perms: %s" %
182                                  keep_perms)
183   if keep_perms == KP_ALWAYS and (uid != -1 or gid != -1 or mode is not None):
184     raise errors.ProgrammerError("When keep_perms==KP_ALWAYS, 'uid', 'gid',"
185                                  " and 'mode' cannot be set")
186
187   if backup and not dry_run and os.path.isfile(file_name):
188     CreateBackup(file_name)
189
190   if keep_perms == KP_ALWAYS or keep_perms == KP_IF_EXISTS:
191     # os.stat() raises an exception if the file doesn't exist
192     try:
193       file_stat = os.stat(file_name)
194       mode = stat.S_IMODE(file_stat.st_mode)
195       uid = file_stat.st_uid
196       gid = file_stat.st_gid
197     except OSError:
198       if keep_perms == KP_ALWAYS:
199         raise
200       # else: if keeep_perms == KP_IF_EXISTS it's ok if the file doesn't exist
201
202   # Whether temporary file needs to be removed (e.g. if any error occurs)
203   do_remove = True
204
205   # Function result
206   result = None
207
208   (dir_name, base_name) = os.path.split(file_name)
209   (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
210                                     dir=dir_name)
211   try:
212     try:
213       if uid != -1 or gid != -1:
214         os.chown(new_name, uid, gid)
215       if mode:
216         os.chmod(new_name, mode)
217       if callable(prewrite):
218         prewrite(fd)
219       if data is not None:
220         if isinstance(data, unicode):
221           data = data.encode()
222         assert isinstance(data, str)
223         to_write = len(data)
224         offset = 0
225         while offset < to_write:
226           written = os.write(fd, buffer(data, offset))
227           assert written >= 0
228           assert written <= to_write - offset
229           offset += written
230         assert offset == to_write
231       else:
232         fn(fd)
233       if callable(postwrite):
234         postwrite(fd)
235       os.fsync(fd)
236       if atime is not None and mtime is not None:
237         os.utime(new_name, (atime, mtime))
238     finally:
239       # Close file unless the file descriptor should be returned
240       if close:
241         os.close(fd)
242       else:
243         result = fd
244
245     # Rename file to destination name
246     if not dry_run:
247       os.rename(new_name, file_name)
248       # Successful, no need to remove anymore
249       do_remove = False
250   finally:
251     if do_remove:
252       RemoveFile(new_name)
253
254   return result
255
256
257 def GetFileID(path=None, fd=None):
258   """Returns the file 'id', i.e. the dev/inode and mtime information.
259
260   Either the path to the file or the fd must be given.
261
262   @param path: the file path
263   @param fd: a file descriptor
264   @return: a tuple of (device number, inode number, mtime)
265
266   """
267   if [path, fd].count(None) != 1:
268     raise errors.ProgrammerError("One and only one of fd/path must be given")
269
270   if fd is None:
271     st = os.stat(path)
272   else:
273     st = os.fstat(fd)
274
275   return (st.st_dev, st.st_ino, st.st_mtime)
276
277
278 def VerifyFileID(fi_disk, fi_ours):
279   """Verifies that two file IDs are matching.
280
281   Differences in the inode/device are not accepted, but and older
282   timestamp for fi_disk is accepted.
283
284   @param fi_disk: tuple (dev, inode, mtime) representing the actual
285       file data
286   @param fi_ours: tuple (dev, inode, mtime) representing the last
287       written file data
288   @rtype: boolean
289
290   """
291   (d1, i1, m1) = fi_disk
292   (d2, i2, m2) = fi_ours
293
294   return (d1, i1) == (d2, i2) and m1 <= m2
295
296
297 def SafeWriteFile(file_name, file_id, **kwargs):
298   """Wraper over L{WriteFile} that locks the target file.
299
300   By keeping the target file locked during WriteFile, we ensure that
301   cooperating writers will safely serialise access to the file.
302
303   @type file_name: str
304   @param file_name: the target filename
305   @type file_id: tuple
306   @param file_id: a result from L{GetFileID}
307
308   """
309   fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
310   try:
311     filelock.LockFile(fd)
312     if file_id is not None:
313       disk_id = GetFileID(fd=fd)
314       if not VerifyFileID(disk_id, file_id):
315         raise errors.LockError("Cannot overwrite file %s, it has been modified"
316                                " since last written" % file_name)
317     return WriteFile(file_name, **kwargs)
318   finally:
319     os.close(fd)
320
321
322 def ReadOneLineFile(file_name, strict=False):
323   """Return the first non-empty line from a file.
324
325   @type strict: boolean
326   @param strict: if True, abort if the file has more than one
327       non-empty line
328
329   """
330   file_lines = ReadFile(file_name).splitlines()
331   full_lines = filter(bool, file_lines)
332   if not file_lines or not full_lines:
333     raise errors.GenericError("No data in one-liner file %s" % file_name)
334   elif strict and len(full_lines) > 1:
335     raise errors.GenericError("Too many lines in one-liner file %s" %
336                               file_name)
337   return full_lines[0]
338
339
340 def RemoveFile(filename):
341   """Remove a file ignoring some errors.
342
343   Remove a file, ignoring non-existing ones or directories. Other
344   errors are passed.
345
346   @type filename: str
347   @param filename: the file to be removed
348
349   """
350   try:
351     os.unlink(filename)
352   except OSError, err:
353     if err.errno not in (errno.ENOENT, errno.EISDIR):
354       raise
355
356
357 def RemoveDir(dirname):
358   """Remove an empty directory.
359
360   Remove a directory, ignoring non-existing ones.
361   Other errors are passed. This includes the case,
362   where the directory is not empty, so it can't be removed.
363
364   @type dirname: str
365   @param dirname: the empty directory to be removed
366
367   """
368   try:
369     os.rmdir(dirname)
370   except OSError, err:
371     if err.errno != errno.ENOENT:
372       raise
373
374
375 def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
376                dir_gid=None):
377   """Renames a file.
378
379   This just creates the very least directory if it does not exist and C{mkdir}
380   is set to true.
381
382   @type old: string
383   @param old: Original path
384   @type new: string
385   @param new: New path
386   @type mkdir: bool
387   @param mkdir: Whether to create target directory if it doesn't exist
388   @type mkdir_mode: int
389   @param mkdir_mode: Mode for newly created directories
390   @type dir_uid: int
391   @param dir_uid: The uid for the (if fresh created) dir
392   @type dir_gid: int
393   @param dir_gid: The gid for the (if fresh created) dir
394
395   """
396   try:
397     return os.rename(old, new)
398   except OSError, err:
399     # In at least one use case of this function, the job queue, directory
400     # creation is very rare. Checking for the directory before renaming is not
401     # as efficient.
402     if mkdir and err.errno == errno.ENOENT:
403       # Create directory and try again
404       dir_path = os.path.dirname(new)
405       MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
406
407       return os.rename(old, new)
408
409     raise
410
411
412 def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
413                       _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
414   """Enforces that given path has given permissions.
415
416   @param path: The path to the file
417   @param mode: The mode of the file
418   @param uid: The uid of the owner of this file
419   @param gid: The gid of the owner of this file
420   @param must_exist: Specifies if non-existance of path will be an error
421   @param _chmod_fn: chmod function to use (unittest only)
422   @param _chown_fn: chown function to use (unittest only)
423
424   """
425   logging.debug("Checking %s", path)
426
427   # chown takes -1 if you want to keep one part of the ownership, however
428   # None is Python standard for that. So we remap them here.
429   if uid is None:
430     uid = -1
431   if gid is None:
432     gid = -1
433
434   try:
435     st = _stat_fn(path)
436
437     fmode = stat.S_IMODE(st[stat.ST_MODE])
438     if fmode != mode:
439       logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
440       _chmod_fn(path, mode)
441
442     if max(uid, gid) > -1:
443       fuid = st[stat.ST_UID]
444       fgid = st[stat.ST_GID]
445       if fuid != uid or fgid != gid:
446         logging.debug("Changing owner of %s from UID %s/GID %s to"
447                       " UID %s/GID %s", path, fuid, fgid, uid, gid)
448         _chown_fn(path, uid, gid)
449   except EnvironmentError, err:
450     if err.errno == errno.ENOENT:
451       if must_exist:
452         raise errors.GenericError("Path %s should exist, but does not" % path)
453     else:
454       raise errors.GenericError("Error while changing permissions on %s: %s" %
455                                 (path, err))
456
457
458 def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
459                     _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
460   """Enforces that given path is a dir and has given mode, uid and gid set.
461
462   @param path: The path to the file
463   @param mode: The mode of the file
464   @param uid: The uid of the owner of this file
465   @param gid: The gid of the owner of this file
466   @param _lstat_fn: Stat function to use (unittest only)
467   @param _mkdir_fn: mkdir function to use (unittest only)
468   @param _perm_fn: permission setter function to use (unittest only)
469
470   """
471   logging.debug("Checking directory %s", path)
472   try:
473     # We don't want to follow symlinks
474     st = _lstat_fn(path)
475   except EnvironmentError, err:
476     if err.errno != errno.ENOENT:
477       raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
478     _mkdir_fn(path)
479   else:
480     if not stat.S_ISDIR(st[stat.ST_MODE]):
481       raise errors.GenericError(("Path %s is expected to be a directory, but "
482                                  "isn't") % path)
483
484   _perm_fn(path, mode, uid=uid, gid=gid)
485
486
487 def Makedirs(path, mode=0750):
488   """Super-mkdir; create a leaf directory and all intermediate ones.
489
490   This is a wrapper around C{os.makedirs} adding error handling not implemented
491   before Python 2.5.
492
493   """
494   try:
495     os.makedirs(path, mode)
496   except OSError, err:
497     # Ignore EEXIST. This is only handled in os.makedirs as included in
498     # Python 2.5 and above.
499     if err.errno != errno.EEXIST or not os.path.exists(path):
500       raise
501
502
503 def TimestampForFilename():
504   """Returns the current time formatted for filenames.
505
506   The format doesn't contain colons as some shells and applications treat them
507   as separators. Uses the local timezone.
508
509   """
510   return time.strftime("%Y-%m-%d_%H_%M_%S")
511
512
513 def CreateBackup(file_name):
514   """Creates a backup of a file.
515
516   @type file_name: str
517   @param file_name: file to be backed up
518   @rtype: str
519   @return: the path to the newly created backup
520   @raise errors.ProgrammerError: for invalid file names
521
522   """
523   if not os.path.isfile(file_name):
524     raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
525                                  file_name)
526
527   prefix = ("%s.backup-%s." %
528             (os.path.basename(file_name), TimestampForFilename()))
529   dir_name = os.path.dirname(file_name)
530
531   fsrc = open(file_name, "rb")
532   try:
533     (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
534     fdst = os.fdopen(fd, "wb")
535     try:
536       logging.debug("Backing up %s at %s", file_name, backup_name)
537       shutil.copyfileobj(fsrc, fdst)
538     finally:
539       fdst.close()
540   finally:
541     fsrc.close()
542
543   return backup_name
544
545
546 def ListVisibleFiles(path, _is_mountpoint=os.path.ismount):
547   """Returns a list of visible files in a directory.
548
549   @type path: str
550   @param path: the directory to enumerate
551   @rtype: list
552   @return: the list of all files not starting with a dot
553   @raise ProgrammerError: if L{path} is not an absolue and normalized path
554
555   """
556   if not IsNormAbsPath(path):
557     raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
558                                  " absolute/normalized: '%s'" % path)
559
560   mountpoint = _is_mountpoint(path)
561
562   def fn(name):
563     """File name filter.
564
565     Ignores files starting with a dot (".") as by Unix convention they're
566     considered hidden. The "lost+found" directory found at the root of some
567     filesystems is also hidden.
568
569     """
570     return not (name.startswith(".") or
571                 (mountpoint and name == _LOST_AND_FOUND and
572                  os.path.isdir(os.path.join(path, name))))
573
574   return filter(fn, os.listdir(path))
575
576
577 def EnsureDirs(dirs):
578   """Make required directories, if they don't exist.
579
580   @param dirs: list of tuples (dir_name, dir_mode)
581   @type dirs: list of (string, integer)
582
583   """
584   for dir_name, dir_mode in dirs:
585     try:
586       os.mkdir(dir_name, dir_mode)
587     except EnvironmentError, err:
588       if err.errno != errno.EEXIST:
589         raise errors.GenericError("Cannot create needed directory"
590                                   " '%s': %s" % (dir_name, err))
591     try:
592       os.chmod(dir_name, dir_mode)
593     except EnvironmentError, err:
594       raise errors.GenericError("Cannot change directory permissions on"
595                                 " '%s': %s" % (dir_name, err))
596     if not os.path.isdir(dir_name):
597       raise errors.GenericError("%s is not a directory" % dir_name)
598
599
600 def FindFile(name, search_path, test=os.path.exists):
601   """Look for a filesystem object in a given path.
602
603   This is an abstract method to search for filesystem object (files,
604   dirs) under a given search path.
605
606   @type name: str
607   @param name: the name to look for
608   @type search_path: str
609   @param search_path: location to start at
610   @type test: callable
611   @param test: a function taking one argument that should return True
612       if the a given object is valid; the default value is
613       os.path.exists, causing only existing files to be returned
614   @rtype: str or None
615   @return: full path to the object if found, None otherwise
616
617   """
618   # validate the filename mask
619   if constants.EXT_PLUGIN_MASK.match(name) is None:
620     logging.critical("Invalid value passed for external script name: '%s'",
621                      name)
622     return None
623
624   for dir_name in search_path:
625     # FIXME: investigate switch to PathJoin
626     item_name = os.path.sep.join([dir_name, name])
627     # check the user test and that we're indeed resolving to the given
628     # basename
629     if test(item_name) and os.path.basename(item_name) == name:
630       return item_name
631   return None
632
633
634 def IsNormAbsPath(path):
635   """Check whether a path is absolute and also normalized
636
637   This avoids things like /dir/../../other/path to be valid.
638
639   """
640   return os.path.normpath(path) == path and os.path.isabs(path)
641
642
643 def IsBelowDir(root, other_path):
644   """Check whether a path is below a root dir.
645
646   This works around the nasty byte-byte comparison of commonprefix.
647
648   """
649   if not (os.path.isabs(root) and os.path.isabs(other_path)):
650     raise ValueError("Provided paths '%s' and '%s' are not absolute" %
651                      (root, other_path))
652
653   norm_other = os.path.normpath(other_path)
654
655   if norm_other == os.sep:
656     # The root directory can never be below another path
657     return False
658
659   norm_root = os.path.normpath(root)
660
661   if norm_root == os.sep:
662     # This is the root directory, no need to add another slash
663     prepared_root = norm_root
664   else:
665     prepared_root = "%s%s" % (norm_root, os.sep)
666
667   return os.path.commonprefix([prepared_root, norm_other]) == prepared_root
668
669
670 def PathJoin(*args):
671   """Safe-join a list of path components.
672
673   Requirements:
674       - the first argument must be an absolute path
675       - no component in the path must have backtracking (e.g. /../),
676         since we check for normalization at the end
677
678   @param args: the path components to be joined
679   @raise ValueError: for invalid paths
680
681   """
682   # ensure we're having at least one path passed in
683   assert args
684   # ensure the first component is an absolute and normalized path name
685   root = args[0]
686   if not IsNormAbsPath(root):
687     raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
688   result = os.path.join(*args)
689   # ensure that the whole path is normalized
690   if not IsNormAbsPath(result):
691     raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
692   # check that we're still under the original prefix
693   if not IsBelowDir(root, result):
694     raise ValueError("Error: path joining resulted in different prefix"
695                      " (%s != %s)" % (result, root))
696   return result
697
698
699 def TailFile(fname, lines=20):
700   """Return the last lines from a file.
701
702   @note: this function will only read and parse the last 4KB of
703       the file; if the lines are very long, it could be that less
704       than the requested number of lines are returned
705
706   @param fname: the file name
707   @type lines: int
708   @param lines: the (maximum) number of lines to return
709
710   """
711   fd = open(fname, "r")
712   try:
713     fd.seek(0, 2)
714     pos = fd.tell()
715     pos = max(0, pos - 4096)
716     fd.seek(pos, 0)
717     raw_data = fd.read()
718   finally:
719     fd.close()
720
721   rows = raw_data.splitlines()
722   return rows[-lines:]
723
724
725 def BytesToMebibyte(value):
726   """Converts bytes to mebibytes.
727
728   @type value: int
729   @param value: Value in bytes
730   @rtype: int
731   @return: Value in mebibytes
732
733   """
734   return int(round(value / (1024.0 * 1024.0), 0))
735
736
737 def CalculateDirectorySize(path):
738   """Calculates the size of a directory recursively.
739
740   @type path: string
741   @param path: Path to directory
742   @rtype: int
743   @return: Size in mebibytes
744
745   """
746   size = 0
747
748   for (curpath, _, files) in os.walk(path):
749     for filename in files:
750       st = os.lstat(PathJoin(curpath, filename))
751       size += st.st_size
752
753   return BytesToMebibyte(size)
754
755
756 def GetFilesystemStats(path):
757   """Returns the total and free space on a filesystem.
758
759   @type path: string
760   @param path: Path on filesystem to be examined
761   @rtype: int
762   @return: tuple of (Total space, Free space) in mebibytes
763
764   """
765   st = os.statvfs(path)
766
767   fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
768   tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
769   return (tsize, fsize)
770
771
772 def ReadPidFile(pidfile):
773   """Read a pid from a file.
774
775   @type  pidfile: string
776   @param pidfile: path to the file containing the pid
777   @rtype: int
778   @return: The process id, if the file exists and contains a valid PID,
779            otherwise 0
780
781   """
782   try:
783     raw_data = ReadOneLineFile(pidfile)
784   except EnvironmentError, err:
785     if err.errno != errno.ENOENT:
786       logging.exception("Can't read pid file")
787     return 0
788
789   return _ParsePidFileContents(raw_data)
790
791
792 def _ParsePidFileContents(data):
793   """Tries to extract a process ID from a PID file's content.
794
795   @type data: string
796   @rtype: int
797   @return: Zero if nothing could be read, PID otherwise
798
799   """
800   try:
801     pid = int(data)
802   except (TypeError, ValueError):
803     logging.info("Can't parse pid file contents", exc_info=True)
804     return 0
805   else:
806     return pid
807
808
809 def ReadLockedPidFile(path):
810   """Reads a locked PID file.
811
812   This can be used together with L{utils.process.StartDaemon}.
813
814   @type path: string
815   @param path: Path to PID file
816   @return: PID as integer or, if file was unlocked or couldn't be opened, None
817
818   """
819   try:
820     fd = os.open(path, os.O_RDONLY)
821   except EnvironmentError, err:
822     if err.errno == errno.ENOENT:
823       # PID file doesn't exist
824       return None
825     raise
826
827   try:
828     try:
829       # Try to acquire lock
830       filelock.LockFile(fd)
831     except errors.LockError:
832       # Couldn't lock, daemon is running
833       return int(os.read(fd, 100))
834   finally:
835     os.close(fd)
836
837   return None
838
839
840 def _SplitSshKey(key):
841   """Splits a line for SSH's C{authorized_keys} file.
842
843   If the line has no options (e.g. no C{command="..."}), only the significant
844   parts, the key type and its hash, are used. Otherwise the whole line is used
845   (split at whitespace).
846
847   @type key: string
848   @param key: Key line
849   @rtype: tuple
850
851   """
852   parts = key.split()
853
854   if parts and parts[0] in constants.SSHAK_ALL:
855     # If the key has no options in front of it, we only want the significant
856     # fields
857     return (False, parts[:2])
858   else:
859     # Can't properly split the line, so use everything
860     return (True, parts)
861
862
863 def AddAuthorizedKey(file_obj, key):
864   """Adds an SSH public key to an authorized_keys file.
865
866   @type file_obj: str or file handle
867   @param file_obj: path to authorized_keys file
868   @type key: str
869   @param key: string containing key
870
871   """
872   key_fields = _SplitSshKey(key)
873
874   if isinstance(file_obj, basestring):
875     f = open(file_obj, "a+")
876   else:
877     f = file_obj
878
879   try:
880     nl = True
881     for line in f:
882       # Ignore whitespace changes
883       if _SplitSshKey(line) == key_fields:
884         break
885       nl = line.endswith("\n")
886     else:
887       if not nl:
888         f.write("\n")
889       f.write(key.rstrip("\r\n"))
890       f.write("\n")
891       f.flush()
892   finally:
893     f.close()
894
895
896 def RemoveAuthorizedKey(file_name, key):
897   """Removes an SSH public key from an authorized_keys file.
898
899   @type file_name: str
900   @param file_name: path to authorized_keys file
901   @type key: str
902   @param key: string containing key
903
904   """
905   key_fields = _SplitSshKey(key)
906
907   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
908   try:
909     out = os.fdopen(fd, "w")
910     try:
911       f = open(file_name, "r")
912       try:
913         for line in f:
914           # Ignore whitespace changes while comparing lines
915           if _SplitSshKey(line) != key_fields:
916             out.write(line)
917
918         out.flush()
919         os.rename(tmpname, file_name)
920       finally:
921         f.close()
922     finally:
923       out.close()
924   except:
925     RemoveFile(tmpname)
926     raise
927
928
929 def DaemonPidFileName(name):
930   """Compute a ganeti pid file absolute path
931
932   @type name: str
933   @param name: the daemon name
934   @rtype: str
935   @return: the full path to the pidfile corresponding to the given
936       daemon name
937
938   """
939   return PathJoin(pathutils.RUN_DIR, "%s.pid" % name)
940
941
942 def WritePidFile(pidfile):
943   """Write the current process pidfile.
944
945   @type pidfile: string
946   @param pidfile: the path to the file to be written
947   @raise errors.LockError: if the pid file already exists and
948       points to a live process
949   @rtype: int
950   @return: the file descriptor of the lock file; do not close this unless
951       you want to unlock the pid file
952
953   """
954   # We don't rename nor truncate the file to not drop locks under
955   # existing processes
956   fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600)
957
958   # Lock the PID file (and fail if not possible to do so). Any code
959   # wanting to send a signal to the daemon should try to lock the PID
960   # file before reading it. If acquiring the lock succeeds, the daemon is
961   # no longer running and the signal should not be sent.
962   try:
963     filelock.LockFile(fd_pidfile)
964   except errors.LockError:
965     msg = ["PID file '%s' is already locked by another process" % pidfile]
966     # Try to read PID file
967     pid = _ParsePidFileContents(os.read(fd_pidfile, 100))
968     if pid > 0:
969       msg.append(", PID read from file is %s" % pid)
970     raise errors.PidFileLockError("".join(msg))
971
972   os.write(fd_pidfile, "%d\n" % os.getpid())
973
974   return fd_pidfile
975
976
977 def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
978   """Reads the watcher pause file.
979
980   @type filename: string
981   @param filename: Path to watcher pause file
982   @type now: None, float or int
983   @param now: Current time as Unix timestamp
984   @type remove_after: int
985   @param remove_after: Remove watcher pause file after specified amount of
986     seconds past the pause end time
987
988   """
989   if now is None:
990     now = time.time()
991
992   try:
993     value = ReadFile(filename)
994   except IOError, err:
995     if err.errno != errno.ENOENT:
996       raise
997     value = None
998
999   if value is not None:
1000     try:
1001       value = int(value)
1002     except ValueError:
1003       logging.warning(("Watcher pause file (%s) contains invalid value,"
1004                        " removing it"), filename)
1005       RemoveFile(filename)
1006       value = None
1007
1008     if value is not None:
1009       # Remove file if it's outdated
1010       if now > (value + remove_after):
1011         RemoveFile(filename)
1012         value = None
1013
1014       elif now > value:
1015         value = None
1016
1017   return value
1018
1019
1020 def NewUUID():
1021   """Returns a random UUID.
1022
1023   @note: This is a Linux-specific method as it uses the /proc
1024       filesystem.
1025   @rtype: str
1026
1027   """
1028   return ReadFile(constants.RANDOM_UUID_FILE, size=128).rstrip("\n")
1029
1030
1031 class TemporaryFileManager(object):
1032   """Stores the list of files to be deleted and removes them on demand.
1033
1034   """
1035
1036   def __init__(self):
1037     self._files = []
1038
1039   def __del__(self):
1040     self.Cleanup()
1041
1042   def Add(self, filename):
1043     """Add file to list of files to be deleted.
1044
1045     @type filename: string
1046     @param filename: path to filename to be added
1047
1048     """
1049     self._files.append(filename)
1050
1051   def Remove(self, filename):
1052     """Remove file from list of files to be deleted.
1053
1054     @type filename: string
1055     @param filename: path to filename to be deleted
1056
1057     """
1058     self._files.remove(filename)
1059
1060   def Cleanup(self):
1061     """Delete all files marked for deletion
1062
1063     """
1064     while self._files:
1065       RemoveFile(self._files.pop())