xref: /aosp_15_r20/external/autotest/utils/frozen_chromite/lib/osutils.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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