blob: 54d77ffb664de304e66957f1e406f3ec92d60b4d [file] [log] [blame]
nodirbe642ff2016-06-09 15:51:51 -07001# Copyright 2016 The LUCI Authors. All rights reserved.
2# Use of this source code is governed under the Apache License, Version 2.0
3# that can be found in the LICENSE file.
4
5"""Fetches CIPD client and installs packages."""
6
nodirbe642ff2016-06-09 15:51:51 -07007import contextlib
8import hashlib
iannucci96fcccc2016-08-30 15:52:22 -07009import json
nodirbe642ff2016-06-09 15:51:51 -070010import logging
11import optparse
12import os
13import platform
Takuto Ikuta62cdb322021-11-17 00:47:55 +000014import re
15import shutil
nodirbe642ff2016-06-09 15:51:51 -070016import sys
17import tempfile
18import time
Junji Watanabe7a677e92022-01-13 06:07:31 +000019import urllib.parse
nodirbe642ff2016-06-09 15:51:51 -070020
21from utils import file_path
22from utils import fs
23from utils import net
24from utils import subprocess42
25from utils import tools
Marc-Antoine Ruel34f5f282018-05-16 16:04:31 -040026
Justin Luong97eda6f2022-08-23 01:29:16 +000027import errors
Marc-Antoine Ruel34f5f282018-05-16 16:04:31 -040028import local_caching
nodirbe642ff2016-06-09 15:51:51 -070029
30
31# .exe on Windows.
32EXECUTABLE_SUFFIX = '.exe' if sys.platform == 'win32' else ''
33
Junji Watanabe4b890ef2020-09-16 01:43:27 +000034_DEFAULT_CIPD_SERVER = 'https://chrome-infra-packages.appspot.com'
35
36_DEFAULT_CIPD_CLIENT_PACKAGE = 'infra/tools/cipd/${platform}'
37
38_DEFAULT_CIPD_CLIENT_VERSION = 'latest'
nodirbe642ff2016-06-09 15:51:51 -070039
iannucci4d7792a2017-03-10 10:30:56 -080040if sys.platform == 'win32':
Junji Watanabe38b28b02020-04-23 10:23:30 +000041
iannucci4d7792a2017-03-10 10:30:56 -080042 def _ensure_batfile(client_path):
43 base, _ = os.path.splitext(client_path)
Junji Watanabe38b28b02020-04-23 10:23:30 +000044 with open(base + ".bat", 'w') as f:
iannucci4d7792a2017-03-10 10:30:56 -080045 f.write('\n'.join([ # python turns \n into CRLF
Junji Watanabe38b28b02020-04-23 10:23:30 +000046 '@set CIPD="%~dp0cipd.exe"', '@shift', '@%CIPD% %*'
iannucci4d7792a2017-03-10 10:30:56 -080047 ]))
48else:
49 def _ensure_batfile(_client_path):
50 pass
51
52
nodirbe642ff2016-06-09 15:51:51 -070053class Error(Exception):
54 """Raised on CIPD errors."""
55
56
57def add_cipd_options(parser):
58 group = optparse.OptionGroup(parser, 'CIPD')
59 group.add_option(
vadimsh902948e2017-01-20 15:57:32 -080060 '--cipd-enabled',
Ye Kuangfff1e502020-07-13 13:21:57 +000061 help='Enable CIPD client bootstrap. Implied by --cipd-package. Cannot '
62 'turn this off while specifying --cipd-package',
63 default=True)
vadimsh902948e2017-01-20 15:57:32 -080064 group.add_option(
nodirbe642ff2016-06-09 15:51:51 -070065 '--cipd-server',
vadimsh902948e2017-01-20 15:57:32 -080066 help='URL of the CIPD server. '
Ye Kuang0a128a82020-06-26 08:57:31 +000067 'Only relevant with --cipd-enabled or --cipd-package.',
Junji Watanabe4b890ef2020-09-16 01:43:27 +000068 default=_DEFAULT_CIPD_SERVER)
nodirbe642ff2016-06-09 15:51:51 -070069 group.add_option(
70 '--cipd-client-package',
nodir90bc8dc2016-06-15 13:35:21 -070071 help='Package name of CIPD client with optional parameters described in '
Junji Watanabe38b28b02020-04-23 10:23:30 +000072 '--cipd-package help. '
73 'Only relevant with --cipd-enabled or --cipd-package. '
74 'Default: "%default"',
Junji Watanabe4b890ef2020-09-16 01:43:27 +000075 default=_DEFAULT_CIPD_CLIENT_PACKAGE)
nodirbe642ff2016-06-09 15:51:51 -070076 group.add_option(
nodir90bc8dc2016-06-15 13:35:21 -070077 '--cipd-client-version',
78 help='Version of CIPD client. '
Junji Watanabe38b28b02020-04-23 10:23:30 +000079 'Only relevant with --cipd-enabled or --cipd-package. '
80 'Default: "%default"',
Junji Watanabe4b890ef2020-09-16 01:43:27 +000081 default=_DEFAULT_CIPD_CLIENT_VERSION)
nodir90bc8dc2016-06-15 13:35:21 -070082 group.add_option(
nodirff531b42016-06-23 13:05:06 -070083 '--cipd-package',
84 dest='cipd_packages',
85 help='A CIPD package to install. '
Junji Watanabe38b28b02020-04-23 10:23:30 +000086 'Format is "<path>:<package_name>:<version>". '
87 '"path" is installation directory relative to run_dir, '
88 'defaults to ".". '
89 '"package_name" may have ${platform} parameter: it will be '
90 'expanded to "<os>-<architecture>". '
91 'The option can be specified multiple times.',
nodirff531b42016-06-23 13:05:06 -070092 action='append',
93 default=[])
nodirbe642ff2016-06-09 15:51:51 -070094 group.add_option(
95 '--cipd-cache',
96 help='CIPD cache directory, separate from isolate cache. '
Junji Watanabe38b28b02020-04-23 10:23:30 +000097 'Only relevant with --cipd-enabled or --cipd-package. '
Jonah Hoopera404c6e2022-12-09 20:57:37 +000098 'Default: "%default". Is only actually set in unit tests. '
99 '`run_isolated.py` sets this as $bot_dir/cipd_cache if `--cipd-enabled` '
100 'is set. ',
nodirbe642ff2016-06-09 15:51:51 -0700101 default='')
102 parser.add_option_group(group)
103
104
105def validate_cipd_options(parser, options):
106 """Calls parser.error on first found error among cipd options."""
vadimsh902948e2017-01-20 15:57:32 -0800107 if not options.cipd_enabled:
Ye Kuangfff1e502020-07-13 13:21:57 +0000108 if options.cipd_packages:
109 parser.error('Cannot install CIPD packages when --cipd-enable=false')
nodirbe642ff2016-06-09 15:51:51 -0700110 return
nodirff531b42016-06-23 13:05:06 -0700111
112 for pkg in options.cipd_packages:
113 parts = pkg.split(':', 2)
114 if len(parts) != 3:
115 parser.error('invalid package "%s": must have at least 2 colons' % pkg)
116 _path, name, version = parts
117 if not name:
118 parser.error('invalid package "%s": package name is not specified' % pkg)
119 if not version:
120 parser.error('invalid package "%s": version is not specified' % pkg)
121
nodirbe642ff2016-06-09 15:51:51 -0700122 if not options.cipd_server:
vadimsh902948e2017-01-20 15:57:32 -0800123 parser.error('cipd is enabled, --cipd-server is required')
nodirbe642ff2016-06-09 15:51:51 -0700124
125 if not options.cipd_client_package:
nodirbe642ff2016-06-09 15:51:51 -0700126 parser.error(
vadimsh902948e2017-01-20 15:57:32 -0800127 'cipd is enabled, --cipd-client-package is required')
nodir90bc8dc2016-06-15 13:35:21 -0700128 if not options.cipd_client_version:
129 parser.error(
vadimsh902948e2017-01-20 15:57:32 -0800130 'cipd is enabled, --cipd-client-version is required')
nodirbe642ff2016-06-09 15:51:51 -0700131
132
Junji Watanabeab2102a2022-01-12 01:44:04 +0000133class CipdClient:
nodirbe642ff2016-06-09 15:51:51 -0700134 """Installs packages."""
135
iannucci96fcccc2016-08-30 15:52:22 -0700136 def __init__(self, binary_path, package_name, instance_id, service_url):
nodirbe642ff2016-06-09 15:51:51 -0700137 """Initializes CipdClient.
138
139 Args:
140 binary_path (str): path to the CIPD client binary.
iannucci96fcccc2016-08-30 15:52:22 -0700141 package_name (str): the CIPD package name for the client itself.
142 instance_id (str): the CIPD instance_id for the client itself.
nodirbe642ff2016-06-09 15:51:51 -0700143 service_url (str): if not None, URL of the CIPD backend that overrides
144 the default one.
145 """
146 self.binary_path = binary_path
iannucci96fcccc2016-08-30 15:52:22 -0700147 self.package_name = package_name
148 self.instance_id = instance_id
nodirbe642ff2016-06-09 15:51:51 -0700149 self.service_url = service_url
150
Junji Watanabe38b28b02020-04-23 10:23:30 +0000151 def ensure(self,
152 site_root,
153 packages,
154 cache_dir=None,
155 tmp_dir=None,
156 timeout=None):
nodirbe642ff2016-06-09 15:51:51 -0700157 """Ensures that packages installed in |site_root| equals |packages| set.
158
159 Blocking call.
160
161 Args:
162 site_root (str): where to install packages.
iannuccib58d10d2017-03-18 02:00:25 -0700163 packages: dict of subdir -> list of (package_template, version) tuples.
nodirbe642ff2016-06-09 15:51:51 -0700164 cache_dir (str): if set, cache dir for cipd binary own cache.
165 Typically contains packages and tags.
166 tmp_dir (str): if not None, dir for temp files.
167 timeout (int): if not None, timeout in seconds for this function to run.
168
iannucci96fcccc2016-08-30 15:52:22 -0700169 Returns:
iannuccib58d10d2017-03-18 02:00:25 -0700170 Pinned packages in the form of {subdir: [(package_name, package_id)]},
171 which correspond 1:1 with the input packages argument.
iannucci96fcccc2016-08-30 15:52:22 -0700172
nodirbe642ff2016-06-09 15:51:51 -0700173 Raises:
174 Error if could not install packages or timed out.
175 """
176 timeoutfn = tools.sliding_timeout(timeout)
177 logging.info('Installing packages %r into %s', packages, site_root)
178
iannuccib58d10d2017-03-18 02:00:25 -0700179 ensure_file_handle, ensure_file_path = tempfile.mkstemp(
Junji Watanabe53d31882022-01-13 07:58:00 +0000180 dir=tmp_dir, prefix='cipd-ensure-file-', suffix='.txt')
iannucci96fcccc2016-08-30 15:52:22 -0700181 json_out_file_handle, json_file_path = tempfile.mkstemp(
Junji Watanabe53d31882022-01-13 07:58:00 +0000182 dir=tmp_dir, prefix='cipd-ensure-result-', suffix='.json')
iannucci96fcccc2016-08-30 15:52:22 -0700183 os.close(json_out_file_handle)
184
nodirbe642ff2016-06-09 15:51:51 -0700185 try:
186 try:
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000187 for subdir, pkgs in sorted(packages.items()):
iannuccib58d10d2017-03-18 02:00:25 -0700188 if '\n' in subdir:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000189 raise Error('Could not install packages; subdir %r contains newline'
190 % subdir)
tikutaddc3ccb2020-07-07 12:36:39 +0000191 os.write(ensure_file_handle, ('@Subdir %s\n' % (subdir,)).encode())
iannuccib58d10d2017-03-18 02:00:25 -0700192 for pkg, version in pkgs:
tikutaddc3ccb2020-07-07 12:36:39 +0000193 os.write(ensure_file_handle, ('%s %s\n' % (pkg, version)).encode())
nodirbe642ff2016-06-09 15:51:51 -0700194 finally:
iannuccib58d10d2017-03-18 02:00:25 -0700195 os.close(ensure_file_handle)
nodirbe642ff2016-06-09 15:51:51 -0700196
197 cmd = [
Junji Watanabe38b28b02020-04-23 10:23:30 +0000198 self.binary_path,
199 'ensure',
200 '-root',
201 site_root,
202 '-ensure-file',
203 ensure_file_path,
204 '-verbose', # this is safe because cipd-ensure does not print a lot
205 '-json-output',
206 json_file_path,
nodirbe642ff2016-06-09 15:51:51 -0700207 ]
208 if cache_dir:
209 cmd += ['-cache-dir', cache_dir]
210 if self.service_url:
211 cmd += ['-service-url', self.service_url]
212
213 logging.debug('Running %r', cmd)
Junji Watanabe88647c62021-05-11 03:41:10 +0000214 kwargs = {}
Junji Watanabe7a677e92022-01-13 06:07:31 +0000215 kwargs['encoding'] = 'utf-8'
216 kwargs['errors'] = 'backslashreplace'
nodirbe642ff2016-06-09 15:51:51 -0700217 process = subprocess42.Popen(
Junji Watanabebc5a7b62021-04-23 08:40:11 +0000218 cmd,
219 stdout=subprocess42.PIPE,
220 stderr=subprocess42.PIPE,
Junji Watanabe88647c62021-05-11 03:41:10 +0000221 universal_newlines=True,
222 **kwargs)
nodirbe642ff2016-06-09 15:51:51 -0700223 output = []
224 for pipe_name, line in process.yield_any_line(timeout=0.1):
225 to = timeoutfn()
226 if to is not None and to <= 0:
227 raise Error(
228 'Could not install packages; took more than %d seconds' % timeout)
229 if not pipe_name:
230 # stdout or stderr was closed, but yield_any_line still may have
231 # something to yield.
232 continue
233 output.append(line)
234 if pipe_name == 'stderr':
235 logging.debug('cipd client: %s', line)
236 else:
237 logging.info('cipd client: %s', line)
238
239 exit_code = process.wait(timeout=timeoutfn())
Justin Luong97eda6f2022-08-23 01:29:16 +0000240
241 ensure_result = {}
242 if os.path.exists(json_file_path):
243 with open(json_file_path) as jfile:
244 result_json = json.load(jfile)
245 ensure_result = result_json['result']
246 status = result_json.get('error_code')
247 if status in ('auth_error', 'bad_argument_error',
248 'invalid_version_error', 'stale_error',
249 'hash_mismatch_error'):
250 details = result_json.get('error_details')
251 cipd_package = cipd_version = cipd_subdir = None
252 if details:
253 cipd_package = details.get('package')
254 cipd_version = details.get('version')
255 cipd_subdir = details.get('subdir')
256 raise errors.NonRecoverableCipdException(status, cipd_package,
257 cipd_subdir, cipd_version)
258
nodirbe642ff2016-06-09 15:51:51 -0700259 if exit_code != 0:
260 raise Error(
261 'Could not install packages; exit code %d\noutput:%s' % (
262 exit_code, '\n'.join(output)))
iannuccib58d10d2017-03-18 02:00:25 -0700263 return {
Justin Luong97eda6f2022-08-23 01:29:16 +0000264 subdir: [(x['package'], x['instance_id']) for x in pins]
265 for subdir, pins in ensure_result.items()
iannuccib58d10d2017-03-18 02:00:25 -0700266 }
nodirbe642ff2016-06-09 15:51:51 -0700267 finally:
iannuccib58d10d2017-03-18 02:00:25 -0700268 fs.remove(ensure_file_path)
iannucci96fcccc2016-08-30 15:52:22 -0700269 fs.remove(json_file_path)
nodirbe642ff2016-06-09 15:51:51 -0700270
271
272def get_platform():
273 """Returns ${platform} parameter value.
274
Vadim Shtayura8f863a42017-10-18 19:23:15 -0700275 The logic is similar to
276 https://chromium.googlesource.com/chromium/tools/build/+/6c5c7e9c/scripts/slave/infra_platform.py
nodirbe642ff2016-06-09 15:51:51 -0700277 """
278 # linux, mac or windows.
Vadim Shtayura8f863a42017-10-18 19:23:15 -0700279 os_name = {
Takuto Ikuta41504032019-10-29 12:23:57 +0000280 'darwin': 'mac',
Takuto Ikuta41504032019-10-29 12:23:57 +0000281 'linux': 'linux',
282 'win32': 'windows',
nodirbe642ff2016-06-09 15:51:51 -0700283 }.get(sys.platform)
Vadim Shtayura8f863a42017-10-18 19:23:15 -0700284 if not os_name:
nodirbe642ff2016-06-09 15:51:51 -0700285 raise Error('Unknown OS: %s' % sys.platform)
286
Vadim Shtayura8f863a42017-10-18 19:23:15 -0700287 # Normalize machine architecture. Some architectures are identical or
288 # compatible with others. We collapse them into one.
289 arch = platform.machine().lower()
Junji Watanabe7b0cb6c2021-10-28 08:42:31 +0000290 if arch in ('arm64', 'aarch64'):
Vadim Shtayura8f863a42017-10-18 19:23:15 -0700291 arch = 'arm64'
292 elif arch.startswith('armv') and arch.endswith('l'):
293 # 32-bit ARM: Standardize on ARM v6 baseline.
294 arch = 'armv6l'
295 elif arch in ('amd64', 'x86_64'):
296 arch = 'amd64'
Vadim Shtayura5059a052017-10-19 13:04:50 -0700297 elif arch in ('i386', 'i686', 'x86'):
Vadim Shtayura8f863a42017-10-18 19:23:15 -0700298 arch = '386'
Junji Watanabe41a770c2021-07-26 23:44:54 +0000299 elif not arch and os_name == 'windows':
300 # On some 32bit Windows7, platform.machine() returns None.
301 # Fallback to 386 in that case.
302 logging.warning('platform.machine() returns None. '
303 'Use \'386\' as CPU architecture.')
304 arch = '386'
nodirbe642ff2016-06-09 15:51:51 -0700305
Vadim Shtayura8f863a42017-10-18 19:23:15 -0700306 # If using a 32-bit python on x86_64 kernel on Linux, "downgrade" the arch to
307 # 32-bit too (this is the bitness of the userland).
308 python_bits = 64 if sys.maxsize > 2**32 else 32
309 if os_name == 'linux' and arch == 'amd64' and python_bits == 32:
310 arch = '386'
nodirbe642ff2016-06-09 15:51:51 -0700311
Vadim Shtayura8f863a42017-10-18 19:23:15 -0700312 return '%s-%s' % (os_name, arch)
nodirbe642ff2016-06-09 15:51:51 -0700313
314
nodirbe642ff2016-06-09 15:51:51 -0700315def _check_response(res, fmt, *args):
316 """Raises Error if response is bad."""
317 if not res:
318 raise Error('%s: no response' % (fmt % args))
319
320 if res.get('status') != 'SUCCESS':
Junji Watanabe38b28b02020-04-23 10:23:30 +0000321 raise Error('%s: %s' % (fmt % args, res.get('error_message') or
322 'status is %s' % res.get('status')))
nodirbe642ff2016-06-09 15:51:51 -0700323
324
325def resolve_version(cipd_server, package_name, version, timeout=None):
326 """Resolves a package instance version (e.g. a tag) to an instance id."""
327 url = '%s/_ah/api/repo/v1/instance/resolve?%s' % (
328 cipd_server,
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000329 urllib.parse.urlencode({
330 'package_name': package_name,
331 'version': version,
nodirbe642ff2016-06-09 15:51:51 -0700332 }))
333 res = net.url_read_json(url, timeout=timeout)
334 _check_response(res, 'Could not resolve version %s:%s', package_name, version)
335 instance_id = res.get('instance_id')
336 if not instance_id:
337 raise Error('Invalid resolveVersion response: no instance id')
338 return instance_id
339
340
341def get_client_fetch_url(service_url, package_name, instance_id, timeout=None):
342 """Returns a fetch URL of CIPD client binary contents.
343
344 Raises:
345 Error if cannot retrieve fetch URL.
346 """
347 # Fetch the URL of the binary from CIPD backend.
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000348 url = '%s/_ah/api/repo/v1/client?%s' % (service_url,
349 urllib.parse.urlencode({
350 'package_name': package_name,
351 'instance_id': instance_id,
352 }))
nodirbe642ff2016-06-09 15:51:51 -0700353 res = net.url_read_json(url, timeout=timeout)
Junji Watanabe38b28b02020-04-23 10:23:30 +0000354 _check_response(res, 'Could not fetch CIPD client %s:%s', package_name,
355 instance_id)
nodirbe642ff2016-06-09 15:51:51 -0700356 fetch_url = res.get('client_binary', {}).get('fetch_url')
357 if not fetch_url:
358 raise Error('Invalid fetchClientBinary response: no fetch_url')
359 return fetch_url
360
361
362def _fetch_cipd_client(disk_cache, instance_id, fetch_url, timeoutfn):
363 """Fetches cipd binary to |disk_cache|.
364
365 Retries requests with exponential back-off.
366
367 Raises:
368 Error if could not fetch content.
369 """
370 sleep_time = 1
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000371 for attempt in range(5):
nodirbe642ff2016-06-09 15:51:51 -0700372 if attempt > 0:
373 if timeoutfn() is not None and timeoutfn() < sleep_time:
374 raise Error('Could not fetch CIPD client: timeout')
375 logging.warning('Will retry to fetch CIPD client in %ds', sleep_time)
376 time.sleep(sleep_time)
377 sleep_time *= 2
378
379 try:
380 res = net.url_open(fetch_url, timeout=timeoutfn())
381 if res:
382 disk_cache.write(instance_id, res.iter_content(64 * 1024))
383 return
384 except net.TimeoutError as ex:
Junji Watanabeab2102a2022-01-12 01:44:04 +0000385 raise Error('Could not fetch CIPD client: %s' % ex)
nodirbe642ff2016-06-09 15:51:51 -0700386 except net.NetError as ex:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000387 logging.warning('Could not fetch CIPD client on attempt #%d: %s',
388 attempt + 1, ex)
nodirbe642ff2016-06-09 15:51:51 -0700389
390 raise Error('Could not fetch CIPD client after 5 retries')
391
392
Takuto Ikuta62cdb322021-11-17 00:47:55 +0000393def _is_valid_hash(value):
394 """Returns if the value is a valid hash for the corresponding algorithm."""
395 size = 2 * hashlib.sha1().digest_size
396 return bool(re.match(r'^[a-fA-F0-9]{%d}$' % size, value))
397
398
nodirbe642ff2016-06-09 15:51:51 -0700399@contextlib.contextmanager
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000400def get_client(cache_dir,
401 service_url=_DEFAULT_CIPD_SERVER,
402 package_template=_DEFAULT_CIPD_CLIENT_PACKAGE,
403 version=_DEFAULT_CIPD_CLIENT_VERSION,
404 timeout=None):
nodirbe642ff2016-06-09 15:51:51 -0700405 """Returns a context manager that yields a CipdClient. A blocking call.
406
vadimsh232f5a82017-01-20 19:23:44 -0800407 Upon exit from the context manager, the client binary may be deleted
408 (if the internal cache is full).
409
nodirbe642ff2016-06-09 15:51:51 -0700410 Args:
vadimsh232f5a82017-01-20 19:23:44 -0800411 service_url (str): URL of the CIPD backend.
Marc-Antoine Ruel6f348a22017-12-06 12:47:37 -0500412 package_template (str): package name template of the CIPD client.
vadimsh232f5a82017-01-20 19:23:44 -0800413 version (str): version of CIPD client package.
414 cache_dir: directory to store instance cache, version cache
415 and a hardlink to the client binary.
416 timeout (int): if not None, timeout in seconds for this function.
nodirbe642ff2016-06-09 15:51:51 -0700417
418 Yields:
419 CipdClient.
420
421 Raises:
422 Error if CIPD client version cannot be resolved or client cannot be fetched.
423 """
424 timeoutfn = tools.sliding_timeout(timeout)
425
Marc-Antoine Ruel6f348a22017-12-06 12:47:37 -0500426 # Package names are always lower case.
427 # TODO(maruel): Assert instead?
428 package_name = package_template.lower().replace('${platform}', get_platform())
nodirbe642ff2016-06-09 15:51:51 -0700429
430 # Resolve version to instance id.
431 # Is it an instance id already? They look like HEX SHA1.
Takuto Ikuta62cdb322021-11-17 00:47:55 +0000432 if _is_valid_hash(version):
nodirbe642ff2016-06-09 15:51:51 -0700433 instance_id = version
Vadim Shtayuraaaecc1c2017-10-19 11:36:42 -0700434 elif ':' in version: # it's an immutable tag, cache the resolved version
435 # version_cache is {hash(package_name, tag) -> instance id} mapping.
nodirbe642ff2016-06-09 15:51:51 -0700436 # It does not take a lot of disk space.
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400437 version_cache = local_caching.DiskContentAddressedCache(
Junji Watanabe7a677e92022-01-13 06:07:31 +0000438 os.path.join(cache_dir, 'versions'),
Marc-Antoine Ruel34f5f282018-05-16 16:04:31 -0400439 local_caching.CachePolicies(
Marc-Antoine Ruel77d93782018-05-24 16:13:55 -0400440 # 1GiB.
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000441 max_cache_size=1024 * 1024 * 1024,
Marc-Antoine Ruel34f5f282018-05-16 16:04:31 -0400442 min_free_space=0,
443 max_items=300,
444 # 3 weeks.
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000445 max_age_secs=21 * 24 * 60 * 60),
maruele6fc9382017-05-04 09:03:48 -0700446 trim=True)
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000447 # Convert (package_name, version) to a string that may be used as a
448 # filename in disk cache by hashing it.
Takuto Ikuta922c8642021-11-18 07:42:16 +0000449 version_digest = hashlib.sha256(
Junji Watanabe7a677e92022-01-13 06:07:31 +0000450 ('%s\n%s' % (package_name, version)).encode()).hexdigest()
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000451 try:
452 with version_cache.getfileobj(version_digest) as f:
Junji Watanabe7a677e92022-01-13 06:07:31 +0000453 instance_id = f.read().decode()
Takuto Ikutab70dd7f2021-09-06 09:42:53 +0000454 logging.info("instance_id %s", instance_id)
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000455 except local_caching.CacheMiss:
Takuto Ikutab70dd7f2021-09-06 09:42:53 +0000456 logging.info("version_cache miss for %s", version_digest)
457 instance_id = ''
458
459 if not instance_id:
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000460 instance_id = resolve_version(
461 service_url, package_name, version, timeout=timeoutfn())
Junji Watanabe7a677e92022-01-13 06:07:31 +0000462 version_cache.write(version_digest, [instance_id.encode()])
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000463 version_cache.trim()
Vadim Shtayuraaaecc1c2017-10-19 11:36:42 -0700464 else: # it's a ref, hit the backend
iannucci6fd57d22016-08-30 17:02:20 -0700465 instance_id = resolve_version(
466 service_url, package_name, version, timeout=timeoutfn())
nodirbe642ff2016-06-09 15:51:51 -0700467
468 # instance_cache is {instance_id -> client binary} mapping.
469 # It is bounded by 5 client versions.
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400470 instance_cache = local_caching.DiskContentAddressedCache(
Junji Watanabe7a677e92022-01-13 06:07:31 +0000471 os.path.join(cache_dir, 'clients'),
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000472 local_caching.CachePolicies(
473 # 1GiB.
474 max_cache_size=1024 * 1024 * 1024,
475 min_free_space=0,
476 max_items=10,
477 # 3 weeks.
478 max_age_secs=21 * 24 * 60 * 60),
maruele6fc9382017-05-04 09:03:48 -0700479 trim=True)
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000480 if instance_id not in instance_cache:
481 logging.info('Fetching CIPD client %s:%s', package_name, instance_id)
482 fetch_url = get_client_fetch_url(
483 service_url, package_name, instance_id, timeout=timeoutfn())
484 _fetch_cipd_client(instance_cache, instance_id, fetch_url, timeoutfn)
nodirbe642ff2016-06-09 15:51:51 -0700485
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000486 # A single host can run multiple swarming bots, but they cannot share same
487 # root bot directory. Thus, it is safe to use the same name for the binary.
Junji Watanabe7a677e92022-01-13 06:07:31 +0000488 cipd_bin_dir = os.path.join(cache_dir, 'bin')
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000489 binary_path = os.path.join(cipd_bin_dir, 'cipd' + EXECUTABLE_SUFFIX)
490 if fs.isfile(binary_path):
491 # TODO(maruel): Do not unconditionally remove the binary.
Takuto Ikuta296ed052019-11-29 01:47:20 +0000492 try:
493 file_path.remove(binary_path)
494 except WindowsError: # pylint: disable=undefined-variable
495 # See whether cipd.exe is running for crbug.com/1028781
496 ret = subprocess42.call(['tasklist.exe'])
497 if ret:
498 logging.error('tasklist returns non-zero: %d', ret)
499 raise
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000500 else:
501 file_path.ensure_tree(cipd_bin_dir)
tansell9e04a8d2016-07-28 09:31:59 -0700502
Takuto Ikuta62cdb322021-11-17 00:47:55 +0000503 with instance_cache.getfileobj(instance_id) as f, fs.open(binary_path,
504 'wb') as dest:
505 shutil.copyfileobj(f, dest)
506 fs.chmod(binary_path, 0o511) # -r-x--x--x
nodirbe642ff2016-06-09 15:51:51 -0700507
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000508 _ensure_batfile(binary_path)
iannucci4d7792a2017-03-10 10:30:56 -0800509
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +0000510 yield CipdClient(
511 binary_path,
512 package_name=package_name,
513 instance_id=instance_id,
514 service_url=service_url)
515 instance_cache.trim()
nodir90bc8dc2016-06-15 13:35:21 -0700516
517
nodirff531b42016-06-23 13:05:06 -0700518def parse_package_args(packages):
519 """Parses --cipd-package arguments.
nodir90bc8dc2016-06-15 13:35:21 -0700520
nodirff531b42016-06-23 13:05:06 -0700521 Assumes |packages| were validated by validate_cipd_options.
522
523 Returns:
iannucci96fcccc2016-08-30 15:52:22 -0700524 A list of [(path, package_name, version), ...]
nodir90bc8dc2016-06-15 13:35:21 -0700525 """
iannucci96fcccc2016-08-30 15:52:22 -0700526 result = []
nodirff531b42016-06-23 13:05:06 -0700527 for pkg in packages:
528 path, name, version = pkg.split(':', 2)
nodir90bc8dc2016-06-15 13:35:21 -0700529 if not name:
nodirff531b42016-06-23 13:05:06 -0700530 raise Error('Invalid package "%s": package name is not specified' % pkg)
nodir90bc8dc2016-06-15 13:35:21 -0700531 if not version:
nodirff531b42016-06-23 13:05:06 -0700532 raise Error('Invalid package "%s": version is not specified' % pkg)
iannucci96fcccc2016-08-30 15:52:22 -0700533 result.append((path, name, version))
nodirff531b42016-06-23 13:05:06 -0700534 return result