blob: 1bbd2c71a73d70e0f75ff87dbd847a4b86df8d27 [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
Raul Tambreb946b232019-03-26 14:48:46 +000021
22try:
23 import urllib2 as urllib
24except ImportError: # For Py3 compatibility
25 import urllib.request as urllib
26
primiano@chromium.orgdf351762014-12-18 11:12:34 +000027import zipfile
hinoka@chromium.org7a790542014-12-10 02:04:39 +000028
29
30GSUTIL_URL = 'https://storage.googleapis.com/pub/'
31API_URL = 'https://www.googleapis.com/storage/v1/b/pub/o/'
32
33THIS_DIR = os.path.dirname(os.path.abspath(__file__))
34DEFAULT_BIN_DIR = os.path.join(THIS_DIR, 'external_bin', 'gsutil')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000035
Dan Jacques509776e2017-09-07 18:01:08 -070036IS_WINDOWS = os.name == 'nt'
37
Josip Sokcevic19096962022-03-10 17:56:09 +000038VERSION = '4.68'
Josip Sokcevicfa474e82021-09-17 16:59:49 +000039
Aravind Vasudevan7af61692023-01-09 23:15:15 +000040# Environment variable to enable LUCI auth feature.
41GSUTIL_ENABLE_LUCI_AUTH = 'GSUTIL_ENABLE_LUCI_AUTH'
42
43# Google OAuth Context required by gsutil.
44LUCI_AUTH_SCOPES = [
45 'https://www.googleapis.com/auth/devstorage.full_control',
46 'https://www.googleapis.com/auth/userinfo.email',
47]
48
Dan Jacques509776e2017-09-07 18:01:08 -070049
hinoka@chromium.org7a790542014-12-10 02:04:39 +000050class InvalidGsutilError(Exception):
51 pass
52
53
hinoka@chromium.org7a790542014-12-10 02:04:39 +000054def download_gsutil(version, target_dir):
55 """Downloads gsutil into the target_dir."""
56 filename = 'gsutil_%s.zip' % version
57 target_filename = os.path.join(target_dir, filename)
58
59 # Check if the target exists already.
60 if os.path.exists(target_filename):
61 md5_calc = hashlib.md5()
62 with open(target_filename, 'rb') as f:
63 while True:
64 buf = f.read(4096)
65 if not buf:
66 break
67 md5_calc.update(buf)
68 local_md5 = md5_calc.hexdigest()
69
70 metadata_url = '%s%s' % (API_URL, filename)
Raul Tambreb946b232019-03-26 14:48:46 +000071 metadata = json.load(urllib.urlopen(metadata_url))
Edward Lemur83aafc92019-11-25 23:25:05 +000072 remote_md5 = base64.b64decode(metadata['md5Hash']).decode('utf-8')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000073
74 if local_md5 == remote_md5:
75 return target_filename
76 os.remove(target_filename)
77
78 # Do the download.
79 url = '%s%s' % (GSUTIL_URL, filename)
Raul Tambreb946b232019-03-26 14:48:46 +000080 u = urllib.urlopen(url)
hinoka@chromium.org7a790542014-12-10 02:04:39 +000081 with open(target_filename, 'wb') as f:
82 while True:
83 buf = u.read(4096)
84 if not buf:
85 break
86 f.write(buf)
87 return target_filename
88
89
dnj@chromium.org605d81d2015-09-18 22:33:53 +000090@contextlib.contextmanager
91def temporary_directory(base):
Takuto Ikuta8daf2442021-10-28 05:51:18 +000092 tmpdir = tempfile.mkdtemp(prefix='t', dir=base)
dnj@chromium.org605d81d2015-09-18 22:33:53 +000093 try:
94 yield tmpdir
95 finally:
96 if os.path.isdir(tmpdir):
97 shutil.rmtree(tmpdir)
98
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000099
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000100def ensure_gsutil(version, target, clean):
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000101 bin_dir = os.path.join(target, 'gsutil_%s' % version)
102 gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
Ryan Tseng83fd81f2017-10-23 11:13:48 -0700103 gsutil_flag = os.path.join(bin_dir, 'gsutil', 'install.flag')
104 # We assume that if gsutil_flag exists, then we have a good version
105 # of the gsutil package.
106 if not clean and os.path.isfile(gsutil_flag):
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000107 # Everything is awesome! we're all done here.
108 return gsutil_bin
109
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000110 if not os.path.exists(target):
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000111 try:
112 os.makedirs(target)
113 except FileExistsError:
114 # Another process is prepping workspace, so let's check if gsutil_bin is
115 # present. If after several checks it's still not, continue with
116 # downloading gsutil.
117 delay = 2 # base delay, in seconds
118 for _ in range(3): # make N attempts
119 # sleep first as it's not expected to have file ready just yet.
120 time.sleep(delay)
121 delay *= 1.5 # next delay increased by that factor
122 if os.path.isfile(gsutil_bin):
123 return gsutil_bin
124
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000125 with temporary_directory(target) as instance_dir:
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000126 # Clean up if we're redownloading a corrupted gsutil.
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000127 cleanup_path = os.path.join(instance_dir, 'clean')
128 try:
129 os.rename(bin_dir, cleanup_path)
130 except (OSError, IOError):
131 cleanup_path = None
132 if cleanup_path:
133 shutil.rmtree(cleanup_path)
134
Takuto Ikuta8daf2442021-10-28 05:51:18 +0000135 download_dir = os.path.join(instance_dir, 'd')
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000136 target_zip_filename = download_gsutil(version, instance_dir)
137 with zipfile.ZipFile(target_zip_filename, 'r') as target_zip:
138 target_zip.extractall(download_dir)
139
Joanna Wang07d6e692022-09-08 01:49:38 +0000140 shutil.move(download_dir, bin_dir)
Ryan Tseng83fd81f2017-10-23 11:13:48 -0700141 # Final check that the gsutil bin exists. This should never fail.
142 if not os.path.isfile(gsutil_bin):
143 raise InvalidGsutilError()
144 # Drop a flag file.
145 with open(gsutil_flag, 'w') as f:
146 f.write('This flag file is dropped by gsutil.py')
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000147
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000148 return gsutil_bin
149
150
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000151def _is_luci_context():
152 """Returns True if the script is run within luci-context"""
153 luci_context_env = os.getenv('LUCI_CONTEXT')
154 if not luci_context_env:
155 return False
156
157 try:
158 with open(luci_context_env) as f:
159 luci_context_json = json.load(f)
160 return 'local_auth' in luci_context_json
161 except (ValueError, FileNotFoundError):
162 return False
163
164
165def luci_context(cmd):
166 """Helper to call`luci-auth context`."""
167 return _luci_auth_cmd('context', wrapped_cmds=cmd)
168
169
170def luci_login():
171 """Helper to run `luci-auth login`."""
Aravind Vasudevan17576772023-01-13 19:50:51 +0000172 # luci-auth requires interactive shell.
173 return _luci_auth_cmd('login', interactive=True)
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000174
175
Aravind Vasudevan17576772023-01-13 19:50:51 +0000176def _luci_auth_cmd(luci_cmd, wrapped_cmds=None, interactive=False):
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000177 """Helper to call luci-auth command."""
Aravind Vasudevan17576772023-01-13 19:50:51 +0000178 print('Using luci-auth login since OOB is deprecated.')
179 print('Override luci-auth by setting `BOTO_CONFIG` in your env.\n')
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000180
181 cmd = ['luci-auth', luci_cmd, '-scopes', ' '.join(LUCI_AUTH_SCOPES)]
182 if wrapped_cmds:
183 cmd += ['--'] + wrapped_cmds
184
Aravind Vasudevan17576772023-01-13 19:50:51 +0000185 if interactive:
Aravind Vasudevana7b20b32023-01-26 18:31:06 +0000186 return _run_subprocess(cmd, interactive=True)
Aravind Vasudevan17576772023-01-13 19:50:51 +0000187
Aravind Vasudevana7b20b32023-01-26 18:31:06 +0000188 p = _run_subprocess(cmd)
Aravind Vasudevan17576772023-01-13 19:50:51 +0000189
190 # If luci-auth is not logged in.
191 if b'Not logged in.' in p.stderr:
192 print('Not logged in.\n')
193 print('Login by running:')
194 print('\t$ gsutil.py config')
195 else:
196 if p.stdout:
197 print(p.stdout.decode('utf-8'))
198
199 if p.stderr:
200 print(p.stderr.decode('utf-8'), file=sys.stderr)
201
202 return p
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000203
204
Aravind Vasudevan17576772023-01-13 19:50:51 +0000205def _run_subprocess(cmd, interactive=False, env=None):
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000206 """Wrapper to run the given command within a subprocess."""
Aravind Vasudevan17576772023-01-13 19:50:51 +0000207 kwargs = {'shell': IS_WINDOWS}
208
209 if env:
210 kwargs['env'] = dict(os.environ, **env)
211
212 if not interactive:
213 kwargs['stdout'] = subprocess.PIPE
214 kwargs['stderr'] = subprocess.PIPE
215
216 return subprocess.run(cmd, **kwargs)
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000217
218
Aravind Vasudevan7b1f98e2023-01-18 17:34:15 +0000219def is_boto_present():
220 """Returns true if the .boto file is present in the default path."""
221 return os.path.isfile(os.path.join(os.path.expanduser('~'), '.boto'))
222
223
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000224def run_gsutil(target, args, clean=False):
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000225 # Redirect gsutil config calls to luci-auth.
Aravind Vasudevana7b20b32023-01-26 18:31:06 +0000226 if os.getenv(GSUTIL_ENABLE_LUCI_AUTH) != '0' and 'config' in args:
Aravind Vasudevan17576772023-01-13 19:50:51 +0000227 return luci_login().returncode
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000228
Josip Sokcevicfa474e82021-09-17 16:59:49 +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
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000232 if sys.platform == 'darwin':
233 # We are experiencing problems with multiprocessing on MacOS where gsutil.py
234 # may hang.
235 # This behavior is documented in gsutil codebase, and recommendation is to
236 # set GSUtil:parallel_process_count=1.
237 # https://github.com/GoogleCloudPlatform/gsutil/blob/06efc9dc23719fab4fd5fadb506d252bbd3fe0dd/gslib/command.py#L1331
238 # https://github.com/GoogleCloudPlatform/gsutil/issues/1100
239 args_opt.extend(['-o', 'GSUtil:parallel_process_count=1'])
Chris Nardiab816ce2017-10-31 15:45:05 -0400240 if sys.platform == 'cygwin':
241 # This script requires Windows Python, so invoke with depot_tools'
242 # Python.
243 def winpath(path):
Edward Lesmes94d6f482019-11-04 20:55:09 +0000244 stdout = subprocess.check_output(['cygpath', '-w', path])
245 return stdout.strip().decode('utf-8', 'replace')
Chris Nardiab816ce2017-10-31 15:45:05 -0400246 cmd = ['python.bat', winpath(__file__)]
247 cmd.extend(args)
248 sys.exit(subprocess.call(cmd))
249 assert sys.platform != 'cygwin'
250
Dan Jacques509776e2017-09-07 18:01:08 -0700251 cmd = [
Josip Sokcevic19096962022-03-10 17:56:09 +0000252 'vpython3',
253 '-vpython-spec', os.path.join(THIS_DIR, 'gsutil.vpython3'),
Gavin Mak37db69d2022-03-10 00:54:39 +0000254 '--',
Dan Jacques509776e2017-09-07 18:01:08 -0700255 gsutil_bin
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000256 ] + args_opt + args
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000257
258 # Bypass luci-auth when run within a bot or .boto file is set.
Aravind Vasudevana7b20b32023-01-26 18:31:06 +0000259 if (os.getenv(GSUTIL_ENABLE_LUCI_AUTH) == '0' or _is_luci_context()
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000260 or os.getenv('SWARMING_HEADLESS') == '1' or os.getenv('BOTO_CONFIG')
Aravind Vasudevan7b1f98e2023-01-18 17:34:15 +0000261 or os.getenv('AWS_CREDENTIAL_FILE') or is_boto_present()):
Aravind Vasudevan17576772023-01-13 19:50:51 +0000262 return _run_subprocess(cmd, interactive=True).returncode
Aravind Vasudevan7af61692023-01-09 23:15:15 +0000263
Aravind Vasudevan17576772023-01-13 19:50:51 +0000264 return luci_context(cmd).returncode
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000265
266
267def parse_args():
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000268 bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
269
Josip Sokcevicc1fd44b2021-09-20 22:31:37 +0000270 # Help is disabled as it conflicts with gsutil -h, which controls headers.
271 parser = argparse.ArgumentParser(add_help=False)
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000272
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000273 parser.add_argument('--clean', action='store_true',
274 help='Clear any existing gsutil package, forcing a new download.')
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000275 parser.add_argument('--target', default=bin_dir,
276 help='The target directory to download/store a gsutil version in. '
277 '(default is %(default)s).')
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000278
279 # These two args exist for backwards-compatibility but are no-ops.
280 parser.add_argument('--force-version', default=VERSION,
281 help='(deprecated, this flag has no effect)')
282 parser.add_argument('--fallback',
283 help='(deprecated, this flag has no effect)')
284
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000285 parser.add_argument('args', nargs=argparse.REMAINDER)
286
hinoka@chromium.orgc13b0542014-12-18 01:06:20 +0000287 args, extras = parser.parse_known_args()
288 if args.args and args.args[0] == '--':
289 args.args.pop(0)
290 if extras:
291 args.args = extras + args.args
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000292 return args
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000293
294
295def main():
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000296 args = parse_args()
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000297 return run_gsutil(args.target, args.args, clean=args.clean)
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000298
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000299
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000300if __name__ == '__main__':
301 sys.exit(main())