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