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