utils: Introduce IsBelowDir
[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 IsBelowDir(root, other_path):
469   """Check whether a path is below a root dir.
470
471   This works around the nasty byte-byte comparisation of commonprefix.
472
473   """
474   if not (os.path.isabs(root) and os.path.isabs(other_path)):
475     raise ValueError("Provided paths '%s' and '%s' are not absolute" %
476                      (root, other_path))
477   prepared_root = "%s%s" % (os.path.normpath(root), os.sep)
478   return os.path.commonprefix([prepared_root,
479                                os.path.normpath(other_path)]) == prepared_root
480
481
482 def PathJoin(*args):
483   """Safe-join a list of path components.
484
485   Requirements:
486       - the first argument must be an absolute path
487       - no component in the path must have backtracking (e.g. /../),
488         since we check for normalization at the end
489
490   @param args: the path components to be joined
491   @raise ValueError: for invalid paths
492
493   """
494   # ensure we're having at least one path passed in
495   assert args
496   # ensure the first component is an absolute and normalized path name
497   root = args[0]
498   if not IsNormAbsPath(root):
499     raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
500   result = os.path.join(*args)
501   # ensure that the whole path is normalized
502   if not IsNormAbsPath(result):
503     raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
504   # check that we're still under the original prefix
505   prefix = os.path.commonprefix([root, result])
506   if prefix != root:
507     raise ValueError("Error: path joining resulted in different prefix"
508                      " (%s != %s)" % (prefix, root))
509   return result
510
511
512 def TailFile(fname, lines=20):
513   """Return the last lines from a file.
514
515   @note: this function will only read and parse the last 4KB of
516       the file; if the lines are very long, it could be that less
517       than the requested number of lines are returned
518
519   @param fname: the file name
520   @type lines: int
521   @param lines: the (maximum) number of lines to return
522
523   """
524   fd = open(fname, "r")
525   try:
526     fd.seek(0, 2)
527     pos = fd.tell()
528     pos = max(0, pos - 4096)
529     fd.seek(pos, 0)
530     raw_data = fd.read()
531   finally:
532     fd.close()
533
534   rows = raw_data.splitlines()
535   return rows[-lines:]
536
537
538 def BytesToMebibyte(value):
539   """Converts bytes to mebibytes.
540
541   @type value: int
542   @param value: Value in bytes
543   @rtype: int
544   @return: Value in mebibytes
545
546   """
547   return int(round(value / (1024.0 * 1024.0), 0))
548
549
550 def CalculateDirectorySize(path):
551   """Calculates the size of a directory recursively.
552
553   @type path: string
554   @param path: Path to directory
555   @rtype: int
556   @return: Size in mebibytes
557
558   """
559   size = 0
560
561   for (curpath, _, files) in os.walk(path):
562     for filename in files:
563       st = os.lstat(PathJoin(curpath, filename))
564       size += st.st_size
565
566   return BytesToMebibyte(size)
567
568
569 def GetFilesystemStats(path):
570   """Returns the total and free space on a filesystem.
571
572   @type path: string
573   @param path: Path on filesystem to be examined
574   @rtype: int
575   @return: tuple of (Total space, Free space) in mebibytes
576
577   """
578   st = os.statvfs(path)
579
580   fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
581   tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
582   return (tsize, fsize)
583
584
585 def ReadPidFile(pidfile):
586   """Read a pid from a file.
587
588   @type  pidfile: string
589   @param pidfile: path to the file containing the pid
590   @rtype: int
591   @return: The process id, if the file exists and contains a valid PID,
592            otherwise 0
593
594   """
595   try:
596     raw_data = ReadOneLineFile(pidfile)
597   except EnvironmentError, err:
598     if err.errno != errno.ENOENT:
599       logging.exception("Can't read pid file")
600     return 0
601
602   try:
603     pid = int(raw_data)
604   except (TypeError, ValueError), err:
605     logging.info("Can't parse pid file contents", exc_info=True)
606     return 0
607
608   return pid
609
610
611 def ReadLockedPidFile(path):
612   """Reads a locked PID file.
613
614   This can be used together with L{utils.process.StartDaemon}.
615
616   @type path: string
617   @param path: Path to PID file
618   @return: PID as integer or, if file was unlocked or couldn't be opened, None
619
620   """
621   try:
622     fd = os.open(path, os.O_RDONLY)
623   except EnvironmentError, err:
624     if err.errno == errno.ENOENT:
625       # PID file doesn't exist
626       return None
627     raise
628
629   try:
630     try:
631       # Try to acquire lock
632       filelock.LockFile(fd)
633     except errors.LockError:
634       # Couldn't lock, daemon is running
635       return int(os.read(fd, 100))
636   finally:
637     os.close(fd)
638
639   return None
640
641
642 def AddAuthorizedKey(file_obj, key):
643   """Adds an SSH public key to an authorized_keys file.
644
645   @type file_obj: str or file handle
646   @param file_obj: path to authorized_keys file
647   @type key: str
648   @param key: string containing key
649
650   """
651   key_fields = key.split()
652
653   if isinstance(file_obj, basestring):
654     f = open(file_obj, "a+")
655   else:
656     f = file_obj
657
658   try:
659     nl = True
660     for line in f:
661       # Ignore whitespace changes
662       if line.split() == key_fields:
663         break
664       nl = line.endswith("\n")
665     else:
666       if not nl:
667         f.write("\n")
668       f.write(key.rstrip("\r\n"))
669       f.write("\n")
670       f.flush()
671   finally:
672     f.close()
673
674
675 def RemoveAuthorizedKey(file_name, key):
676   """Removes an SSH public key from an authorized_keys file.
677
678   @type file_name: str
679   @param file_name: path to authorized_keys file
680   @type key: str
681   @param key: string containing key
682
683   """
684   key_fields = key.split()
685
686   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
687   try:
688     out = os.fdopen(fd, "w")
689     try:
690       f = open(file_name, "r")
691       try:
692         for line in f:
693           # Ignore whitespace changes while comparing lines
694           if line.split() != key_fields:
695             out.write(line)
696
697         out.flush()
698         os.rename(tmpname, file_name)
699       finally:
700         f.close()
701     finally:
702       out.close()
703   except:
704     RemoveFile(tmpname)
705     raise
706
707
708 def DaemonPidFileName(name):
709   """Compute a ganeti pid file absolute path
710
711   @type name: str
712   @param name: the daemon name
713   @rtype: str
714   @return: the full path to the pidfile corresponding to the given
715       daemon name
716
717   """
718   return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
719
720
721 def WritePidFile(pidfile):
722   """Write the current process pidfile.
723
724   @type pidfile: string
725   @param pidfile: the path to the file to be written
726   @raise errors.LockError: if the pid file already exists and
727       points to a live process
728   @rtype: int
729   @return: the file descriptor of the lock file; do not close this unless
730       you want to unlock the pid file
731
732   """
733   # We don't rename nor truncate the file to not drop locks under
734   # existing processes
735   fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
736
737   # Lock the PID file (and fail if not possible to do so). Any code
738   # wanting to send a signal to the daemon should try to lock the PID
739   # file before reading it. If acquiring the lock succeeds, the daemon is
740   # no longer running and the signal should not be sent.
741   filelock.LockFile(fd_pidfile)
742
743   os.write(fd_pidfile, "%d\n" % os.getpid())
744
745   return fd_pidfile
746
747
748 def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
749   """Reads the watcher pause file.
750
751   @type filename: string
752   @param filename: Path to watcher pause file
753   @type now: None, float or int
754   @param now: Current time as Unix timestamp
755   @type remove_after: int
756   @param remove_after: Remove watcher pause file after specified amount of
757     seconds past the pause end time
758
759   """
760   if now is None:
761     now = time.time()
762
763   try:
764     value = ReadFile(filename)
765   except IOError, err:
766     if err.errno != errno.ENOENT:
767       raise
768     value = None
769
770   if value is not None:
771     try:
772       value = int(value)
773     except ValueError:
774       logging.warning(("Watcher pause file (%s) contains invalid value,"
775                        " removing it"), filename)
776       RemoveFile(filename)
777       value = None
778
779     if value is not None:
780       # Remove file if it's outdated
781       if now > (value + remove_after):
782         RemoveFile(filename)
783         value = None
784
785       elif now > value:
786         value = None
787
788   return value
789
790
791 def NewUUID():
792   """Returns a random UUID.
793
794   @note: This is a Linux-specific method as it uses the /proc
795       filesystem.
796   @rtype: str
797
798   """
799   return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")