gsutil.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. #!/usr/bin/env python3
  2. # Copyright 2014 The Chromium 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. """Run a pinned gsutil."""
  6. from __future__ import print_function
  7. import argparse
  8. import base64
  9. import contextlib
  10. import hashlib
  11. import json
  12. import os
  13. import shutil
  14. import subprocess
  15. import sys
  16. import tempfile
  17. import time
  18. try:
  19. import urllib2 as urllib
  20. except ImportError: # For Py3 compatibility
  21. import urllib.request as urllib
  22. import zipfile
  23. GSUTIL_URL = 'https://storage.googleapis.com/pub/'
  24. API_URL = 'https://www.googleapis.com/storage/v1/b/pub/o/'
  25. THIS_DIR = os.path.dirname(os.path.abspath(__file__))
  26. DEFAULT_BIN_DIR = os.path.join(THIS_DIR, 'external_bin', 'gsutil')
  27. IS_WINDOWS = os.name == 'nt'
  28. VERSION = '4.68'
  29. # Environment variable to enable LUCI auth feature.
  30. GSUTIL_ENABLE_LUCI_AUTH = 'GSUTIL_ENABLE_LUCI_AUTH'
  31. # Google OAuth Context required by gsutil.
  32. LUCI_AUTH_SCOPES = [
  33. 'https://www.googleapis.com/auth/devstorage.full_control',
  34. 'https://www.googleapis.com/auth/userinfo.email',
  35. ]
  36. class InvalidGsutilError(Exception):
  37. pass
  38. def download_gsutil(version, target_dir):
  39. """Downloads gsutil into the target_dir."""
  40. filename = 'gsutil_%s.zip' % version
  41. target_filename = os.path.join(target_dir, filename)
  42. # Check if the target exists already.
  43. if os.path.exists(target_filename):
  44. md5_calc = hashlib.md5()
  45. with open(target_filename, 'rb') as f:
  46. while True:
  47. buf = f.read(4096)
  48. if not buf:
  49. break
  50. md5_calc.update(buf)
  51. local_md5 = md5_calc.hexdigest()
  52. metadata_url = '%s%s' % (API_URL, filename)
  53. metadata = json.load(urllib.urlopen(metadata_url))
  54. remote_md5 = base64.b64decode(metadata['md5Hash']).decode('utf-8')
  55. if local_md5 == remote_md5:
  56. return target_filename
  57. os.remove(target_filename)
  58. # Do the download.
  59. url = '%s%s' % (GSUTIL_URL, filename)
  60. u = urllib.urlopen(url)
  61. with open(target_filename, 'wb') as f:
  62. while True:
  63. buf = u.read(4096)
  64. if not buf:
  65. break
  66. f.write(buf)
  67. return target_filename
  68. @contextlib.contextmanager
  69. def temporary_directory(base):
  70. tmpdir = tempfile.mkdtemp(prefix='t', dir=base)
  71. try:
  72. yield tmpdir
  73. finally:
  74. if os.path.isdir(tmpdir):
  75. shutil.rmtree(tmpdir)
  76. def ensure_gsutil(version, target, clean):
  77. bin_dir = os.path.join(target, 'gsutil_%s' % version)
  78. gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
  79. gsutil_flag = os.path.join(bin_dir, 'gsutil', 'install.flag')
  80. # We assume that if gsutil_flag exists, then we have a good version
  81. # of the gsutil package.
  82. if not clean and os.path.isfile(gsutil_flag):
  83. # Everything is awesome! we're all done here.
  84. return gsutil_bin
  85. if not os.path.exists(target):
  86. try:
  87. os.makedirs(target)
  88. except FileExistsError:
  89. # Another process is prepping workspace, so let's check if gsutil_bin is
  90. # present. If after several checks it's still not, continue with
  91. # downloading gsutil.
  92. delay = 2 # base delay, in seconds
  93. for _ in range(3): # make N attempts
  94. # sleep first as it's not expected to have file ready just yet.
  95. time.sleep(delay)
  96. delay *= 1.5 # next delay increased by that factor
  97. if os.path.isfile(gsutil_bin):
  98. return gsutil_bin
  99. with temporary_directory(target) as instance_dir:
  100. # Clean up if we're redownloading a corrupted gsutil.
  101. cleanup_path = os.path.join(instance_dir, 'clean')
  102. try:
  103. os.rename(bin_dir, cleanup_path)
  104. except (OSError, IOError):
  105. cleanup_path = None
  106. if cleanup_path:
  107. shutil.rmtree(cleanup_path)
  108. download_dir = os.path.join(instance_dir, 'd')
  109. target_zip_filename = download_gsutil(version, instance_dir)
  110. with zipfile.ZipFile(target_zip_filename, 'r') as target_zip:
  111. target_zip.extractall(download_dir)
  112. shutil.move(download_dir, bin_dir)
  113. # Final check that the gsutil bin exists. This should never fail.
  114. if not os.path.isfile(gsutil_bin):
  115. raise InvalidGsutilError()
  116. # Drop a flag file.
  117. with open(gsutil_flag, 'w') as f:
  118. f.write('This flag file is dropped by gsutil.py')
  119. return gsutil_bin
  120. def _is_luci_context():
  121. """Returns True if the script is run within luci-context"""
  122. luci_context_env = os.getenv('LUCI_CONTEXT')
  123. if not luci_context_env:
  124. return False
  125. try:
  126. with open(luci_context_env) as f:
  127. luci_context_json = json.load(f)
  128. return 'local_auth' in luci_context_json
  129. except (ValueError, FileNotFoundError):
  130. return False
  131. def luci_context(cmd):
  132. """Helper to call`luci-auth context`."""
  133. return _luci_auth_cmd('context', wrapped_cmds=cmd)
  134. def luci_login():
  135. """Helper to run `luci-auth login`."""
  136. _luci_auth_cmd('login')
  137. def _luci_auth_cmd(luci_cmd, wrapped_cmds=None):
  138. """Helper to call luci-auth command."""
  139. print('WARNING: OOB authentication flow has been deprecated.')
  140. print('Using luci-auth login instead.')
  141. print('Override luci-auth by setting `BOTO_CONFIG` or '
  142. '`AWS_CREDENTIAL_FILE` in your env.\n')
  143. cmd = ['luci-auth', luci_cmd, '-scopes', ' '.join(LUCI_AUTH_SCOPES)]
  144. if wrapped_cmds:
  145. cmd += ['--'] + wrapped_cmds
  146. return _run_subprocess(cmd)
  147. def _run_subprocess(cmd):
  148. """Wrapper to run the given command within a subprocess."""
  149. return subprocess.call(cmd, shell=IS_WINDOWS)
  150. def run_gsutil(target, args, clean=False):
  151. # Redirect gsutil config calls to luci-auth.
  152. if os.getenv(GSUTIL_ENABLE_LUCI_AUTH) == '1' and 'config' in args:
  153. return luci_login()
  154. gsutil_bin = ensure_gsutil(VERSION, target, clean)
  155. args_opt = ['-o', 'GSUtil:software_update_check_period=0']
  156. if sys.platform == 'darwin':
  157. # We are experiencing problems with multiprocessing on MacOS where gsutil.py
  158. # may hang.
  159. # This behavior is documented in gsutil codebase, and recommendation is to
  160. # set GSUtil:parallel_process_count=1.
  161. # https://github.com/GoogleCloudPlatform/gsutil/blob/06efc9dc23719fab4fd5fadb506d252bbd3fe0dd/gslib/command.py#L1331
  162. # https://github.com/GoogleCloudPlatform/gsutil/issues/1100
  163. args_opt.extend(['-o', 'GSUtil:parallel_process_count=1'])
  164. if sys.platform == 'cygwin':
  165. # This script requires Windows Python, so invoke with depot_tools'
  166. # Python.
  167. def winpath(path):
  168. stdout = subprocess.check_output(['cygpath', '-w', path])
  169. return stdout.strip().decode('utf-8', 'replace')
  170. cmd = ['python.bat', winpath(__file__)]
  171. cmd.extend(args)
  172. sys.exit(subprocess.call(cmd))
  173. assert sys.platform != 'cygwin'
  174. cmd = [
  175. 'vpython3',
  176. '-vpython-spec', os.path.join(THIS_DIR, 'gsutil.vpython3'),
  177. '--',
  178. gsutil_bin
  179. ] + args_opt + args
  180. # Bypass luci-auth when run within a bot or .boto file is set.
  181. if (os.getenv(GSUTIL_ENABLE_LUCI_AUTH) != '1' or _is_luci_context()
  182. or os.getenv('SWARMING_HEADLESS') == '1' or os.getenv('BOTO_CONFIG')
  183. or os.getenv('AWS_CREDENTIAL_FILE')):
  184. return _run_subprocess(cmd)
  185. return luci_context(cmd)
  186. def parse_args():
  187. bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
  188. # Help is disabled as it conflicts with gsutil -h, which controls headers.
  189. parser = argparse.ArgumentParser(add_help=False)
  190. parser.add_argument('--clean', action='store_true',
  191. help='Clear any existing gsutil package, forcing a new download.')
  192. parser.add_argument('--target', default=bin_dir,
  193. help='The target directory to download/store a gsutil version in. '
  194. '(default is %(default)s).')
  195. # These two args exist for backwards-compatibility but are no-ops.
  196. parser.add_argument('--force-version', default=VERSION,
  197. help='(deprecated, this flag has no effect)')
  198. parser.add_argument('--fallback',
  199. help='(deprecated, this flag has no effect)')
  200. parser.add_argument('args', nargs=argparse.REMAINDER)
  201. args, extras = parser.parse_known_args()
  202. if args.args and args.args[0] == '--':
  203. args.args.pop(0)
  204. if extras:
  205. args.args = extras + args.args
  206. return args
  207. def main():
  208. args = parse_args()
  209. return run_gsutil(args.target, args.args, clean=args.clean)
  210. if __name__ == '__main__':
  211. sys.exit(main())