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