c64c125d1de97f65dbaf168b9116c7d8133bb69f
[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
32 from ganeti import errors
33 from ganeti import constants
34 from ganeti.utils import filelock
35
36
37 #: Path generating random UUID
38 _RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
39
40
41 def ReadFile(file_name, size=-1, preread=None):
42   """Reads a file.
43
44   @type size: int
45   @param size: Read at most size bytes (if negative, entire file)
46   @type preread: callable receiving file handle as single parameter
47   @param preread: Function called before file is read
48   @rtype: str
49   @return: the (possibly partial) content of the file
50
51   """
52   f = open(file_name, "r")
53   try:
54     if preread:
55       preread(f)
56
57     return f.read(size)
58   finally:
59     f.close()
60
61
62 def WriteFile(file_name, fn=None, data=None,
63               mode=None, uid=-1, gid=-1,
64               atime=None, mtime=None, close=True,
65               dry_run=False, backup=False,
66               prewrite=None, postwrite=None):
67   """(Over)write a file atomically.
68
69   The file_name and either fn (a function taking one argument, the
70   file descriptor, and which should write the data to it) or data (the
71   contents of the file) must be passed. The other arguments are
72   optional and allow setting the file mode, owner and group, and the
73   mtime/atime of the file.
74
75   If the function doesn't raise an exception, it has succeeded and the
76   target file has the new contents. If the function has raised an
77   exception, an existing target file should be unmodified and the
78   temporary file should be removed.
79
80   @type file_name: str
81   @param file_name: the target filename
82   @type fn: callable
83   @param fn: content writing function, called with
84       file descriptor as parameter
85   @type data: str
86   @param data: contents of the file
87   @type mode: int
88   @param mode: file mode
89   @type uid: int
90   @param uid: the owner of the file
91   @type gid: int
92   @param gid: the group of the file
93   @type atime: int
94   @param atime: a custom access time to be set on the file
95   @type mtime: int
96   @param mtime: a custom modification time to be set on the file
97   @type close: boolean
98   @param close: whether to close file after writing it
99   @type prewrite: callable
100   @param prewrite: function to be called before writing content
101   @type postwrite: callable
102   @param postwrite: function to be called after writing content
103
104   @rtype: None or int
105   @return: None if the 'close' parameter evaluates to True,
106       otherwise the file descriptor
107
108   @raise errors.ProgrammerError: if any of the arguments are not valid
109
110   """
111   if not os.path.isabs(file_name):
112     raise errors.ProgrammerError("Path passed to WriteFile is not"
113                                  " absolute: '%s'" % file_name)
114
115   if [fn, data].count(None) != 1:
116     raise errors.ProgrammerError("fn or data required")
117
118   if [atime, mtime].count(None) == 1:
119     raise errors.ProgrammerError("Both atime and mtime must be either"
120                                  " set or None")
121
122   if backup and not dry_run and os.path.isfile(file_name):
123     CreateBackup(file_name)
124
125   # Whether temporary file needs to be removed (e.g. if any error occurs)
126   do_remove = True
127
128   # Function result
129   result = None
130
131   (dir_name, base_name) = os.path.split(file_name)
132   (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
133                                     dir=dir_name)
134   try:
135     try:
136       if uid != -1 or gid != -1:
137         os.chown(new_name, uid, gid)
138       if mode:
139         os.chmod(new_name, mode)
140       if callable(prewrite):
141         prewrite(fd)
142       if data is not None:
143         if isinstance(data, unicode):
144           data = data.encode()
145         assert isinstance(data, str)
146         to_write = len(data)
147         offset = 0
148         while offset < to_write:
149           written = os.write(fd, buffer(data, offset))
150           assert written >= 0
151           assert written <= to_write - offset
152           offset += written
153         assert offset == to_write
154       else:
155         fn(fd)
156       if callable(postwrite):
157         postwrite(fd)
158       os.fsync(fd)
159       if atime is not None and mtime is not None:
160         os.utime(new_name, (atime, mtime))
161     finally:
162       # Close file unless the file descriptor should be returned
163       if close:
164         os.close(fd)
165       else:
166         result = fd
167
168     # Rename file to destination name
169     if not dry_run:
170       os.rename(new_name, file_name)
171       # Successful, no need to remove anymore
172       do_remove = False
173   finally:
174     if do_remove:
175       RemoveFile(new_name)
176
177   return result
178
179
180 def GetFileID(path=None, fd=None):
181   """Returns the file 'id', i.e. the dev/inode and mtime information.
182
183   Either the path to the file or the fd must be given.
184
185   @param path: the file path
186   @param fd: a file descriptor
187   @return: a tuple of (device number, inode number, mtime)
188
189   """
190   if [path, fd].count(None) != 1:
191     raise errors.ProgrammerError("One and only one of fd/path must be given")
192
193   if fd is None:
194     st = os.stat(path)
195   else:
196     st = os.fstat(fd)
197
198   return (st.st_dev, st.st_ino, st.st_mtime)
199
200
201 def VerifyFileID(fi_disk, fi_ours):
202   """Verifies that two file IDs are matching.
203
204   Differences in the inode/device are not accepted, but and older
205   timestamp for fi_disk is accepted.
206
207   @param fi_disk: tuple (dev, inode, mtime) representing the actual
208       file data
209   @param fi_ours: tuple (dev, inode, mtime) representing the last
210       written file data
211   @rtype: boolean
212
213   """
214   (d1, i1, m1) = fi_disk
215   (d2, i2, m2) = fi_ours
216
217   return (d1, i1) == (d2, i2) and m1 <= m2
218
219
220 def SafeWriteFile(file_name, file_id, **kwargs):
221   """Wraper over L{WriteFile} that locks the target file.
222
223   By keeping the target file locked during WriteFile, we ensure that
224   cooperating writers will safely serialise access to the file.
225
226   @type file_name: str
227   @param file_name: the target filename
228   @type file_id: tuple
229   @param file_id: a result from L{GetFileID}
230
231   """
232   fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
233   try:
234     filelock.LockFile(fd)
235     if file_id is not None:
236       disk_id = GetFileID(fd=fd)
237       if not VerifyFileID(disk_id, file_id):
238         raise errors.LockError("Cannot overwrite file %s, it has been modified"
239                                " since last written" % file_name)
240     return WriteFile(file_name, **kwargs)
241   finally:
242     os.close(fd)
243
244
245 def ReadOneLineFile(file_name, strict=False):
246   """Return the first non-empty line from a file.
247
248   @type strict: boolean
249   @param strict: if True, abort if the file has more than one
250       non-empty line
251
252   """
253   file_lines = ReadFile(file_name).splitlines()
254   full_lines = filter(bool, file_lines)
255   if not file_lines or not full_lines:
256     raise errors.GenericError("No data in one-liner file %s" % file_name)
257   elif strict and len(full_lines) > 1:
258     raise errors.GenericError("Too many lines in one-liner file %s" %
259                               file_name)
260   return full_lines[0]
261
262
263 def RemoveFile(filename):
264   """Remove a file ignoring some errors.
265
266   Remove a file, ignoring non-existing ones or directories. Other
267   errors are passed.
268
269   @type filename: str
270   @param filename: the file to be removed
271
272   """
273   try:
274     os.unlink(filename)
275   except OSError, err:
276     if err.errno not in (errno.ENOENT, errno.EISDIR):
277       raise
278
279
280 def RemoveDir(dirname):
281   """Remove an empty directory.
282
283   Remove a directory, ignoring non-existing ones.
284   Other errors are passed. This includes the case,
285   where the directory is not empty, so it can't be removed.
286
287   @type dirname: str
288   @param dirname: the empty directory to be removed
289
290   """
291   try:
292     os.rmdir(dirname)
293   except OSError, err:
294     if err.errno != errno.ENOENT:
295       raise
296
297
298 def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
299   """Renames a file.
300
301   @type old: string
302   @param old: Original path
303   @type new: string
304   @param new: New path
305   @type mkdir: bool
306   @param mkdir: Whether to create target directory if it doesn't exist
307   @type mkdir_mode: int
308   @param mkdir_mode: Mode for newly created directories
309
310   """
311   try:
312     return os.rename(old, new)
313   except OSError, err:
314     # In at least one use case of this function, the job queue, directory
315     # creation is very rare. Checking for the directory before renaming is not
316     # as efficient.
317     if mkdir and err.errno == errno.ENOENT:
318       # Create directory and try again
319       Makedirs(os.path.dirname(new), mode=mkdir_mode)
320
321       return os.rename(old, new)
322
323     raise
324
325
326 def Makedirs(path, mode=0750):
327   """Super-mkdir; create a leaf directory and all intermediate ones.
328
329   This is a wrapper around C{os.makedirs} adding error handling not implemented
330   before Python 2.5.
331
332   """
333   try:
334     os.makedirs(path, mode)
335   except OSError, err:
336     # Ignore EEXIST. This is only handled in os.makedirs as included in
337     # Python 2.5 and above.
338     if err.errno != errno.EEXIST or not os.path.exists(path):
339       raise
340
341
342 def TimestampForFilename():
343   """Returns the current time formatted for filenames.
344
345   The format doesn't contain colons as some shells and applications treat them
346   as separators. Uses the local timezone.
347
348   """
349   return time.strftime("%Y-%m-%d_%H_%M_%S")
350
351
352 def CreateBackup(file_name):
353   """Creates a backup of a file.
354
355   @type file_name: str
356   @param file_name: file to be backed up
357   @rtype: str
358   @return: the path to the newly created backup
359   @raise errors.ProgrammerError: for invalid file names
360
361   """
362   if not os.path.isfile(file_name):
363     raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
364                                 file_name)
365
366   prefix = ("%s.backup-%s." %
367             (os.path.basename(file_name), TimestampForFilename()))
368   dir_name = os.path.dirname(file_name)
369
370   fsrc = open(file_name, "rb")
371   try:
372     (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
373     fdst = os.fdopen(fd, "wb")
374     try:
375       logging.debug("Backing up %s at %s", file_name, backup_name)
376       shutil.copyfileobj(fsrc, fdst)
377     finally:
378       fdst.close()
379   finally:
380     fsrc.close()
381
382   return backup_name
383
384
385 def ListVisibleFiles(path):
386   """Returns a list of visible files in a directory.
387
388   @type path: str
389   @param path: the directory to enumerate
390   @rtype: list
391   @return: the list of all files not starting with a dot
392   @raise ProgrammerError: if L{path} is not an absolue and normalized path
393
394   """
395   if not IsNormAbsPath(path):
396     raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
397                                  " absolute/normalized: '%s'" % path)
398   files = [i for i in os.listdir(path) if not i.startswith(".")]
399   return files
400
401
402 def EnsureDirs(dirs):
403   """Make required directories, if they don't exist.
404
405   @param dirs: list of tuples (dir_name, dir_mode)
406   @type dirs: list of (string, integer)
407
408   """
409   for dir_name, dir_mode in dirs:
410     try:
411       os.mkdir(dir_name, dir_mode)
412     except EnvironmentError, err:
413       if err.errno != errno.EEXIST:
414         raise errors.GenericError("Cannot create needed directory"
415                                   " '%s': %s" % (dir_name, err))
416     try:
417       os.chmod(dir_name, dir_mode)
418     except EnvironmentError, err:
419       raise errors.GenericError("Cannot change directory permissions on"
420                                 " '%s': %s" % (dir_name, err))
421     if not os.path.isdir(dir_name):
422       raise errors.GenericError("%s is not a directory" % dir_name)
423
424
425 def FindFile(name, search_path, test=os.path.exists):
426   """Look for a filesystem object in a given path.
427
428   This is an abstract method to search for filesystem object (files,
429   dirs) under a given search path.
430
431   @type name: str
432   @param name: the name to look for
433   @type search_path: str
434   @param search_path: location to start at
435   @type test: callable
436   @param test: a function taking one argument that should return True
437       if the a given object is valid; the default value is
438       os.path.exists, causing only existing files to be returned
439   @rtype: str or None
440   @return: full path to the object if found, None otherwise
441
442   """
443   # validate the filename mask
444   if constants.EXT_PLUGIN_MASK.match(name) is None:
445     logging.critical("Invalid value passed for external script name: '%s'",
446                      name)
447     return None
448
449   for dir_name in search_path:
450     # FIXME: investigate switch to PathJoin
451     item_name = os.path.sep.join([dir_name, name])
452     # check the user test and that we're indeed resolving to the given
453     # basename
454     if test(item_name) and os.path.basename(item_name) == name:
455       return item_name
456   return None
457
458
459 def IsNormAbsPath(path):
460   """Check whether a path is absolute and also normalized
461
462   This avoids things like /dir/../../other/path to be valid.
463
464   """
465   return os.path.normpath(path) == path and os.path.isabs(path)
466
467
468 def PathJoin(*args):
469   """Safe-join a list of path components.
470
471   Requirements:
472       - the first argument must be an absolute path
473       - no component in the path must have backtracking (e.g. /../),
474         since we check for normalization at the end
475
476   @param args: the path components to be joined
477   @raise ValueError: for invalid paths
478
479   """
480   # ensure we're having at least one path passed in
481   assert args
482   # ensure the first component is an absolute and normalized path name
483   root = args[0]
484   if not IsNormAbsPath(root):
485     raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
486   result = os.path.join(*args)
487   # ensure that the whole path is normalized
488   if not IsNormAbsPath(result):
489     raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
490   # check that we're still under the original prefix
491   prefix = os.path.commonprefix([root, result])
492   if prefix != root:
493     raise ValueError("Error: path joining resulted in different prefix"
494                      " (%s != %s)" % (prefix, root))
495   return result
496
497
498 def TailFile(fname, lines=20):
499   """Return the last lines from a file.
500
501   @note: this function will only read and parse the last 4KB of
502       the file; if the lines are very long, it could be that less
503       than the requested number of lines are returned
504
505   @param fname: the file name
506   @type lines: int
507   @param lines: the (maximum) number of lines to return
508
509   """
510   fd = open(fname, "r")
511   try:
512     fd.seek(0, 2)
513     pos = fd.tell()
514     pos = max(0, pos - 4096)
515     fd.seek(pos, 0)
516     raw_data = fd.read()
517   finally:
518     fd.close()
519
520   rows = raw_data.splitlines()
521   return rows[-lines:]
522
523
524 def BytesToMebibyte(value):
525   """Converts bytes to mebibytes.
526
527   @type value: int
528   @param value: Value in bytes
529   @rtype: int
530   @return: Value in mebibytes
531
532   """
533   return int(round(value / (1024.0 * 1024.0), 0))
534
535
536 def CalculateDirectorySize(path):
537   """Calculates the size of a directory recursively.
538
539   @type path: string
540   @param path: Path to directory
541   @rtype: int
542   @return: Size in mebibytes
543
544   """
545   size = 0
546
547   for (curpath, _, files) in os.walk(path):
548     for filename in files:
549       st = os.lstat(PathJoin(curpath, filename))
550       size += st.st_size
551
552   return BytesToMebibyte(size)
553
554
555 def GetFilesystemStats(path):
556   """Returns the total and free space on a filesystem.
557
558   @type path: string
559   @param path: Path on filesystem to be examined
560   @rtype: int
561   @return: tuple of (Total space, Free space) in mebibytes
562
563   """
564   st = os.statvfs(path)
565
566   fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
567   tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
568   return (tsize, fsize)
569
570
571 def ReadPidFile(pidfile):
572   """Read a pid from a file.
573
574   @type  pidfile: string
575   @param pidfile: path to the file containing the pid
576   @rtype: int
577   @return: The process id, if the file exists and contains a valid PID,
578            otherwise 0
579
580   """
581   try:
582     raw_data = ReadOneLineFile(pidfile)
583   except EnvironmentError, err:
584     if err.errno != errno.ENOENT:
585       logging.exception("Can't read pid file")
586     return 0
587
588   try:
589     pid = int(raw_data)
590   except (TypeError, ValueError), err:
591     logging.info("Can't parse pid file contents", exc_info=True)
592     return 0
593
594   return pid
595
596
597 def ReadLockedPidFile(path):
598   """Reads a locked PID file.
599
600   This can be used together with L{utils.process.StartDaemon}.
601
602   @type path: string
603   @param path: Path to PID file
604   @return: PID as integer or, if file was unlocked or couldn't be opened, None
605
606   """
607   try:
608     fd = os.open(path, os.O_RDONLY)
609   except EnvironmentError, err:
610     if err.errno == errno.ENOENT:
611       # PID file doesn't exist
612       return None
613     raise
614
615   try:
616     try:
617       # Try to acquire lock
618       filelock.LockFile(fd)
619     except errors.LockError:
620       # Couldn't lock, daemon is running
621       return int(os.read(fd, 100))
622   finally:
623     os.close(fd)
624
625   return None
626
627
628 def AddAuthorizedKey(file_obj, key):
629   """Adds an SSH public key to an authorized_keys file.
630
631   @type file_obj: str or file handle
632   @param file_obj: path to authorized_keys file
633   @type key: str
634   @param key: string containing key
635
636   """
637   key_fields = key.split()
638
639   if isinstance(file_obj, basestring):
640     f = open(file_obj, "a+")
641   else:
642     f = file_obj
643
644   try:
645     nl = True
646     for line in f:
647       # Ignore whitespace changes
648       if line.split() == key_fields:
649         break
650       nl = line.endswith("\n")
651     else:
652       if not nl:
653         f.write("\n")
654       f.write(key.rstrip("\r\n"))
655       f.write("\n")
656       f.flush()
657   finally:
658     f.close()
659
660
661 def RemoveAuthorizedKey(file_name, key):
662   """Removes an SSH public key from an authorized_keys file.
663
664   @type file_name: str
665   @param file_name: path to authorized_keys file
666   @type key: str
667   @param key: string containing key
668
669   """
670   key_fields = key.split()
671
672   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
673   try:
674     out = os.fdopen(fd, "w")
675     try:
676       f = open(file_name, "r")
677       try:
678         for line in f:
679           # Ignore whitespace changes while comparing lines
680           if line.split() != key_fields:
681             out.write(line)
682
683         out.flush()
684         os.rename(tmpname, file_name)
685       finally:
686         f.close()
687     finally:
688       out.close()
689   except:
690     RemoveFile(tmpname)
691     raise
692
693
694 def DaemonPidFileName(name):
695   """Compute a ganeti pid file absolute path
696
697   @type name: str
698   @param name: the daemon name
699   @rtype: str
700   @return: the full path to the pidfile corresponding to the given
701       daemon name
702
703   """
704   return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
705
706
707 def WritePidFile(pidfile):
708   """Write the current process pidfile.
709
710   @type pidfile: string
711   @param pidfile: the path to the file to be written
712   @raise errors.LockError: if the pid file already exists and
713       points to a live process
714   @rtype: int
715   @return: the file descriptor of the lock file; do not close this unless
716       you want to unlock the pid file
717
718   """
719   # We don't rename nor truncate the file to not drop locks under
720   # existing processes
721   fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
722
723   # Lock the PID file (and fail if not possible to do so). Any code
724   # wanting to send a signal to the daemon should try to lock the PID
725   # file before reading it. If acquiring the lock succeeds, the daemon is
726   # no longer running and the signal should not be sent.
727   filelock.LockFile(fd_pidfile)
728
729   os.write(fd_pidfile, "%d\n" % os.getpid())
730
731   return fd_pidfile
732
733
734 def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
735   """Reads the watcher pause file.
736
737   @type filename: string
738   @param filename: Path to watcher pause file
739   @type now: None, float or int
740   @param now: Current time as Unix timestamp
741   @type remove_after: int
742   @param remove_after: Remove watcher pause file after specified amount of
743     seconds past the pause end time
744
745   """
746   if now is None:
747     now = time.time()
748
749   try:
750     value = ReadFile(filename)
751   except IOError, err:
752     if err.errno != errno.ENOENT:
753       raise
754     value = None
755
756   if value is not None:
757     try:
758       value = int(value)
759     except ValueError:
760       logging.warning(("Watcher pause file (%s) contains invalid value,"
761                        " removing it"), filename)
762       RemoveFile(filename)
763       value = None
764
765     if value is not None:
766       # Remove file if it's outdated
767       if now > (value + remove_after):
768         RemoveFile(filename)
769         value = None
770
771       elif now > value:
772         value = None
773
774   return value
775
776
777 def NewUUID():
778   """Returns a random UUID.
779
780   @note: This is a Linux-specific method as it uses the /proc
781       filesystem.
782   @rtype: str
783
784   """
785   return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")