blob: 3534760683baecc2e1e34e4d3882f08c7e63e7e3 [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
hinoka@chromium.org7a790542014-12-10 02:04:39 +00007import argparse
hinoka@chromium.org7a790542014-12-10 02:04:39 +00008import base64
dnj@chromium.org605d81d2015-09-18 22:33:53 +00009import contextlib
primiano@chromium.orgdf351762014-12-18 11:12:34 +000010import hashlib
hinoka@chromium.org7a790542014-12-10 02:04:39 +000011import json
primiano@chromium.orgdf351762014-12-18 11:12:34 +000012import os
13import shutil
hinoka@chromium.org7a790542014-12-10 02:04:39 +000014import subprocess
primiano@chromium.orgdf351762014-12-18 11:12:34 +000015import sys
dnj@chromium.org605d81d2015-09-18 22:33:53 +000016import tempfile
17import time
Gavin Mak1c1cc062023-08-30 15:33:44 +000018import urllib.request
Raul Tambreb946b232019-03-26 14:48:46 +000019
primiano@chromium.orgdf351762014-12-18 11:12:34 +000020import zipfile
hinoka@chromium.org7a790542014-12-10 02:04:39 +000021
hinoka@chromium.org7a790542014-12-10 02:04:39 +000022GSUTIL_URL = 'https://storage.googleapis.com/pub/'
23API_URL = 'https://www.googleapis.com/storage/v1/b/pub/o/'
24
25THIS_DIR = os.path.dirname(os.path.abspath(__file__))
26DEFAULT_BIN_DIR = os.path.join(THIS_DIR, 'external_bin', 'gsutil')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000027
Dan Jacques509776e2017-09-07 18:01:08 -070028IS_WINDOWS = os.name == 'nt'
29
Josip Sokcevic19096962022-03-10 17:56:09 +000030VERSION = '4.68'
Josip Sokcevicfa474e82021-09-17 16:59:49 +000031
Aravind Vasudevan7af61692023-01-09 23:15:15 +000032# Google OAuth Context required by gsutil.
33LUCI_AUTH_SCOPES = [
34 'https://www.googleapis.com/auth/devstorage.full_control',
35 'https://www.googleapis.com/auth/userinfo.email',
36]
37
Dan Jacques509776e2017-09-07 18:01:08 -070038
Aravind Vasudevana413ee72023-11-01 22:19:25 +000039# Platforms unsupported by luci-auth.
40LUCI_AUTH_UNSUPPORTED_PLATFORMS = ['aix']
41
42
hinoka@chromium.org7a790542014-12-10 02:04:39 +000043class InvalidGsutilError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000044 pass
hinoka@chromium.org7a790542014-12-10 02:04:39 +000045
46
hinoka@chromium.org7a790542014-12-10 02:04:39 +000047def download_gsutil(version, target_dir):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000048 """Downloads gsutil into the target_dir."""
49 filename = 'gsutil_%s.zip' % version
50 target_filename = os.path.join(target_dir, filename)
hinoka@chromium.org7a790542014-12-10 02:04:39 +000051
Mike Frysinger124bb8e2023-09-06 05:48:55 +000052 # Check if the target exists already.
53 if os.path.exists(target_filename):
54 md5_calc = hashlib.md5()
55 with open(target_filename, 'rb') as f:
56 while True:
57 buf = f.read(4096)
58 if not buf:
59 break
60 md5_calc.update(buf)
61 local_md5 = md5_calc.hexdigest()
hinoka@chromium.org7a790542014-12-10 02:04:39 +000062
Mike Frysinger124bb8e2023-09-06 05:48:55 +000063 metadata_url = '%s%s' % (API_URL, filename)
64 metadata = json.load(urllib.request.urlopen(metadata_url))
65 remote_md5 = base64.b64decode(metadata['md5Hash']).decode('utf-8')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000066
Mike Frysinger124bb8e2023-09-06 05:48:55 +000067 if local_md5 == remote_md5:
68 return target_filename
69 os.remove(target_filename)
hinoka@chromium.org7a790542014-12-10 02:04:39 +000070
Mike Frysinger124bb8e2023-09-06 05:48:55 +000071 # Do the download.
72 url = '%s%s' % (GSUTIL_URL, filename)
73 u = urllib.request.urlopen(url)
74 with open(target_filename, 'wb') as f:
75 while True:
76 buf = u.read(4096)
77 if not buf:
78 break
79 f.write(buf)
80 return target_filename
hinoka@chromium.org7a790542014-12-10 02:04:39 +000081
82
dnj@chromium.org605d81d2015-09-18 22:33:53 +000083@contextlib.contextmanager
84def temporary_directory(base):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000085 tmpdir = tempfile.mkdtemp(prefix='t', dir=base)
86 try:
87 yield tmpdir
88 finally:
89 if os.path.isdir(tmpdir):
90 shutil.rmtree(tmpdir)
dnj@chromium.org605d81d2015-09-18 22:33:53 +000091
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000092
dnj@chromium.org605d81d2015-09-18 22:33:53 +000093def ensure_gsutil(version, target, clean):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000094 bin_dir = os.path.join(target, 'gsutil_%s' % version)
95 gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
96 gsutil_flag = os.path.join(bin_dir, 'gsutil', 'install.flag')
97 # We assume that if gsutil_flag exists, then we have a good version
98 # of the gsutil package.
99 if not clean and os.path.isfile(gsutil_flag):
100 # Everything is awesome! we're all done here.
101 return gsutil_bin
102
103 if not os.path.exists(target):
104 try:
105 os.makedirs(target)
106 except FileExistsError:
107 # Another process is prepping workspace, so let's check if
108 # gsutil_bin is present. If after several checks it's still not,
109 # continue with downloading gsutil.
110 delay = 2 # base delay, in seconds
111 for _ in range(3): # make N attempts
112 # sleep first as it's not expected to have file ready just yet.
113 time.sleep(delay)
114 delay *= 1.5 # next delay increased by that factor
115 if os.path.isfile(gsutil_bin):
116 return gsutil_bin
117
118 with temporary_directory(target) as instance_dir:
119 # Clean up if we're redownloading a corrupted gsutil.
120 cleanup_path = os.path.join(instance_dir, 'clean')
121 try:
122 os.rename(bin_dir, cleanup_path)
123 except (OSError, IOError):
124 cleanup_path = None
125 if cleanup_path:
126 shutil.rmtree(cleanup_path)
127
128 download_dir = os.path.join(instance_dir, 'd')
129 target_zip_filename = download_gsutil(version, instance_dir)
130 with zipfile.ZipFile(target_zip_filename, 'r') as target_zip:
131 target_zip.extractall(download_dir)
132
133 shutil.move(download_dir, bin_dir)
134 # Final check that the gsutil bin exists. This should never fail.
135 if not os.path.isfile(gsutil_bin):
136 raise InvalidGsutilError()
137 # Drop a flag file.
138 with open(gsutil_flag, 'w') as f:
139 f.write('This flag file is dropped by gsutil.py')
140
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000141 return gsutil_bin
142
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000143
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000144def _is_luci_context():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000145 """Returns True if the script is run within luci-context"""
146 if os.getenv('SWARMING_HEADLESS') == '1':
147 return True
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000148
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000149 luci_context_env = os.getenv('LUCI_CONTEXT')
150 if not luci_context_env:
151 return False
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000152
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000153 try:
154 with open(luci_context_env) as f:
155 luci_context_json = json.load(f)
156 return 'local_auth' in luci_context_json
157 except (ValueError, FileNotFoundError):
158 return False
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000159
160
Aravind Vasudevana413ee72023-11-01 22:19:25 +0000161def _is_luci_auth_supported_platform():
162 """Returns True if luci-auth is supported in the current platform."""
163 return not any(map(sys.platform.startswith,
164 LUCI_AUTH_UNSUPPORTED_PLATFORMS))
165
166
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000167def luci_context(cmd):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000168 """Helper to call`luci-auth context`."""
169 p = _luci_auth_cmd('context', wrapped_cmds=cmd)
Aravind Vasudevaneffdecd2023-01-30 17:02:17 +0000170
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000171 # If luci-auth is not logged in, fallback to normal execution.
172 if b'Not logged in.' in p.stderr:
173 return _run_subprocess(cmd, interactive=True)
Aravind Vasudevaneffdecd2023-01-30 17:02:17 +0000174
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000175 _print_subprocess_result(p)
176 return p
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000177
178
179def luci_login():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000180 """Helper to run `luci-auth login`."""
181 # luci-auth requires interactive shell.
182 return _luci_auth_cmd('login', interactive=True)
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000183
184
Aravind Vasudevan17576772023-01-13 19:50:51 +0000185def _luci_auth_cmd(luci_cmd, wrapped_cmds=None, interactive=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000186 """Helper to call luci-auth command."""
187 cmd = ['luci-auth', luci_cmd, '-scopes', ' '.join(LUCI_AUTH_SCOPES)]
188 if wrapped_cmds:
189 cmd += ['--'] + wrapped_cmds
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000190
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000191 return _run_subprocess(cmd, interactive)
Aravind Vasudevanb7d8efd2023-01-27 18:46:40 +0000192
193
Aravind Vasudevan17576772023-01-13 19:50:51 +0000194def _run_subprocess(cmd, interactive=False, env=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000195 """Wrapper to run the given command within a subprocess."""
196 kwargs = {'shell': IS_WINDOWS}
Aravind Vasudevan17576772023-01-13 19:50:51 +0000197
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000198 if env:
199 kwargs['env'] = dict(os.environ, **env)
Aravind Vasudevan17576772023-01-13 19:50:51 +0000200
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000201 if not interactive:
202 kwargs['stdout'] = subprocess.PIPE
203 kwargs['stderr'] = subprocess.PIPE
Aravind Vasudevan17576772023-01-13 19:50:51 +0000204
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000205 return subprocess.run(cmd, **kwargs)
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000206
207
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000208def _print_subprocess_result(p):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000209 """Prints the subprocess result to stdout & stderr."""
210 if p.stdout:
211 sys.stdout.buffer.write(p.stdout)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000212
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000213 if p.stderr:
214 sys.stderr.buffer.write(p.stderr)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000215
216
Aravind Vasudevan7b1f98e2023-01-18 17:34:15 +0000217def is_boto_present():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000218 """Returns true if the .boto file is present in the default path."""
219 return os.getenv('BOTO_CONFIG') or os.getenv(
220 'AWS_CREDENTIAL_FILE') or os.path.isfile(
221 os.path.join(os.path.expanduser('~'), '.boto'))
Aravind Vasudevan7b1f98e2023-01-18 17:34:15 +0000222
223
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000224def run_gsutil(target, args, clean=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000225 # Redirect gsutil config calls to luci-auth.
226 if 'config' in args:
227 return luci_login().returncode
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000228
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000229 gsutil_bin = ensure_gsutil(VERSION, target, clean)
230 args_opt = ['-o', 'GSUtil:software_update_check_period=0']
Dan Jacques509776e2017-09-07 18:01:08 -0700231
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000232 if sys.platform == 'darwin':
233 # We are experiencing problems with multiprocessing on MacOS where
234 # gsutil.py may hang. This behavior is documented in gsutil codebase,
235 # and recommendation is to set GSUtil:parallel_process_count=1.
236 # https://github.com/GoogleCloudPlatform/gsutil/blob/06efc9dc23719fab4fd5fadb506d252bbd3fe0dd/gslib/command.py#L1331
237 # https://github.com/GoogleCloudPlatform/gsutil/issues/1100
238 args_opt.extend(['-o', 'GSUtil:parallel_process_count=1'])
239 if sys.platform == 'cygwin':
240 # This script requires Windows Python, so invoke with depot_tools'
241 # Python.
242 def winpath(path):
243 stdout = subprocess.check_output(['cygpath', '-w', path])
244 return stdout.strip().decode('utf-8', 'replace')
Chris Nardiab816ce2017-10-31 15:45:05 -0400245
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000246 cmd = ['python.bat', winpath(__file__)]
247 cmd.extend(args)
248 sys.exit(subprocess.call(cmd))
249 assert sys.platform != 'cygwin'
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000250
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000251 cmd = [
252 'vpython3', '-vpython-spec',
253 os.path.join(THIS_DIR, 'gsutil.vpython3'), '--', gsutil_bin
254 ] + args_opt + args
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000255
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000256 # When .boto is present, try without additional wrappers and handle specific
257 # errors.
258 if is_boto_present():
259 p = _run_subprocess(cmd)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000260
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000261 # Notify user that their .boto file might be outdated.
262 if b'Your credentials are invalid.' in p.stderr:
263 # Make sure this error message is visible when invoked by gclient
264 # runhooks
265 separator = '*' * 80
266 print(
267 '\n' + separator + '\n' +
268 'Warning: You might have an outdated .boto file. If this issue '
269 'persists after running `gsutil.py config`, try removing your '
270 '.boto, usually located in your home directory.\n' + separator +
271 '\n',
272 file=sys.stderr)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000273
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000274 _print_subprocess_result(p)
275 return p.returncode
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000276
Aravind Vasudevana413ee72023-11-01 22:19:25 +0000277 # Skip wrapping commands if luci-auth is already being used or if the
278 # platform is unsupported by luci-auth.
279 if _is_luci_context() or not _is_luci_auth_supported_platform():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000280 return _run_subprocess(cmd, interactive=True).returncode
281
282 # Wrap gsutil with luci-auth context.
283 return luci_context(cmd).returncode
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000284
285
286def parse_args():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000287 bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000288
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000289 # Help is disabled as it conflicts with gsutil -h, which controls headers.
290 parser = argparse.ArgumentParser(add_help=False)
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000291
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000292 parser.add_argument(
293 '--clean',
294 action='store_true',
295 help='Clear any existing gsutil package, forcing a new download.')
296 parser.add_argument(
297 '--target',
298 default=bin_dir,
299 help='The target directory to download/store a gsutil version in. '
300 '(default is %(default)s).')
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000301
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000302 # These two args exist for backwards-compatibility but are no-ops.
303 parser.add_argument('--force-version',
304 default=VERSION,
305 help='(deprecated, this flag has no effect)')
306 parser.add_argument('--fallback',
307 help='(deprecated, this flag has no effect)')
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000308
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000309 parser.add_argument('args', nargs=argparse.REMAINDER)
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000310
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000311 args, extras = parser.parse_known_args()
312 if args.args and args.args[0] == '--':
313 args.args.pop(0)
314 if extras:
315 args.args = extras + args.args
316 return args
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000317
318
319def main():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000320 args = parse_args()
321 return run_gsutil(args.target, args.args, clean=args.clean)
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000322
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000323
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000324if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000325 sys.exit(main())