Package ganeti :: Package utils :: Module io
[hide private]
[frames] | no frames]

Source Code for Module ganeti.utils.io

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