blob: c8eb0ff15c2e7b124123d57fe1dd2a37764f4abc [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.
5
6"""Run a pinned gsutil."""
7
Aravind Vasudevan7af61692023-01-09 23:15:15 +00008from __future__ import print_function
hinoka@chromium.org7a790542014-12-10 02:04:39 +00009
10import argparse
hinoka@chromium.org7a790542014-12-10 02:04:39 +000011import base64
dnj@chromium.org605d81d2015-09-18 22:33:53 +000012import contextlib
primiano@chromium.orgdf351762014-12-18 11:12:34 +000013import hashlib
hinoka@chromium.org7a790542014-12-10 02:04:39 +000014import json
primiano@chromium.orgdf351762014-12-18 11:12:34 +000015import os
16import shutil
hinoka@chromium.org7a790542014-12-10 02:04:39 +000017import subprocess
primiano@chromium.orgdf351762014-12-18 11:12:34 +000018import sys
dnj@chromium.org605d81d2015-09-18 22:33:53 +000019import tempfile
20import time
Gavin Mak1c1cc062023-08-30 15:33:44 +000021import urllib.request
Raul Tambreb946b232019-03-26 14:48:46 +000022
primiano@chromium.orgdf351762014-12-18 11:12:34 +000023import zipfile
hinoka@chromium.org7a790542014-12-10 02:04:39 +000024
25
26GSUTIL_URL = 'https://storage.googleapis.com/pub/'
27API_URL = 'https://www.googleapis.com/storage/v1/b/pub/o/'
28
29THIS_DIR = os.path.dirname(os.path.abspath(__file__))
30DEFAULT_BIN_DIR = os.path.join(THIS_DIR, 'external_bin', 'gsutil')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000031
Dan Jacques509776e2017-09-07 18:01:08 -070032IS_WINDOWS = os.name == 'nt'
33
Josip Sokcevic19096962022-03-10 17:56:09 +000034VERSION = '4.68'
Josip Sokcevicfa474e82021-09-17 16:59:49 +000035
Aravind Vasudevan7af61692023-01-09 23:15:15 +000036# Google OAuth Context required by gsutil.
37LUCI_AUTH_SCOPES = [
38 'https://www.googleapis.com/auth/devstorage.full_control',
39 'https://www.googleapis.com/auth/userinfo.email',
40]
41
Dan Jacques509776e2017-09-07 18:01:08 -070042
hinoka@chromium.org7a790542014-12-10 02:04:39 +000043class InvalidGsutilError(Exception):
44 pass
45
46
hinoka@chromium.org7a790542014-12-10 02:04:39 +000047def download_gsutil(version, target_dir):
48 """Downloads gsutil into the target_dir."""
49 filename = 'gsutil_%s.zip' % version
50 target_filename = os.path.join(target_dir, filename)
51
52 # 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()
62
63 metadata_url = '%s%s' % (API_URL, filename)
Gavin Mak1c1cc062023-08-30 15:33:44 +000064 metadata = json.load(urllib.request.urlopen(metadata_url))
Edward Lemur83aafc92019-11-25 23:25:05 +000065 remote_md5 = base64.b64decode(metadata['md5Hash']).decode('utf-8')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000066
67 if local_md5 == remote_md5:
68 return target_filename
69 os.remove(target_filename)
70
71 # Do the download.
72 url = '%s%s' % (GSUTIL_URL, filename)
Gavin Mak1c1cc062023-08-30 15:33:44 +000073 u = urllib.request.urlopen(url)
hinoka@chromium.org7a790542014-12-10 02:04:39 +000074 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
81
82
dnj@chromium.org605d81d2015-09-18 22:33:53 +000083@contextlib.contextmanager
84def temporary_directory(base):
Takuto Ikuta8daf2442021-10-28 05:51:18 +000085 tmpdir = tempfile.mkdtemp(prefix='t', dir=base)
dnj@chromium.org605d81d2015-09-18 22:33:53 +000086 try:
87 yield tmpdir
88 finally:
89 if os.path.isdir(tmpdir):
90 shutil.rmtree(tmpdir)
91
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000092
dnj@chromium.org605d81d2015-09-18 22:33:53 +000093def ensure_gsutil(version, target, clean):
hinoka@chromium.org7a790542014-12-10 02:04:39 +000094 bin_dir = os.path.join(target, 'gsutil_%s' % version)
95 gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
Ryan Tseng83fd81f2017-10-23 11:13:48 -070096 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):
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000100 # Everything is awesome! we're all done here.
101 return gsutil_bin
102
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000103 if not os.path.exists(target):
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000104 try:
105 os.makedirs(target)
106 except FileExistsError:
107 # Another process is prepping workspace, so let's check if gsutil_bin is
108 # present. If after several checks it's still not, continue with
109 # 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
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000118 with temporary_directory(target) as instance_dir:
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000119 # Clean up if we're redownloading a corrupted gsutil.
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000120 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
Takuto Ikuta8daf2442021-10-28 05:51:18 +0000128 download_dir = os.path.join(instance_dir, 'd')
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000129 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
Joanna Wang07d6e692022-09-08 01:49:38 +0000133 shutil.move(download_dir, bin_dir)
Ryan Tseng83fd81f2017-10-23 11:13:48 -0700134 # 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')
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000140
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000141 return gsutil_bin
142
143
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000144def _is_luci_context():
145 """Returns True if the script is run within luci-context"""
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000146 if os.getenv('SWARMING_HEADLESS') == '1':
147 return True
148
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000149 luci_context_env = os.getenv('LUCI_CONTEXT')
150 if not luci_context_env:
151 return False
152
153 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
159
160
161def luci_context(cmd):
162 """Helper to call`luci-auth context`."""
Aravind Vasudevaneffdecd2023-01-30 17:02:17 +0000163 p = _luci_auth_cmd('context', wrapped_cmds=cmd)
164
165 # If luci-auth is not logged in, fallback to normal execution.
166 if b'Not logged in.' in p.stderr:
167 return _run_subprocess(cmd, interactive=True)
168
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000169 _print_subprocess_result(p)
Aravind Vasudevaneffdecd2023-01-30 17:02:17 +0000170 return p
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000171
172
173def luci_login():
174 """Helper to run `luci-auth login`."""
Aravind Vasudevan17576772023-01-13 19:50:51 +0000175 # luci-auth requires interactive shell.
176 return _luci_auth_cmd('login', interactive=True)
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000177
178
Aravind Vasudevan17576772023-01-13 19:50:51 +0000179def _luci_auth_cmd(luci_cmd, wrapped_cmds=None, interactive=False):
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000180 """Helper to call luci-auth command."""
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000181 cmd = ['luci-auth', luci_cmd, '-scopes', ' '.join(LUCI_AUTH_SCOPES)]
182 if wrapped_cmds:
183 cmd += ['--'] + wrapped_cmds
184
Aravind Vasudevaneffdecd2023-01-30 17:02:17 +0000185 return _run_subprocess(cmd, interactive)
Aravind Vasudevanb7d8efd2023-01-27 18:46:40 +0000186
187
Aravind Vasudevan17576772023-01-13 19:50:51 +0000188def _run_subprocess(cmd, interactive=False, env=None):
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000189 """Wrapper to run the given command within a subprocess."""
Aravind Vasudevan17576772023-01-13 19:50:51 +0000190 kwargs = {'shell': IS_WINDOWS}
191
192 if env:
193 kwargs['env'] = dict(os.environ, **env)
194
195 if not interactive:
196 kwargs['stdout'] = subprocess.PIPE
197 kwargs['stderr'] = subprocess.PIPE
198
199 return subprocess.run(cmd, **kwargs)
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000200
201
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000202def _print_subprocess_result(p):
203 """Prints the subprocess result to stdout & stderr."""
204 if p.stdout:
205 sys.stdout.buffer.write(p.stdout)
206
207 if p.stderr:
208 sys.stderr.buffer.write(p.stderr)
209
210
Aravind Vasudevan7b1f98e2023-01-18 17:34:15 +0000211def is_boto_present():
212 """Returns true if the .boto file is present in the default path."""
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000213 return os.getenv('BOTO_CONFIG') or os.getenv(
214 'AWS_CREDENTIAL_FILE') or os.path.isfile(
215 os.path.join(os.path.expanduser('~'), '.boto'))
Aravind Vasudevan7b1f98e2023-01-18 17:34:15 +0000216
217
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000218def run_gsutil(target, args, clean=False):
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000219 # Redirect gsutil config calls to luci-auth.
Aravind Vasudevan9ae55e52023-02-14 22:18:58 +0000220 if 'config' in args:
Aravind Vasudevan17576772023-01-13 19:50:51 +0000221 return luci_login().returncode
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000222
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000223 gsutil_bin = ensure_gsutil(VERSION, target, clean)
224 args_opt = ['-o', 'GSUtil:software_update_check_period=0']
Dan Jacques509776e2017-09-07 18:01:08 -0700225
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000226 if sys.platform == 'darwin':
227 # We are experiencing problems with multiprocessing on MacOS where gsutil.py
228 # may hang.
229 # This behavior is documented in gsutil codebase, and recommendation is to
230 # set GSUtil:parallel_process_count=1.
231 # https://github.com/GoogleCloudPlatform/gsutil/blob/06efc9dc23719fab4fd5fadb506d252bbd3fe0dd/gslib/command.py#L1331
232 # https://github.com/GoogleCloudPlatform/gsutil/issues/1100
233 args_opt.extend(['-o', 'GSUtil:parallel_process_count=1'])
Chris Nardiab816ce2017-10-31 15:45:05 -0400234 if sys.platform == 'cygwin':
235 # This script requires Windows Python, so invoke with depot_tools'
236 # Python.
237 def winpath(path):
Edward Lesmes94d6f482019-11-04 20:55:09 +0000238 stdout = subprocess.check_output(['cygpath', '-w', path])
239 return stdout.strip().decode('utf-8', 'replace')
Chris Nardiab816ce2017-10-31 15:45:05 -0400240 cmd = ['python.bat', winpath(__file__)]
241 cmd.extend(args)
242 sys.exit(subprocess.call(cmd))
243 assert sys.platform != 'cygwin'
244
Dan Jacques509776e2017-09-07 18:01:08 -0700245 cmd = [
Josip Sokcevic19096962022-03-10 17:56:09 +0000246 'vpython3',
247 '-vpython-spec', os.path.join(THIS_DIR, 'gsutil.vpython3'),
Gavin Mak37db69d2022-03-10 00:54:39 +0000248 '--',
Dan Jacques509776e2017-09-07 18:01:08 -0700249 gsutil_bin
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000250 ] + args_opt + args
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000251
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000252 # When .boto is present, try without additional wrappers and handle specific
253 # errors.
254 if is_boto_present():
255 p = _run_subprocess(cmd)
256
257 # Notify user that their .boto file might be outdated.
258 if b'Your credentials are invalid.' in p.stderr:
Bruce Dawson04206432023-03-03 23:05:19 +0000259 # Make sure this error message is visible when invoked by gclient runhooks
260 separator = '*' * 80
Josip Sokcevicb11693a2023-08-19 00:30:41 +0000261 print('\n' + separator + '\n' +
262 'Warning: You might have an outdated .boto file. If this issue '
263 'persists after running `gsutil.py config`, try removing your '
264 '.boto, usually located in your home directory.\n' + separator +
265 '\n',
266 file=sys.stderr)
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000267
268 _print_subprocess_result(p)
269 return p.returncode
270
271 # Skip wrapping commands if luci-auth is already being
272 if _is_luci_context():
Aravind Vasudevan17576772023-01-13 19:50:51 +0000273 return _run_subprocess(cmd, interactive=True).returncode
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000274
Aravind Vasudevan3879bd82023-02-16 23:09:33 +0000275 # Wrap gsutil with luci-auth context.
Aravind Vasudevan17576772023-01-13 19:50:51 +0000276 return luci_context(cmd).returncode
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000277
278
279def parse_args():
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000280 bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
281
Josip Sokcevicc1fd44b2021-09-20 22:31:37 +0000282 # Help is disabled as it conflicts with gsutil -h, which controls headers.
283 parser = argparse.ArgumentParser(add_help=False)
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000284
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000285 parser.add_argument('--clean', action='store_true',
286 help='Clear any existing gsutil package, forcing a new download.')
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000287 parser.add_argument('--target', default=bin_dir,
288 help='The target directory to download/store a gsutil version in. '
289 '(default is %(default)s).')
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000290
291 # These two args exist for backwards-compatibility but are no-ops.
292 parser.add_argument('--force-version', default=VERSION,
293 help='(deprecated, this flag has no effect)')
294 parser.add_argument('--fallback',
295 help='(deprecated, this flag has no effect)')
296
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000297 parser.add_argument('args', nargs=argparse.REMAINDER)
298
hinoka@chromium.orgc13b0542014-12-18 01:06:20 +0000299 args, extras = parser.parse_known_args()
300 if args.args and args.args[0] == '--':
301 args.args.pop(0)
302 if extras:
303 args.args = extras + args.args
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000304 return args
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000305
306
307def main():
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000308 args = parse_args()
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000309 return run_gsutil(args.target, args.args, clean=args.clean)
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000310
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000311
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000312if __name__ == '__main__':
313 sys.exit(main())