blob: 1e90f56617cbcb9f3d8f9badf78ef600b5a165ce [file] [log] [blame]
Josip Sokcevicfb12b3f2021-04-19 18:09:50 +00001#!/usr/bin/env python3
hinoka@chromium.org7a790542014-12-10 02:04:39 +00002# 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.
hinoka@chromium.org7a790542014-12-10 02:04:39 +00005"""Run a pinned gsutil."""
6
Aravind Vasudevan7af61692023-01-09 23:15:15 +00007from __future__ import print_function
hinoka@chromium.org7a790542014-12-10 02:04:39 +00008
9import argparse
hinoka@chromium.org7a790542014-12-10 02:04:39 +000010import base64
dnj@chromium.org605d81d2015-09-18 22:33:53 +000011import contextlib
primiano@chromium.orgdf351762014-12-18 11:12:34 +000012import hashlib
hinoka@chromium.org7a790542014-12-10 02:04:39 +000013import json
primiano@chromium.orgdf351762014-12-18 11:12:34 +000014import os
15import shutil
hinoka@chromium.org7a790542014-12-10 02:04:39 +000016import subprocess
primiano@chromium.orgdf351762014-12-18 11:12:34 +000017import sys
dnj@chromium.org605d81d2015-09-18 22:33:53 +000018import tempfile
19import time
Gavin Mak1c1cc062023-08-30 15:33:44 +000020import urllib.request
Raul Tambreb946b232019-03-26 14:48:46 +000021
primiano@chromium.orgdf351762014-12-18 11:12:34 +000022import zipfile
hinoka@chromium.org7a790542014-12-10 02:04:39 +000023
hinoka@chromium.org7a790542014-12-10 02:04:39 +000024GSUTIL_URL = 'https://storage.googleapis.com/pub/'
25API_URL = 'https://www.googleapis.com/storage/v1/b/pub/o/'
26
27THIS_DIR = os.path.dirname(os.path.abspath(__file__))
28DEFAULT_BIN_DIR = os.path.join(THIS_DIR, 'external_bin', 'gsutil')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000029
Dan Jacques509776e2017-09-07 18:01:08 -070030IS_WINDOWS = os.name == 'nt'
31
Josip Sokcevic19096962022-03-10 17:56:09 +000032VERSION = '4.68'
Josip Sokcevicfa474e82021-09-17 16:59:49 +000033
Aravind Vasudevan7af61692023-01-09 23:15:15 +000034# Google OAuth Context required by gsutil.
35LUCI_AUTH_SCOPES = [
36 'https://www.googleapis.com/auth/devstorage.full_control',
37 'https://www.googleapis.com/auth/userinfo.email',
38]
39
Dan Jacques509776e2017-09-07 18:01:08 -070040
hinoka@chromium.org7a790542014-12-10 02:04:39 +000041class InvalidGsutilError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000042 pass
hinoka@chromium.org7a790542014-12-10 02:04:39 +000043
44
hinoka@chromium.org7a790542014-12-10 02:04:39 +000045def download_gsutil(version, target_dir):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000046 """Downloads gsutil into the target_dir."""
47 filename = 'gsutil_%s.zip' % version
48 target_filename = os.path.join(target_dir, filename)
hinoka@chromium.org7a790542014-12-10 02:04:39 +000049
Mike Frysinger124bb8e2023-09-06 05:48:55 +000050 # Check if the target exists already.
51 if os.path.exists(target_filename):
52 md5_calc = hashlib.md5()
53 with open(target_filename, 'rb') as f:
54 while True:
55 buf = f.read(4096)
56 if not buf:
57 break
58 md5_calc.update(buf)
59 local_md5 = md5_calc.hexdigest()
hinoka@chromium.org7a790542014-12-10 02:04:39 +000060
Mike Frysinger124bb8e2023-09-06 05:48:55 +000061 metadata_url = '%s%s' % (API_URL, filename)
62 metadata = json.load(urllib.request.urlopen(metadata_url))
63 remote_md5 = base64.b64decode(metadata['md5Hash']).decode('utf-8')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000064
Mike Frysinger124bb8e2023-09-06 05:48:55 +000065 if local_md5 == remote_md5:
66 return target_filename
67 os.remove(target_filename)
hinoka@chromium.org7a790542014-12-10 02:04:39 +000068
Mike Frysinger124bb8e2023-09-06 05:48:55 +000069 # Do the download.
70 url = '%s%s' % (GSUTIL_URL, filename)
71 u = urllib.request.urlopen(url)
72 with open(target_filename, 'wb') as f:
73 while True:
74 buf = u.read(4096)
75 if not buf:
76 break
77 f.write(buf)
78 return target_filename
hinoka@chromium.org7a790542014-12-10 02:04:39 +000079
80
dnj@chromium.org605d81d2015-09-18 22:33:53 +000081@contextlib.contextmanager
82def temporary_directory(base):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000083 tmpdir = tempfile.mkdtemp(prefix='t', dir=base)
84 try:
85 yield tmpdir
86 finally:
87 if os.path.isdir(tmpdir):
88 shutil.rmtree(tmpdir)
dnj@chromium.org605d81d2015-09-18 22:33:53 +000089
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000090
dnj@chromium.org605d81d2015-09-18 22:33:53 +000091def ensure_gsutil(version, target, clean):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000092 bin_dir = os.path.join(target, 'gsutil_%s' % version)
93 gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
94 gsutil_flag = os.path.join(bin_dir, 'gsutil', 'install.flag')
95 # We assume that if gsutil_flag exists, then we have a good version
96 # of the gsutil package.
97 if not clean and os.path.isfile(gsutil_flag):
98 # Everything is awesome! we're all done here.
99 return gsutil_bin
100
101 if not os.path.exists(target):
102 try:
103 os.makedirs(target)
104 except FileExistsError:
105 # Another process is prepping workspace, so let's check if
106 # gsutil_bin is present. If after several checks it's still not,
107 # continue with downloading gsutil.
108 delay = 2 # base delay, in seconds
109 for _ in range(3): # make N attempts
110 # sleep first as it's not expected to have file ready just yet.
111 time.sleep(delay)
112 delay *= 1.5 # next delay increased by that factor
113 if os.path.isfile(gsutil_bin):
114 return gsutil_bin
115
116 with temporary_directory(target) as instance_dir:
117 # Clean up if we're redownloading a corrupted gsutil.
118 cleanup_path = os.path.join(instance_dir, 'clean')
119 try:
120 os.rename(bin_dir, cleanup_path)
121 except (OSError, IOError):
122 cleanup_path = None
123 if cleanup_path:
124 shutil.rmtree(cleanup_path)
125
126 download_dir = os.path.join(instance_dir, 'd')
127 target_zip_filename = download_gsutil(version, instance_dir)
128 with zipfile.ZipFile(target_zip_filename, 'r') as target_zip:
129 target_zip.extractall(download_dir)
130
131 shutil.move(download_dir, bin_dir)
132 # Final check that the gsutil bin exists. This should never fail.
133 if not os.path.isfile(gsutil_bin):
134 raise InvalidGsutilError()
135 # Drop a flag file.
136 with open(gsutil_flag, 'w') as f:
137 f.write('This flag file is dropped by gsutil.py')
138
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000139 return gsutil_bin
140
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000141
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000142def _is_luci_context():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000143 """Returns True if the script is run within luci-context"""
144 if os.getenv('SWARMING_HEADLESS') == '1':
145 return True
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000146
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000147 luci_context_env = os.getenv('LUCI_CONTEXT')
148 if not luci_context_env:
149 return False
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000150
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000151 try:
152 with open(luci_context_env) as f:
153 luci_context_json = json.load(f)
154 return 'local_auth' in luci_context_json
155 except (ValueError, FileNotFoundError):
156 return False
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000157
158
159def luci_context(cmd):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000160 """Helper to call`luci-auth context`."""
161 p = _luci_auth_cmd('context', wrapped_cmds=cmd)
Aravind Vasudevaneffdecd2023-01-30 17:02:17 +0000162
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000163 # If luci-auth is not logged in, fallback to normal execution.
164 if b'Not logged in.' in p.stderr:
165 return _run_subprocess(cmd, interactive=True)
Aravind Vasudevaneffdecd2023-01-30 17:02:17 +0000166
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000167 _print_subprocess_result(p)
168 return p
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000169
170
171def luci_login():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000172 """Helper to run `luci-auth login`."""
173 # luci-auth requires interactive shell.
174 return _luci_auth_cmd('login', interactive=True)
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000175
176
Aravind Vasudevan17576772023-01-13 19:50:51 +0000177def _luci_auth_cmd(luci_cmd, wrapped_cmds=None, interactive=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000178 """Helper to call luci-auth command."""
179 cmd = ['luci-auth', luci_cmd, '-scopes', ' '.join(LUCI_AUTH_SCOPES)]
180 if wrapped_cmds:
181 cmd += ['--'] + wrapped_cmds
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000182
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000183 return _run_subprocess(cmd, interactive)
Aravind Vasudevanb7d8efd2023-01-27 18:46:40 +0000184
185
Aravind Vasudevan17576772023-01-13 19:50:51 +0000186def _run_subprocess(cmd, interactive=False, env=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000187 """Wrapper to run the given command within a subprocess."""
188 kwargs = {'shell': IS_WINDOWS}
Aravind Vasudevan17576772023-01-13 19:50:51 +0000189
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000190 if env:
191 kwargs['env'] = dict(os.environ, **env)
Aravind Vasudevan17576772023-01-13 19:50:51 +0000192
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000193 if not interactive:
194 kwargs['stdout'] = subprocess.PIPE
195 kwargs['stderr'] = subprocess.PIPE
Aravind Vasudevan17576772023-01-13 19:50:51 +0000196
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000197 return subprocess.run(cmd, **kwargs)
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000198
199
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000200def _print_subprocess_result(p):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000201 """Prints the subprocess result to stdout & stderr."""
202 if p.stdout:
203 sys.stdout.buffer.write(p.stdout)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000204
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000205 if p.stderr:
206 sys.stderr.buffer.write(p.stderr)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000207
208
Aravind Vasudevan7b1f98e2023-01-18 17:34:15 +0000209def is_boto_present():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000210 """Returns true if the .boto file is present in the default path."""
211 return os.getenv('BOTO_CONFIG') or os.getenv(
212 'AWS_CREDENTIAL_FILE') or os.path.isfile(
213 os.path.join(os.path.expanduser('~'), '.boto'))
Aravind Vasudevan7b1f98e2023-01-18 17:34:15 +0000214
215
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000216def run_gsutil(target, args, clean=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000217 # Redirect gsutil config calls to luci-auth.
218 if 'config' in args:
219 return luci_login().returncode
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000220
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000221 gsutil_bin = ensure_gsutil(VERSION, target, clean)
222 args_opt = ['-o', 'GSUtil:software_update_check_period=0']
Dan Jacques509776e2017-09-07 18:01:08 -0700223
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000224 if sys.platform == 'darwin':
225 # We are experiencing problems with multiprocessing on MacOS where
226 # gsutil.py may hang. This behavior is documented in gsutil codebase,
227 # and recommendation is to set GSUtil:parallel_process_count=1.
228 # https://github.com/GoogleCloudPlatform/gsutil/blob/06efc9dc23719fab4fd5fadb506d252bbd3fe0dd/gslib/command.py#L1331
229 # https://github.com/GoogleCloudPlatform/gsutil/issues/1100
230 args_opt.extend(['-o', 'GSUtil:parallel_process_count=1'])
231 if sys.platform == 'cygwin':
232 # This script requires Windows Python, so invoke with depot_tools'
233 # Python.
234 def winpath(path):
235 stdout = subprocess.check_output(['cygpath', '-w', path])
236 return stdout.strip().decode('utf-8', 'replace')
Chris Nardiab816ce2017-10-31 15:45:05 -0400237
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000238 cmd = ['python.bat', winpath(__file__)]
239 cmd.extend(args)
240 sys.exit(subprocess.call(cmd))
241 assert sys.platform != 'cygwin'
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000242
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000243 cmd = [
244 'vpython3', '-vpython-spec',
245 os.path.join(THIS_DIR, 'gsutil.vpython3'), '--', gsutil_bin
246 ] + args_opt + args
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000247
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000248 # When .boto is present, try without additional wrappers and handle specific
249 # errors.
250 if is_boto_present():
251 p = _run_subprocess(cmd)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000252
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000253 # Notify user that their .boto file might be outdated.
254 if b'Your credentials are invalid.' in p.stderr:
255 # Make sure this error message is visible when invoked by gclient
256 # runhooks
257 separator = '*' * 80
258 print(
259 '\n' + separator + '\n' +
260 'Warning: You might have an outdated .boto file. If this issue '
261 'persists after running `gsutil.py config`, try removing your '
262 '.boto, usually located in your home directory.\n' + separator +
263 '\n',
264 file=sys.stderr)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000265
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000266 _print_subprocess_result(p)
267 return p.returncode
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000268
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000269 # Skip wrapping commands if luci-auth is already being
270 if _is_luci_context():
271 return _run_subprocess(cmd, interactive=True).returncode
272
273 # Wrap gsutil with luci-auth context.
274 return luci_context(cmd).returncode
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000275
276
277def parse_args():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000278 bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000279
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000280 # Help is disabled as it conflicts with gsutil -h, which controls headers.
281 parser = argparse.ArgumentParser(add_help=False)
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000282
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000283 parser.add_argument(
284 '--clean',
285 action='store_true',
286 help='Clear any existing gsutil package, forcing a new download.')
287 parser.add_argument(
288 '--target',
289 default=bin_dir,
290 help='The target directory to download/store a gsutil version in. '
291 '(default is %(default)s).')
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000292
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000293 # These two args exist for backwards-compatibility but are no-ops.
294 parser.add_argument('--force-version',
295 default=VERSION,
296 help='(deprecated, this flag has no effect)')
297 parser.add_argument('--fallback',
298 help='(deprecated, this flag has no effect)')
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000299
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000300 parser.add_argument('args', nargs=argparse.REMAINDER)
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000301
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000302 args, extras = parser.parse_known_args()
303 if args.args and args.args[0] == '--':
304 args.args.pop(0)
305 if extras:
306 args.args = extras + args.args
307 return args
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000308
309
310def main():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000311 args = parse_args()
312 return run_gsutil(args.target, args.args, clean=args.clean)
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000313
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000314
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000315if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000316 sys.exit(main())