1# -*- coding: utf-8 -*- 2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Common file and os related utilities, including tempdir manipulation.""" 7 8from __future__ import print_function 9 10import collections 11import contextlib 12import ctypes 13import ctypes.util 14import datetime 15import errno 16import glob 17import hashlib 18import os 19import pwd 20import re 21import shutil 22import stat 23import subprocess 24import tempfile 25 26import six 27 28from autotest_lib.utils.frozen_chromite.lib import cros_build_lib 29from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging 30from autotest_lib.utils.frozen_chromite.lib import retry_util 31from autotest_lib.utils.frozen_chromite.utils import key_value_store 32 33 34# Env vars that tempdir can be gotten from; minimally, this 35# needs to match python's tempfile module and match normal 36# unix standards. 37_TEMPDIR_ENV_VARS = ('TMPDIR', 'TEMP', 'TMP') 38 39 40def GetNonRootUser(): 41 """Returns a non-root user. Defaults to the current user. 42 43 If the current user is root, returns the username of the person who 44 ran the emerge command. If running using sudo, returns the username 45 of the person who ran the sudo command. If no non-root user is 46 found, returns None. 47 """ 48 uid = os.getuid() 49 if uid == 0: 50 user = os.environ.get('PORTAGE_USERNAME', os.environ.get('SUDO_USER')) 51 else: 52 user = pwd.getpwuid(os.getuid()).pw_name 53 54 if user == 'root': 55 return None 56 else: 57 return user 58 59 60def IsChildProcess(pid, name=None): 61 """Return True if pid is a child of the current process. 62 63 Args: 64 pid: Child pid to search for in current process's pstree. 65 name: Name of the child process. 66 67 Note: 68 This function is not fool proof. If the process tree contains wierd names, 69 an incorrect match might be possible. 70 """ 71 cmd = ['pstree', '-Ap', str(os.getpid())] 72 pstree = cros_build_lib.run(cmd, capture_output=True, print_cmd=False, 73 encoding='utf-8').stdout 74 if name is None: 75 match = '(%d)' % pid 76 else: 77 match = '-%s(%d)' % (name, pid) 78 return match in pstree 79 80 81def ExpandPath(path): 82 """Returns path after passing through realpath and expanduser.""" 83 return os.path.realpath(os.path.expanduser(path)) 84 85 86def IsSubPath(path, other): 87 """Returns whether |path| is a sub path of |other|.""" 88 path = os.path.abspath(path) 89 other = os.path.abspath(other) 90 if path == other: 91 return True 92 return path.startswith(other + os.sep) 93 94 95def AllocateFile(path, size, makedirs=False): 96 """Allocates a file of a certain |size| in |path|. 97 98 Args: 99 path: Path to allocate the file. 100 size: The length, in bytes, of the desired file. 101 makedirs: If True, create missing leading directories in the path. 102 """ 103 if makedirs: 104 SafeMakedirs(os.path.dirname(path)) 105 106 with open(path, 'w') as out: 107 out.truncate(size) 108 109 110# All the modes that we allow people to pass to WriteFile. This allows us to 111# make assumptions about the input so we can update it if needed. 112_VALID_WRITE_MODES = { 113 # Read & write, but no truncation, and file offset is 0. 114 'r+', 'r+b', 115 # Writing (and maybe reading) with truncation. 116 'w', 'wb', 'w+', 'w+b', 117 # Writing (and maybe reading), but no truncation, and file offset is at end. 118 'a', 'ab', 'a+', 'a+b', 119} 120 121 122def WriteFile(path, content, mode='w', encoding=None, errors=None, atomic=False, 123 makedirs=False, sudo=False): 124 """Write the given content to disk. 125 126 Args: 127 path: Pathway to write the content to. 128 content: Content to write. May be either an iterable, or a string. 129 mode: The mode to use when opening the file. 'w' is for text files (see the 130 following settings) and 'wb' is for binary files. If appending, pass 131 'w+', etc... 132 encoding: The encoding of the file content. Text files default to 'utf-8'. 133 errors: How to handle encoding errors. Text files default to 'strict'. 134 atomic: If the updating of the file should be done atomically. Note this 135 option is incompatible w/ append mode. 136 makedirs: If True, create missing leading directories in the path. 137 sudo: If True, write the file as root. 138 """ 139 if mode not in _VALID_WRITE_MODES: 140 raise ValueError('mode must be one of {"%s"}, not %r' % 141 ('", "'.join(sorted(_VALID_WRITE_MODES)), mode)) 142 143 if sudo and atomic and ('a' in mode or '+' in mode): 144 raise ValueError('append mode does not work in sudo+atomic mode') 145 146 if 'b' in mode: 147 if encoding is not None or errors is not None: 148 raise ValueError('binary mode does not use encoding/errors') 149 else: 150 if encoding is None: 151 encoding = 'utf-8' 152 if errors is None: 153 errors = 'strict' 154 155 if makedirs: 156 SafeMakedirs(os.path.dirname(path), sudo=sudo) 157 158 # TODO(vapier): We can merge encoding/errors into the open call once we are 159 # Python 3 only. Until then, we have to handle it ourselves. 160 if 'b' in mode: 161 write_wrapper = lambda x: x 162 else: 163 mode += 'b' 164 def write_wrapper(iterable): 165 for item in iterable: 166 yield item.encode(encoding, errors) 167 168 # If the file needs to be written as root and we are not root, write to a temp 169 # file, move it and change the permission. 170 if sudo and os.getuid() != 0: 171 if 'a' in mode or '+' in mode: 172 # Use dd to run through sudo & append the output, and write the new data 173 # to it through stdin. 174 cros_build_lib.sudo_run( 175 ['dd', 'conv=notrunc', 'oflag=append', 'status=none', 176 'of=%s' % (path,)], print_cmd=False, input=content) 177 178 else: 179 with tempfile.NamedTemporaryFile(mode=mode, delete=False) as temp: 180 write_path = temp.name 181 temp.writelines(write_wrapper( 182 cros_build_lib.iflatten_instance(content))) 183 os.chmod(write_path, 0o644) 184 185 try: 186 mv_target = path if not atomic else path + '.tmp' 187 cros_build_lib.sudo_run(['mv', write_path, mv_target], 188 print_cmd=False, stderr=True) 189 Chown(mv_target, user='root', group='root') 190 if atomic: 191 cros_build_lib.sudo_run(['mv', mv_target, path], 192 print_cmd=False, stderr=True) 193 194 except cros_build_lib.RunCommandError: 195 SafeUnlink(write_path) 196 SafeUnlink(mv_target) 197 raise 198 199 else: 200 # We have the right permissions, simply write the file in python. 201 write_path = path 202 if atomic: 203 write_path = path + '.tmp' 204 with open(write_path, mode) as f: 205 f.writelines(write_wrapper(cros_build_lib.iflatten_instance(content))) 206 207 if not atomic: 208 return 209 210 try: 211 os.rename(write_path, path) 212 except EnvironmentError: 213 SafeUnlink(write_path) 214 raise 215 216 217def Touch(path, makedirs=False, mode=None): 218 """Simulate unix touch. Create if doesn't exist and update its timestamp. 219 220 Args: 221 path: a string, file name of the file to touch (creating if not present). 222 makedirs: If True, create missing leading directories in the path. 223 mode: The access permissions to set. In the style of chmod. Defaults to 224 using the umask. 225 """ 226 if makedirs: 227 SafeMakedirs(os.path.dirname(path)) 228 229 # Create the file if nonexistant. 230 open(path, 'a').close() 231 if mode is not None: 232 os.chmod(path, mode) 233 # Update timestamp to right now. 234 os.utime(path, None) 235 236 237def Chown(path, user=None, group=None, recursive=False): 238 """Simple sudo chown path to the user. 239 240 Defaults to user running command. Does nothing if run as root user unless 241 a new owner is provided. 242 243 Args: 244 path: str - File/directory to chown. 245 user: str|int|None - User to chown the file to. Defaults to current user. 246 group: str|int|None - Group to assign the file to. 247 recursive: Also chown child files/directories recursively. 248 """ 249 if user is None: 250 user = GetNonRootUser() or '' 251 else: 252 user = str(user) 253 254 group = '' if group is None else str(group) 255 256 if user or group: 257 cmd = ['chown'] 258 if recursive: 259 cmd += ['-R'] 260 cmd += ['%s:%s' % (user, group), path] 261 cros_build_lib.sudo_run(cmd, print_cmd=False, 262 stderr=True, stdout=True) 263 264 265def ReadFile(path, mode='r', encoding=None, errors=None): 266 """Read a given file on disk. Primarily useful for one off small files. 267 268 The defaults are geared towards reading UTF-8 encoded text. 269 270 Args: 271 path: The file to read. 272 mode: The mode to use when opening the file. 'r' is for text files (see the 273 following settings) and 'rb' is for binary files. 274 encoding: The encoding of the file content. Text files default to 'utf-8'. 275 errors: How to handle encoding errors. Text files default to 'strict'. 276 277 Returns: 278 The content of the file, either as bytes or a string (with the specified 279 encoding). 280 """ 281 if mode not in ('r', 'rb'): 282 raise ValueError('mode may only be "r" or "rb", not %r' % (mode,)) 283 284 if 'b' in mode: 285 if encoding is not None or errors is not None: 286 raise ValueError('binary mode does not use encoding/errors') 287 else: 288 if encoding is None: 289 encoding = 'utf-8' 290 if errors is None: 291 errors = 'strict' 292 293 with open(path, 'rb') as f: 294 # TODO(vapier): We can merge encoding/errors into the open call once we are 295 # Python 3 only. Until then, we have to handle it ourselves. 296 ret = f.read() 297 if 'b' not in mode: 298 ret = ret.decode(encoding, errors) 299 return ret 300 301 302def MD5HashFile(path): 303 """Calculate the md5 hash of a given file path. 304 305 Args: 306 path: The path of the file to hash. 307 308 Returns: 309 The hex digest of the md5 hash of the file. 310 """ 311 contents = ReadFile(path, mode='rb') 312 return hashlib.md5(contents).hexdigest() 313 314 315def SafeSymlink(source, dest, sudo=False): 316 """Create a symlink at |dest| pointing to |source|. 317 318 This will override the |dest| if the symlink exists. This operation is not 319 atomic. 320 321 Args: 322 source: source path. 323 dest: destination path. 324 sudo: If True, create the link as root. 325 """ 326 if sudo and os.getuid() != 0: 327 cros_build_lib.sudo_run(['ln', '-sfT', source, dest], 328 print_cmd=False, stderr=True) 329 else: 330 SafeUnlink(dest) 331 os.symlink(source, dest) 332 333 334def SafeUnlink(path, sudo=False): 335 """Unlink a file from disk, ignoring if it doesn't exist. 336 337 Returns: 338 True if the file existed and was removed, False if it didn't exist. 339 """ 340 try: 341 os.unlink(path) 342 return True 343 except EnvironmentError as e: 344 if e.errno == errno.ENOENT: 345 return False 346 347 if not sudo: 348 raise 349 350 # If we're still here, we're falling back to sudo. 351 cros_build_lib.sudo_run(['rm', '--', path], print_cmd=False, stderr=True) 352 return True 353 354 355def SafeMakedirs(path, mode=0o775, sudo=False, user='root'): 356 """Make parent directories if needed. Ignore if existing. 357 358 Args: 359 path: The path to create. Intermediate directories will be created as 360 needed. This can be either a |Path| or |str|. 361 mode: The access permissions in the style of chmod. 362 sudo: If True, create it via sudo, thus root owned. 363 user: If |sudo| is True, run sudo as |user|. 364 365 Returns: 366 True if the directory had to be created, False if otherwise. 367 368 Raises: 369 EnvironmentError: If the makedir failed. 370 RunCommandError: If using run and the command failed for any reason. 371 """ 372 if sudo and not (os.getuid() == 0 and user == 'root'): 373 if os.path.isdir(path): 374 return False 375 cros_build_lib.sudo_run( 376 ['mkdir', '-p', '--mode', '%o' % mode, str(path)], user=user, 377 print_cmd=False, stderr=True, stdout=True) 378 cros_build_lib.sudo_run( 379 ['chmod', '%o' % mode, str(path)], 380 print_cmd=False, stderr=True, stdout=True) 381 return True 382 383 try: 384 os.makedirs(path, mode) 385 # If we made the directory, force the mode. 386 os.chmod(path, mode) 387 return True 388 except EnvironmentError as e: 389 if e.errno != errno.EEXIST or not os.path.isdir(path): 390 raise 391 392 # If the mode on the directory does not match the request, log it. 393 # It is the callers responsibility to coordinate mode values if there is a 394 # need for that. 395 if stat.S_IMODE(os.stat(path).st_mode) != mode: 396 try: 397 os.chmod(path, mode) 398 except EnvironmentError: 399 # Just make sure it's a directory. 400 if not os.path.isdir(path): 401 raise 402 return False 403 404 405class MakingDirsAsRoot(Exception): 406 """Raised when creating directories as root.""" 407 408 409def SafeMakedirsNonRoot(path, mode=0o775, user=None): 410 """Create directories and make sure they are not owned by root. 411 412 See SafeMakedirs for the arguments and returns. 413 """ 414 if user is None: 415 user = GetNonRootUser() 416 417 if user is None or user == 'root': 418 raise MakingDirsAsRoot('Refusing to create %s as user %s!' % (path, user)) 419 420 created = False 421 should_chown = False 422 try: 423 created = SafeMakedirs(path, mode=mode, user=user) 424 if not created: 425 # Sometimes, the directory exists, but is owned by root. As a HACK, we 426 # will chown it to the requested user. 427 stat_info = os.stat(path) 428 should_chown = (stat_info.st_uid == 0) 429 except OSError as e: 430 if e.errno == errno.EACCES: 431 # Sometimes, (a prefix of the) path we're making the directory in may be 432 # owned by root, and so we fail. As a HACK, use da power to create 433 # directory and then chown it. 434 created = should_chown = SafeMakedirs(path, mode=mode, sudo=True) 435 436 if should_chown: 437 Chown(path, user=user) 438 439 return created 440 441 442class BadPathsException(Exception): 443 """Raised by various osutils path manipulation functions on bad input.""" 444 445 446def CopyDirContents(from_dir, to_dir, symlinks=False, allow_nonempty=False): 447 """Copy contents of from_dir to to_dir. Both should exist. 448 449 shutil.copytree allows one to copy a rooted directory tree along with the 450 containing directory. OTOH, this function copies the contents of from_dir to 451 an existing directory. For example, for the given paths: 452 453 from/ 454 inside/x.py 455 y.py 456 to/ 457 458 shutil.copytree('from', 'to') 459 # Raises because 'to' already exists. 460 461 shutil.copytree('from', 'to/non_existent_dir') 462 to/non_existent_dir/ 463 inside/x.py 464 y.py 465 466 CopyDirContents('from', 'to') 467 to/ 468 inside/x.py 469 y.py 470 471 Args: 472 from_dir: The directory whose contents should be copied. Must exist. Either 473 a |Path| or a |str|. 474 to_dir: The directory to which contents should be copied. Must exist. 475 Either a |Path| or a |str|. 476 symlinks: Whether symlinks should be copied or dereferenced. When True, all 477 symlinks will be copied as symlinks into the destination. When False, 478 the symlinks will be dereferenced and the contents copied over. 479 allow_nonempty: If True, do not die when to_dir is nonempty. 480 481 Raises: 482 BadPathsException: if the source / target directories don't exist, or if 483 target directory is non-empty when allow_nonempty=False. 484 OSError: on esoteric permission errors. 485 """ 486 if not os.path.isdir(from_dir): 487 raise BadPathsException('Source directory %s does not exist.' % from_dir) 488 if not os.path.isdir(to_dir): 489 raise BadPathsException('Destination directory %s does not exist.' % to_dir) 490 if os.listdir(to_dir) and not allow_nonempty: 491 raise BadPathsException('Destination directory %s is not empty.' % to_dir) 492 493 for name in os.listdir(from_dir): 494 from_path = os.path.join(from_dir, name) 495 to_path = os.path.join(to_dir, name) 496 if symlinks and os.path.islink(from_path): 497 os.symlink(os.readlink(from_path), to_path) 498 elif os.path.isdir(from_path): 499 shutil.copytree(from_path, to_path, symlinks=symlinks) 500 elif os.path.isfile(from_path): 501 shutil.copy2(from_path, to_path) 502 503 504def RmDir(path, ignore_missing=False, sudo=False): 505 """Recursively remove a directory. 506 507 Args: 508 path: Path of directory to remove. Either a |Path| or |str|. 509 ignore_missing: Do not error when path does not exist. 510 sudo: Remove directories as root. 511 """ 512 # Using `sudo` is a bit expensive, so try to delete everything natively first. 513 try: 514 shutil.rmtree(path) 515 return 516 except EnvironmentError as e: 517 if ignore_missing and e.errno == errno.ENOENT: 518 return 519 520 if not sudo: 521 raise 522 523 # If we're still here, we're falling back to sudo. 524 try: 525 cros_build_lib.sudo_run( 526 ['rm', '-r%s' % ('f' if ignore_missing else '',), '--', str(path)], 527 debug_level=logging.DEBUG, stdout=True, stderr=True) 528 except cros_build_lib.RunCommandError: 529 if not ignore_missing or os.path.exists(path): 530 # If we're not ignoring the rm ENOENT equivalent, throw it; 531 # if the pathway still exists, something failed, thus throw it. 532 raise 533 534 535class EmptyDirNonExistentException(BadPathsException): 536 """EmptyDir was called on a non-existent directory without ignore_missing.""" 537 538 539def EmptyDir(path, ignore_missing=False, sudo=False, exclude=()): 540 """Remove all files inside a directory, including subdirs. 541 542 Args: 543 path: Path of directory to empty. 544 ignore_missing: Do not error when path does not exist. 545 sudo: Remove directories as root. 546 exclude: Iterable of file names to exclude from the cleanup. They should 547 exactly match the file or directory name in path. 548 e.g. ['foo', 'bar'] 549 550 Raises: 551 EmptyDirNonExistentException: if ignore_missing false, and dir is missing. 552 OSError: If the directory is not user writable. 553 """ 554 path = ExpandPath(path) 555 exclude = set(exclude) 556 557 if not os.path.exists(path): 558 if ignore_missing: 559 return 560 raise EmptyDirNonExistentException( 561 'EmptyDir called non-existent: %s' % path) 562 563 # We don't catch OSError if path is not a directory. 564 for candidate in os.listdir(path): 565 if candidate not in exclude: 566 subpath = os.path.join(path, candidate) 567 # Both options can throw OSError if there is a permission problem. 568 if os.path.isdir(subpath): 569 RmDir(subpath, ignore_missing=ignore_missing, sudo=sudo) 570 else: 571 SafeUnlink(subpath, sudo) 572 573 574def Which(binary, path=None, mode=os.X_OK, root=None): 575 """Return the absolute path to the specified binary. 576 577 Args: 578 binary: The binary to look for. 579 path: Search path. Defaults to os.environ['PATH']. 580 mode: File mode to check on the binary. 581 root: Path to automatically prefix to every element of |path|. 582 583 Returns: 584 The full path to |binary| if found (with the right mode). Otherwise, None. 585 """ 586 if path is None: 587 path = os.environ.get('PATH', '') 588 for p in path.split(os.pathsep): 589 if root and p.startswith('/'): 590 # Don't prefix relative paths. We might want to support this at some 591 # point, but it's not worth the coding hassle currently. 592 p = os.path.join(root, p.lstrip('/')) 593 p = os.path.join(p, binary) 594 if os.path.isfile(p) and os.access(p, mode): 595 return p 596 return None 597 598 599def FindMissingBinaries(needed_tools): 600 """Verifies that the required tools are present on the system. 601 602 This is especially important for scripts that are intended to run 603 outside the chroot. 604 605 Args: 606 needed_tools: an array of string specified binaries to look for. 607 608 Returns: 609 If all tools are found, returns the empty list. Otherwise, returns the 610 list of missing tools. 611 """ 612 return [binary for binary in needed_tools if Which(binary) is None] 613 614 615def DirectoryIterator(base_path): 616 """Iterates through the files and subdirs of a directory.""" 617 for root, dirs, files in os.walk(base_path): 618 for e in [d + os.sep for d in dirs] + files: 619 yield os.path.join(root, e) 620 621 622def IteratePaths(end_path): 623 """Generator that iterates down to |end_path| from root /. 624 625 Args: 626 end_path: The destination. If this is a relative path, it will be resolved 627 to absolute path. In all cases, it will be normalized. 628 629 Yields: 630 All the paths gradually constructed from / to |end_path|. For example: 631 IteratePaths("/this/path") yields "/", "/this", and "/this/path". 632 """ 633 return reversed(list(IteratePathParents(end_path))) 634 635 636def IteratePathParents(start_path): 637 """Generator that iterates through a directory's parents. 638 639 Args: 640 start_path: The path to start from. 641 642 Yields: 643 The passed-in path, along with its parents. i.e., 644 IteratePathParents('/usr/local') would yield '/usr/local', '/usr', and '/'. 645 """ 646 path = os.path.abspath(start_path) 647 # There's a bug that abspath('//') returns '//'. We need to renormalize it. 648 if path == '//': 649 path = '/' 650 yield path 651 while path.strip('/'): 652 path = os.path.dirname(path) 653 yield path 654 655 656def FindInPathParents(path_to_find, start_path, test_func=None, end_path=None): 657 """Look for a relative path, ascending through parent directories. 658 659 Ascend through parent directories of current path looking for a relative 660 path. I.e., given a directory structure like: 661 -/ 662 | 663 --usr 664 | 665 --bin 666 | 667 --local 668 | 669 --google 670 671 the call FindInPathParents('bin', '/usr/local') would return '/usr/bin', and 672 the call FindInPathParents('google', '/usr/local') would return 673 '/usr/local/google'. 674 675 Args: 676 path_to_find: The relative path to look for. 677 start_path: The path to start the search from. If |start_path| is a 678 directory, it will be included in the directories that are searched. 679 test_func: The function to use to verify the relative path. Defaults to 680 os.path.exists. The function will be passed one argument - the target 681 path to test. A True return value will cause AscendingLookup to return 682 the target. 683 end_path: The path to stop searching. 684 """ 685 if end_path is not None: 686 end_path = os.path.abspath(end_path) 687 if test_func is None: 688 test_func = os.path.exists 689 for path in IteratePathParents(start_path): 690 if path == end_path: 691 return None 692 target = os.path.join(path, path_to_find) 693 if test_func(target): 694 return target 695 return None 696 697 698def SetGlobalTempDir(tempdir_value, tempdir_env=None): 699 """Set the global temp directory to the specified |tempdir_value| 700 701 Args: 702 tempdir_value: The new location for the global temp directory. 703 tempdir_env: Optional. A list of key/value pairs to set in the 704 environment. If not provided, set all global tempdir environment 705 variables to point at |tempdir_value|. 706 707 Returns: 708 Returns (old_tempdir_value, old_tempdir_env). 709 710 old_tempdir_value: The old value of the global temp directory. 711 old_tempdir_env: A list of the key/value pairs that control the tempdir 712 environment and were set prior to this function. If the environment 713 variable was not set, it is recorded as None. 714 """ 715 # pylint: disable=protected-access 716 with tempfile._once_lock: 717 old_tempdir_value = GetGlobalTempDir() 718 old_tempdir_env = tuple((x, os.environ.get(x)) for x in _TEMPDIR_ENV_VARS) 719 720 # Now update TMPDIR/TEMP/TMP, and poke the python 721 # internals to ensure all subprocess/raw tempfile 722 # access goes into this location. 723 if tempdir_env is None: 724 os.environ.update((x, tempdir_value) for x in _TEMPDIR_ENV_VARS) 725 else: 726 for key, value in tempdir_env: 727 if value is None: 728 os.environ.pop(key, None) 729 else: 730 os.environ[key] = value 731 732 # Finally, adjust python's cached value (we know it's cached by here 733 # since we invoked _get_default_tempdir from above). Note this 734 # is necessary since we want *all* output from that point 735 # forward to go to this location. 736 tempfile.tempdir = tempdir_value 737 738 return (old_tempdir_value, old_tempdir_env) 739 740 741def GetGlobalTempDir(): 742 """Get the path to the current global tempdir. 743 744 The global tempdir path can be modified through calls to SetGlobalTempDir. 745 """ 746 # pylint: disable=protected-access 747 return tempfile._get_default_tempdir() 748 749 750def _TempDirSetup(self, prefix='tmp', set_global=False, base_dir=None): 751 """Generate a tempdir, modifying the object, and env to use it. 752 753 Specifically, if set_global is True, then from this invocation forward, 754 python and all subprocesses will use this location for their tempdir. 755 756 The matching _TempDirTearDown restores the env to what it was. 757 """ 758 # Stash the old tempdir that was used so we can 759 # switch it back on the way out. 760 self.tempdir = tempfile.mkdtemp(prefix=prefix, dir=base_dir) 761 os.chmod(self.tempdir, 0o700) 762 763 if set_global: 764 self._orig_tempdir_value, self._orig_tempdir_env = \ 765 SetGlobalTempDir(self.tempdir) 766 767 768def _TempDirTearDown(self, force_sudo, delete=True): 769 # Note that _TempDirSetup may have failed, resulting in these attributes 770 # not being set; this is why we use getattr here (and must). 771 tempdir = getattr(self, 'tempdir', None) 772 try: 773 if tempdir is not None and delete: 774 RmDir(tempdir, ignore_missing=True, sudo=force_sudo) 775 except EnvironmentError as e: 776 # Suppress ENOENT since we may be invoked 777 # in a context where parallel wipes of the tempdir 778 # may be occuring; primarily during hard shutdowns. 779 if e.errno != errno.ENOENT: 780 raise 781 782 # Restore environment modification if necessary. 783 orig_tempdir_value = getattr(self, '_orig_tempdir_value', None) 784 if orig_tempdir_value is not None: 785 # pylint: disable=protected-access 786 SetGlobalTempDir(orig_tempdir_value, self._orig_tempdir_env) 787 788 789class TempDir(object): 790 """Object that creates a temporary directory. 791 792 This object can either be used as a context manager or just as a simple 793 object. The temporary directory is stored as self.tempdir in the object, and 794 is returned as a string by a 'with' statement. 795 """ 796 797 def __init__(self, **kwargs): 798 """Constructor. Creates the temporary directory. 799 800 Args: 801 prefix: See tempfile.mkdtemp documentation. 802 base_dir: The directory to place the temporary directory. 803 set_global: Set this directory as the global temporary directory. 804 delete: Whether the temporary dir should be deleted as part of cleanup. 805 (default: True) 806 sudo_rm: Whether the temporary dir will need root privileges to remove. 807 (default: False) 808 """ 809 self.kwargs = kwargs.copy() 810 self.delete = kwargs.pop('delete', True) 811 self.sudo_rm = kwargs.pop('sudo_rm', False) 812 self.tempdir = None 813 _TempDirSetup(self, **kwargs) 814 815 def SetSudoRm(self, enable=True): 816 """Sets |sudo_rm|, which forces us to delete temporary files as root.""" 817 self.sudo_rm = enable 818 819 def Cleanup(self): 820 """Clean up the temporary directory.""" 821 if self.tempdir is not None: 822 try: 823 _TempDirTearDown(self, self.sudo_rm, delete=self.delete) 824 finally: 825 self.tempdir = None 826 827 def __enter__(self): 828 """Return the temporary directory.""" 829 return self.tempdir 830 831 def __exit__(self, exc_type, exc_value, exc_traceback): 832 try: 833 self.Cleanup() 834 except Exception: 835 if exc_type: 836 # If an exception from inside the context was already in progress, 837 # log our cleanup exception, then allow the original to resume. 838 logging.error('While exiting %s:', self, exc_info=True) 839 840 if self.tempdir: 841 # Log all files in tempdir at the time of the failure. 842 try: 843 logging.error('Directory contents were:') 844 for name in os.listdir(self.tempdir): 845 logging.error(' %s', name) 846 except OSError: 847 logging.error(' Directory did not exist.') 848 849 # Log all mounts at the time of the failure, since that's the most 850 # common cause. 851 mount_results = cros_build_lib.run( 852 ['mount'], stdout=True, stderr=subprocess.STDOUT, 853 check=False) 854 logging.error('Mounts were:') 855 logging.error(' %s', mount_results.output) 856 857 else: 858 # If there was not an exception from the context, raise ours. 859 raise 860 861 def __del__(self): 862 self.Cleanup() 863 864 def __str__(self): 865 return self.tempdir if self.tempdir else '' 866 867 868def TempDirDecorator(func): 869 """Populates self.tempdir with path to a temporary writeable directory.""" 870 def f(self, *args, **kwargs): 871 with TempDir() as tempdir: 872 self.tempdir = tempdir 873 return func(self, *args, **kwargs) 874 875 f.__name__ = func.__name__ 876 f.__doc__ = func.__doc__ 877 f.__module__ = func.__module__ 878 return f 879 880 881def TempFileDecorator(func): 882 """Populates self.tempfile with path to a temporary writeable file""" 883 def f(self, *args, **kwargs): 884 with tempfile.NamedTemporaryFile(dir=self.tempdir, delete=False) as f: 885 self.tempfile = f.name 886 return func(self, *args, **kwargs) 887 888 f.__name__ = func.__name__ 889 f.__doc__ = func.__doc__ 890 f.__module__ = func.__module__ 891 return TempDirDecorator(f) 892 893 894# Flags synced from sys/mount.h. See mount(2) for details. 895MS_RDONLY = 1 896MS_NOSUID = 2 897MS_NODEV = 4 898MS_NOEXEC = 8 899MS_SYNCHRONOUS = 16 900MS_REMOUNT = 32 901MS_MANDLOCK = 64 902MS_DIRSYNC = 128 903MS_NOATIME = 1024 904MS_NODIRATIME = 2048 905MS_BIND = 4096 906MS_MOVE = 8192 907MS_REC = 16384 908MS_SILENT = 32768 909MS_POSIXACL = 1 << 16 910MS_UNBINDABLE = 1 << 17 911MS_PRIVATE = 1 << 18 912MS_SLAVE = 1 << 19 913MS_SHARED = 1 << 20 914MS_RELATIME = 1 << 21 915MS_KERNMOUNT = 1 << 22 916MS_I_VERSION = 1 << 23 917MS_STRICTATIME = 1 << 24 918MS_ACTIVE = 1 << 30 919MS_NOUSER = 1 << 31 920 921 922def Mount(source, target, fstype, flags, data=''): 923 """Call the mount(2) func; see the man page for details.""" 924 libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True) 925 # These fields might be a string or 0 (for NULL). Convert to bytes. 926 def _MaybeEncode(s): 927 return s.encode('utf-8') if isinstance(s, six.string_types) else s 928 if libc.mount(_MaybeEncode(source), _MaybeEncode(target), 929 _MaybeEncode(fstype), ctypes.c_int(flags), 930 _MaybeEncode(data)) != 0: 931 e = ctypes.get_errno() 932 raise OSError(e, os.strerror(e)) 933 934 935def MountDir(src_path, dst_path, fs_type=None, sudo=True, makedirs=True, 936 mount_opts=('nodev', 'noexec', 'nosuid'), skip_mtab=False, 937 **kwargs): 938 """Mount |src_path| at |dst_path| 939 940 Args: 941 src_path: Source of the new mount. 942 dst_path: Where to mount things. 943 fs_type: Specify the filesystem type to use. Defaults to autodetect. 944 sudo: Run through sudo. 945 makedirs: Create |dst_path| if it doesn't exist. 946 mount_opts: List of options to pass to `mount`. 947 skip_mtab: Whether to write new entries to /etc/mtab. 948 kwargs: Pass all other args to run. 949 """ 950 if sudo: 951 runcmd = cros_build_lib.sudo_run 952 else: 953 runcmd = cros_build_lib.run 954 955 if makedirs: 956 SafeMakedirs(dst_path, sudo=sudo) 957 958 cmd = ['mount', src_path, dst_path] 959 if skip_mtab: 960 cmd += ['-n'] 961 if fs_type: 962 cmd += ['-t', fs_type] 963 if mount_opts: 964 cmd += ['-o', ','.join(mount_opts)] 965 runcmd(cmd, **kwargs) 966 967 968def MountTmpfsDir(path, name='osutils.tmpfs', size='5G', 969 mount_opts=('nodev', 'noexec', 'nosuid'), **kwargs): 970 """Mount a tmpfs at |path| 971 972 Args: 973 path: Directory to mount the tmpfs. 974 name: Friendly name to include in mount output. 975 size: Size of the temp fs. 976 mount_opts: List of options to pass to `mount`. 977 kwargs: Pass all other args to MountDir. 978 """ 979 mount_opts = list(mount_opts) + ['size=%s' % size] 980 MountDir(name, path, fs_type='tmpfs', mount_opts=mount_opts, **kwargs) 981 982 983def UmountDir(path, lazy=True, sudo=True, cleanup=True): 984 """Unmount a previously mounted temp fs mount. 985 986 Args: 987 path: Directory to unmount. 988 lazy: Whether to do a lazy unmount. 989 sudo: Run through sudo. 990 cleanup: Whether to delete the |path| after unmounting. 991 Note: Does not work when |lazy| is set. 992 """ 993 if sudo: 994 runcmd = cros_build_lib.sudo_run 995 else: 996 runcmd = cros_build_lib.run 997 998 cmd = ['umount', '-d', path] 999 if lazy: 1000 cmd += ['-l'] 1001 runcmd(cmd, debug_level=logging.DEBUG) 1002 1003 if cleanup: 1004 # We will randomly get EBUSY here even when the umount worked. Suspect 1005 # this is due to the host distro doing stupid crap on us like autoscanning 1006 # directories when they get mounted. 1007 def _retry(e): 1008 # When we're using `rm` (which is required for sudo), we can't cleanly 1009 # detect the aforementioned failure. This is because `rm` will see the 1010 # errno, handle itself, and then do exit(1). Which means all we see is 1011 # that rm failed. Assume it's this issue as -rf will ignore most things. 1012 if isinstance(e, cros_build_lib.RunCommandError): 1013 return True 1014 elif isinstance(e, OSError): 1015 # When we aren't using sudo, we do the unlink ourselves, so the exact 1016 # errno is bubbled up to us and we can detect it specifically without 1017 # potentially ignoring all other possible failures. 1018 return e.errno == errno.EBUSY 1019 else: 1020 # Something else, we don't know so do not retry. 1021 return False 1022 retry_util.GenericRetry(_retry, 60, RmDir, path, sudo=sudo, sleep=1) 1023 1024 1025def UmountTree(path): 1026 """Unmounts |path| and any submounts under it.""" 1027 # Scrape it from /proc/mounts since it's easily accessible; 1028 # additionally, unmount in reverse order of what's listed there 1029 # rather than trying a reverse sorting; it's possible for 1030 # mount /z /foon 1031 # mount /foon/blah -o loop /a 1032 # which reverse sorting cannot handle. 1033 path = os.path.realpath(path).rstrip('/') + '/' 1034 mounts = [mtab.destination for mtab in IterateMountPoints() if 1035 mtab.destination.startswith(path) or 1036 mtab.destination == path.rstrip('/')] 1037 1038 for mount_pt in reversed(mounts): 1039 UmountDir(mount_pt, lazy=False, cleanup=False) 1040 1041 1042def SetEnvironment(env): 1043 """Restore the environment variables to that of passed in dictionary.""" 1044 os.environ.clear() 1045 os.environ.update(env) 1046 1047 1048def SourceEnvironment(script, whitelist, ifs=',', env=None, multiline=False): 1049 """Returns the environment exported by a shell script. 1050 1051 Note that the script is actually executed (sourced), so do not use this on 1052 files that have side effects (such as modify the file system). Stdout will 1053 be sent to /dev/null, so just echoing is OK. 1054 1055 Args: 1056 script: The shell script to 'source'. 1057 whitelist: An iterable of environment variables to retrieve values for. 1058 ifs: When showing arrays, what separator to use. 1059 env: A dict of the initial env to pass down. You can also pass it None 1060 (to clear the env) or True (to preserve the current env). 1061 multiline: Allow a variable to span multiple lines. 1062 1063 Returns: 1064 A dictionary containing the values of the whitelisted environment 1065 variables that are set. 1066 """ 1067 dump_script = ['source "%s" >/dev/null' % script, 1068 'IFS="%s"' % ifs] 1069 for var in whitelist: 1070 # Note: If we want to get more exact results out of bash, we should switch 1071 # to using `declare -p "${var}"`. It would require writing a custom parser 1072 # here, but it would be more robust. 1073 dump_script.append( 1074 '[[ "${%(var)s+set}" == "set" ]] && echo "%(var)s=\\"${%(var)s[*]}\\""' 1075 % {'var': var}) 1076 dump_script.append('exit 0') 1077 1078 if env is None: 1079 env = {} 1080 elif env is True: 1081 env = None 1082 output = cros_build_lib.run(['bash'], env=env, capture_output=True, 1083 print_cmd=False, encoding='utf-8', 1084 input='\n'.join(dump_script)).output 1085 return key_value_store.LoadData(output, multiline=multiline) 1086 1087 1088def ListBlockDevices(device_path=None, in_bytes=False): 1089 """Lists all block devices. 1090 1091 Args: 1092 device_path: device path (e.g. /dev/sdc). 1093 in_bytes: whether to display size in bytes. 1094 1095 Returns: 1096 A list of BlockDevice items with attributes 'NAME', 'RM', 'TYPE', 1097 'SIZE' (RM stands for removable). 1098 """ 1099 keys = ['NAME', 'RM', 'TYPE', 'SIZE'] 1100 BlockDevice = collections.namedtuple('BlockDevice', keys) 1101 1102 cmd = ['lsblk', '--pairs'] 1103 if in_bytes: 1104 cmd.append('--bytes') 1105 1106 if device_path: 1107 cmd.append(device_path) 1108 1109 cmd += ['--output', ','.join(keys)] 1110 result = cros_build_lib.dbg_run(cmd, capture_output=True, encoding='utf-8') 1111 devices = [] 1112 for line in result.stdout.strip().splitlines(): 1113 d = {} 1114 for k, v in re.findall(r'(\S+?)=\"(.+?)\"', line): 1115 d[k] = v 1116 1117 devices.append(BlockDevice(**d)) 1118 1119 return devices 1120 1121 1122def GetDeviceInfo(device, keyword='model'): 1123 """Get information of |device| by searching through device path. 1124 1125 Looks for the file named |keyword| in the path upwards from 1126 /sys/block/|device|/device. This path is a symlink and will be fully 1127 expanded when searching. 1128 1129 Args: 1130 device: Device name (e.g. 'sdc'). 1131 keyword: The filename to look for (e.g. product, model). 1132 1133 Returns: 1134 The content of the |keyword| file. 1135 """ 1136 device_path = os.path.join('/sys', 'block', device) 1137 if not os.path.isdir(device_path): 1138 raise ValueError('%s is not a valid device path.' % device_path) 1139 1140 path_list = ExpandPath(os.path.join(device_path, 'device')).split(os.path.sep) 1141 while len(path_list) > 2: 1142 target = os.path.join(os.path.sep.join(path_list), keyword) 1143 if os.path.isfile(target): 1144 return ReadFile(target).strip() 1145 1146 path_list = path_list[:-1] 1147 1148 1149def GetDeviceSize(device_path, in_bytes=False): 1150 """Returns the size of |device|. 1151 1152 Args: 1153 device_path: Device path (e.g. '/dev/sdc'). 1154 in_bytes: If set True, returns the size in bytes. 1155 1156 Returns: 1157 Size of the device in human readable format unless |in_bytes| is set. 1158 """ 1159 devices = ListBlockDevices(device_path=device_path, in_bytes=in_bytes) 1160 for d in devices: 1161 if d.TYPE == 'disk': 1162 return int(d.SIZE) if in_bytes else d.SIZE 1163 1164 raise ValueError('No size info of %s is found.' % device_path) 1165 1166 1167FileInfo = collections.namedtuple( 1168 'FileInfo', ['path', 'owner', 'size', 'atime', 'mtime']) 1169 1170 1171def StatFilesInDirectory(path, recursive=False, to_string=False): 1172 """Stat files in the directory |path|. 1173 1174 Args: 1175 path: Path to the target directory. 1176 recursive: Whether to recurisvely list all files in |path|. 1177 to_string: Whether to return a string containing the metadata of the 1178 files. 1179 1180 Returns: 1181 If |to_string| is False, returns a list of FileInfo objects. Otherwise, 1182 returns a string of metadata of the files. 1183 """ 1184 path = ExpandPath(path) 1185 def ToFileInfo(path, stat_val): 1186 return FileInfo(path, 1187 pwd.getpwuid(stat_val.st_uid)[0], 1188 stat_val.st_size, 1189 datetime.datetime.fromtimestamp(stat_val.st_atime), 1190 datetime.datetime.fromtimestamp(stat_val.st_mtime)) 1191 1192 file_infos = [] 1193 for root, dirs, files in os.walk(path, topdown=True): 1194 for filename in dirs + files: 1195 filepath = os.path.join(root, filename) 1196 file_infos.append(ToFileInfo(filepath, os.lstat(filepath))) 1197 1198 if not recursive: 1199 # Process only the top-most directory. 1200 break 1201 1202 if not to_string: 1203 return file_infos 1204 1205 msg = 'Listing the content of %s' % path 1206 msg_format = ('Path: {x.path}, Owner: {x.owner}, Size: {x.size} bytes, ' 1207 'Accessed: {x.atime}, Modified: {x.mtime}') 1208 msg = '%s\n%s' % (msg, 1209 '\n'.join([msg_format.format(x=x) for x in file_infos])) 1210 return msg 1211 1212 1213@contextlib.contextmanager 1214def ChdirContext(target_dir): 1215 """A context manager to chdir() into |target_dir| and back out on exit. 1216 1217 Args: 1218 target_dir: A target directory to chdir into. 1219 """ 1220 1221 cwd = os.getcwd() 1222 os.chdir(target_dir) 1223 try: 1224 yield 1225 finally: 1226 os.chdir(cwd) 1227 1228 1229def _SameFileSystem(path1, path2): 1230 """Determine whether two paths are on the same filesystem. 1231 1232 Be resilient to nonsense paths. Return False instead of blowing up. 1233 """ 1234 try: 1235 return os.stat(path1).st_dev == os.stat(path2).st_dev 1236 except OSError: 1237 return False 1238 1239 1240class MountOverlayContext(object): 1241 """A context manager for mounting an OverlayFS directory. 1242 1243 An overlay filesystem will be mounted at |mount_dir|, and will be unmounted 1244 when the context exits. 1245 """ 1246 1247 OVERLAY_FS_MOUNT_ERRORS = (32,) 1248 def __init__(self, lower_dir, upper_dir, mount_dir, cleanup=False): 1249 """Initialize. 1250 1251 Args: 1252 lower_dir: The lower directory (read-only). 1253 upper_dir: The upper directory (read-write). 1254 mount_dir: The mount point for the merged overlay. 1255 cleanup: Whether to remove the mount point after unmounting. This uses an 1256 internal retry logic for cases where unmount is successful but the 1257 directory still appears busy, and is generally more resilient than 1258 removing it independently. 1259 """ 1260 self._lower_dir = lower_dir 1261 self._upper_dir = upper_dir 1262 self._mount_dir = mount_dir 1263 self._cleanup = cleanup 1264 self.tempdir = None 1265 1266 def __enter__(self): 1267 # Upstream Kernel 3.18 and the ubuntu backport of overlayfs have different 1268 # APIs. We must support both. 1269 try_legacy = False 1270 stashed_e_overlay_str = None 1271 1272 # We must ensure that upperdir and workdir are on the same filesystem. 1273 if _SameFileSystem(self._upper_dir, GetGlobalTempDir()): 1274 _TempDirSetup(self) 1275 elif _SameFileSystem(self._upper_dir, os.path.dirname(self._upper_dir)): 1276 _TempDirSetup(self, base_dir=os.path.dirname(self._upper_dir)) 1277 else: 1278 logging.debug('Could create find a workdir on the same filesystem as %s. ' 1279 'Trying legacy API instead.', 1280 self._upper_dir) 1281 try_legacy = True 1282 1283 if not try_legacy: 1284 try: 1285 MountDir('overlay', self._mount_dir, fs_type='overlay', makedirs=False, 1286 mount_opts=('lowerdir=%s' % self._lower_dir, 1287 'upperdir=%s' % self._upper_dir, 1288 'workdir=%s' % self.tempdir), 1289 quiet=True) 1290 except cros_build_lib.RunCommandError as e_overlay: 1291 if e_overlay.result.returncode not in self.OVERLAY_FS_MOUNT_ERRORS: 1292 raise 1293 logging.debug('Failed to mount overlay filesystem. Trying legacy API.') 1294 stashed_e_overlay_str = str(e_overlay) 1295 try_legacy = True 1296 1297 if try_legacy: 1298 try: 1299 MountDir('overlayfs', self._mount_dir, fs_type='overlayfs', 1300 makedirs=False, 1301 mount_opts=('lowerdir=%s' % self._lower_dir, 1302 'upperdir=%s' % self._upper_dir), 1303 quiet=True) 1304 except cros_build_lib.RunCommandError as e_overlayfs: 1305 logging.error('All attempts at mounting overlay filesystem failed.') 1306 if stashed_e_overlay_str is not None: 1307 logging.error('overlay: %s', stashed_e_overlay_str) 1308 logging.error('overlayfs: %s', str(e_overlayfs)) 1309 raise 1310 1311 return self 1312 1313 def __exit__(self, exc_type, exc_value, traceback): 1314 UmountDir(self._mount_dir, cleanup=self._cleanup) 1315 _TempDirTearDown(self, force_sudo=True) 1316 1317 1318MountInfo = collections.namedtuple( 1319 'MountInfo', 1320 'source destination filesystem options') 1321 1322 1323def IterateMountPoints(proc_file='/proc/mounts'): 1324 """Iterate over all mounts as reported by "/proc/mounts". 1325 1326 Args: 1327 proc_file: A path to a file whose content is similar to /proc/mounts. 1328 Default to "/proc/mounts" itself. 1329 1330 Returns: 1331 A generator that yields MountInfo objects. 1332 """ 1333 with open(proc_file) as f: 1334 for line in f: 1335 # Escape any \xxx to a char. 1336 source, destination, filesystem, options, _, _ = [ 1337 re.sub(r'\\([0-7]{3})', lambda m: chr(int(m.group(1), 8)), x) 1338 for x in line.split() 1339 ] 1340 mtab = MountInfo(source, destination, filesystem, options) 1341 yield mtab 1342 1343 1344def IsMounted(path): 1345 """Determine if |path| is already mounted or not.""" 1346 path = os.path.realpath(path).rstrip('/') 1347 mounts = [mtab.destination for mtab in IterateMountPoints()] 1348 if path in mounts: 1349 return True 1350 1351 return False 1352 1353 1354def ResolveSymlinkInRoot(file_name, root): 1355 """Resolve a symlink |file_name| relative to |root|. 1356 1357 This can be used to resolve absolute symlinks within an alternative root 1358 path (i.e. chroot). For example: 1359 1360 ROOT-A/absolute_symlink --> /an/abs/path 1361 ROOT-A/relative_symlink --> a/relative/path 1362 1363 absolute_symlink will be resolved to ROOT-A/an/abs/path 1364 relative_symlink will be resolved to ROOT-A/a/relative/path 1365 1366 Args: 1367 file_name (str): A path to the file. 1368 root (str|None): A path to the root directory. 1369 1370 Returns: 1371 |file_name| if |file_name| is not a symlink. Otherwise, the ultimate path 1372 that |file_name| points to, with links resolved relative to |root|. 1373 """ 1374 count = 0 1375 while os.path.islink(file_name): 1376 count += 1 1377 if count > 128: 1378 raise ValueError('Too many link levels for %s.' % file_name) 1379 link = os.readlink(file_name) 1380 if link.startswith('/'): 1381 file_name = os.path.join(root, link[1:]) if root else link 1382 else: 1383 file_name = os.path.join(os.path.dirname(file_name), link) 1384 return file_name 1385 1386 1387def ResolveSymlink(file_name): 1388 """Resolve a symlink |file_name| to an absolute path. 1389 1390 This is similar to ResolveSymlinkInRoot, but does not resolve absolute 1391 symlinks to an alternative root, and normalizes the path before returning. 1392 1393 Args: 1394 file_name (str): The symlink. 1395 1396 Returns: 1397 str - |file_name| if |file_name| is not a symlink. Otherwise, the ultimate 1398 path that |file_name| points to. 1399 """ 1400 return os.path.realpath(ResolveSymlinkInRoot(file_name, None)) 1401 1402 1403def IsInsideVm(): 1404 """Return True if we are running inside a virtual machine. 1405 1406 The detection is based on the model of the hard drive. 1407 """ 1408 for blk_model in glob.glob('/sys/block/*/device/model'): 1409 if os.path.isfile(blk_model): 1410 model = ReadFile(blk_model) 1411 if model.startswith('VBOX') or model.startswith('VMware'): 1412 return True 1413 1414 return False 1415