nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 1 | # 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 | |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 7 | import contextlib |
| 8 | import hashlib |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 9 | import json |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 10 | import logging |
| 11 | import optparse |
| 12 | import os |
| 13 | import platform |
| 14 | import sys |
| 15 | import tempfile |
| 16 | import time |
| 17 | import urllib |
| 18 | |
| 19 | from utils import file_path |
| 20 | from utils import fs |
| 21 | from utils import net |
| 22 | from utils import subprocess42 |
| 23 | from utils import tools |
| 24 | import isolated_format |
| 25 | import isolateserver |
| 26 | |
| 27 | |
| 28 | # .exe on Windows. |
| 29 | EXECUTABLE_SUFFIX = '.exe' if sys.platform == 'win32' else '' |
| 30 | |
| 31 | |
iannucci | 4d7792a | 2017-03-10 10:30:56 -0800 | [diff] [blame^] | 32 | if sys.platform == 'win32': |
| 33 | def _ensure_batfile(client_path): |
| 34 | base, _ = os.path.splitext(client_path) |
| 35 | with open(base+".bat", 'w') as f: |
| 36 | f.write('\n'.join([ # python turns \n into CRLF |
| 37 | '@set CIPD="%~dp0cipd.exe"', |
| 38 | '@shift', |
| 39 | '@%CIPD% %*' |
| 40 | ])) |
| 41 | else: |
| 42 | def _ensure_batfile(_client_path): |
| 43 | pass |
| 44 | |
| 45 | |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 46 | class Error(Exception): |
| 47 | """Raised on CIPD errors.""" |
| 48 | |
| 49 | |
| 50 | def add_cipd_options(parser): |
| 51 | group = optparse.OptionGroup(parser, 'CIPD') |
| 52 | group.add_option( |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 53 | '--cipd-enabled', |
| 54 | help='Enable CIPD client bootstrap. Implied by --cipd-package.', |
| 55 | action='store_true', |
| 56 | default=False) |
| 57 | group.add_option( |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 58 | '--cipd-server', |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 59 | help='URL of the CIPD server. ' |
| 60 | 'Only relevant with --cipd-enabled or --cipd-package.') |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 61 | group.add_option( |
| 62 | '--cipd-client-package', |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 63 | help='Package name of CIPD client with optional parameters described in ' |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 64 | '--cipd-package help. ' |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 65 | 'Only relevant with --cipd-enabled or --cipd-package. ' |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 66 | 'Default: "%default"', |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 67 | default='infra/tools/cipd/${platform}') |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 68 | group.add_option( |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 69 | '--cipd-client-version', |
| 70 | help='Version of CIPD client. ' |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 71 | 'Only relevant with --cipd-enabled or --cipd-package. ' |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 72 | 'Default: "%default"', |
| 73 | default='latest') |
| 74 | group.add_option( |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 75 | '--cipd-package', |
| 76 | dest='cipd_packages', |
| 77 | help='A CIPD package to install. ' |
| 78 | 'Format is "<path>:<package_name>:<version>". ' |
| 79 | '"path" is installation directory relative to run_dir, ' |
| 80 | 'defaults to ".". ' |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 81 | '"package_name" may have ${platform} and/or ${os_ver} parameters. ' |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 82 | '${platform} will be expanded to "<os>-<architecture>" and ' |
| 83 | '${os_ver} will be expanded to OS version name. ' |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 84 | 'The option can be specified multiple times.', |
| 85 | action='append', |
| 86 | default=[]) |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 87 | group.add_option( |
| 88 | '--cipd-cache', |
| 89 | help='CIPD cache directory, separate from isolate cache. ' |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 90 | 'Only relevant with --cipd-enabled or --cipd-package. ' |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 91 | 'Default: "%default".', |
| 92 | default='') |
| 93 | parser.add_option_group(group) |
| 94 | |
| 95 | |
| 96 | def validate_cipd_options(parser, options): |
| 97 | """Calls parser.error on first found error among cipd options.""" |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 98 | if options.cipd_packages: |
| 99 | options.cipd_enabled = True |
| 100 | |
| 101 | if not options.cipd_enabled: |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 102 | return |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 103 | |
| 104 | for pkg in options.cipd_packages: |
| 105 | parts = pkg.split(':', 2) |
| 106 | if len(parts) != 3: |
| 107 | parser.error('invalid package "%s": must have at least 2 colons' % pkg) |
| 108 | _path, name, version = parts |
| 109 | if not name: |
| 110 | parser.error('invalid package "%s": package name is not specified' % pkg) |
| 111 | if not version: |
| 112 | parser.error('invalid package "%s": version is not specified' % pkg) |
| 113 | |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 114 | if not options.cipd_server: |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 115 | parser.error('cipd is enabled, --cipd-server is required') |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 116 | |
| 117 | if not options.cipd_client_package: |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 118 | parser.error( |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 119 | 'cipd is enabled, --cipd-client-package is required') |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 120 | if not options.cipd_client_version: |
| 121 | parser.error( |
vadimsh | 902948e | 2017-01-20 15:57:32 -0800 | [diff] [blame] | 122 | 'cipd is enabled, --cipd-client-version is required') |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 123 | |
| 124 | |
| 125 | class CipdClient(object): |
| 126 | """Installs packages.""" |
| 127 | |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 128 | def __init__(self, binary_path, package_name, instance_id, service_url): |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 129 | """Initializes CipdClient. |
| 130 | |
| 131 | Args: |
| 132 | binary_path (str): path to the CIPD client binary. |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 133 | package_name (str): the CIPD package name for the client itself. |
| 134 | instance_id (str): the CIPD instance_id for the client itself. |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 135 | service_url (str): if not None, URL of the CIPD backend that overrides |
| 136 | the default one. |
| 137 | """ |
| 138 | self.binary_path = binary_path |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 139 | self.package_name = package_name |
| 140 | self.instance_id = instance_id |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 141 | self.service_url = service_url |
| 142 | |
| 143 | def ensure( |
| 144 | self, site_root, packages, cache_dir=None, tmp_dir=None, timeout=None): |
| 145 | """Ensures that packages installed in |site_root| equals |packages| set. |
| 146 | |
| 147 | Blocking call. |
| 148 | |
| 149 | Args: |
| 150 | site_root (str): where to install packages. |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 151 | packages: list of (package_template, version) tuples. |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 152 | cache_dir (str): if set, cache dir for cipd binary own cache. |
| 153 | Typically contains packages and tags. |
| 154 | tmp_dir (str): if not None, dir for temp files. |
| 155 | timeout (int): if not None, timeout in seconds for this function to run. |
| 156 | |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 157 | Returns: |
| 158 | Pinned packages in the form of [(package_name, package_id)], which |
| 159 | correspond 1:1 with the input packages argument. |
| 160 | |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 161 | Raises: |
| 162 | Error if could not install packages or timed out. |
| 163 | """ |
| 164 | timeoutfn = tools.sliding_timeout(timeout) |
| 165 | logging.info('Installing packages %r into %s', packages, site_root) |
| 166 | |
| 167 | list_file_handle, list_file_path = tempfile.mkstemp( |
| 168 | dir=tmp_dir, prefix=u'cipd-ensure-list-', suffix='.txt') |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 169 | json_out_file_handle, json_file_path = tempfile.mkstemp( |
| 170 | dir=tmp_dir, prefix=u'cipd-ensure-result-', suffix='.json') |
| 171 | os.close(json_out_file_handle) |
| 172 | |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 173 | try: |
| 174 | try: |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 175 | for pkg, version in packages: |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 176 | pkg = render_package_name_template(pkg) |
| 177 | os.write(list_file_handle, '%s %s\n' % (pkg, version)) |
| 178 | finally: |
| 179 | os.close(list_file_handle) |
| 180 | |
| 181 | cmd = [ |
| 182 | self.binary_path, 'ensure', |
| 183 | '-root', site_root, |
| 184 | '-list', list_file_path, |
| 185 | '-verbose', # this is safe because cipd-ensure does not print a lot |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 186 | '-json-output', json_file_path, |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 187 | ] |
| 188 | if cache_dir: |
| 189 | cmd += ['-cache-dir', cache_dir] |
| 190 | if self.service_url: |
| 191 | cmd += ['-service-url', self.service_url] |
| 192 | |
| 193 | logging.debug('Running %r', cmd) |
| 194 | process = subprocess42.Popen( |
| 195 | cmd, stdout=subprocess42.PIPE, stderr=subprocess42.PIPE) |
| 196 | output = [] |
| 197 | for pipe_name, line in process.yield_any_line(timeout=0.1): |
| 198 | to = timeoutfn() |
| 199 | if to is not None and to <= 0: |
| 200 | raise Error( |
| 201 | 'Could not install packages; took more than %d seconds' % timeout) |
| 202 | if not pipe_name: |
| 203 | # stdout or stderr was closed, but yield_any_line still may have |
| 204 | # something to yield. |
| 205 | continue |
| 206 | output.append(line) |
| 207 | if pipe_name == 'stderr': |
| 208 | logging.debug('cipd client: %s', line) |
| 209 | else: |
| 210 | logging.info('cipd client: %s', line) |
| 211 | |
| 212 | exit_code = process.wait(timeout=timeoutfn()) |
| 213 | if exit_code != 0: |
| 214 | raise Error( |
| 215 | 'Could not install packages; exit code %d\noutput:%s' % ( |
| 216 | exit_code, '\n'.join(output))) |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 217 | with open(json_file_path) as jfile: |
| 218 | result_json = json.load(jfile) |
iannucci | 1c9a369 | 2017-01-30 14:10:49 -0800 | [diff] [blame] | 219 | # TEMPORARY(iannucci): this code handles cipd <1.4 and cipd >=1.5 |
| 220 | # formatted ensure result formats. Cipd 1.5 added support for subdirs, and |
| 221 | # as part of the transition, the result of the ensure command needed to |
| 222 | # change. To ease the transition, we always return data as-if we're using |
| 223 | # the new format. Once cipd 1.5+ is deployed everywhere, this type switch |
| 224 | # can be removed. |
| 225 | if isinstance(result_json['result'], dict): |
| 226 | # cipd 1.5 |
| 227 | return { |
| 228 | subdir: [(x['package'], x['instance_id']) for x in pins] |
| 229 | for subdir, pins in result_json['result'].iteritems() |
| 230 | } |
| 231 | else: |
| 232 | # cipd 1.4 |
| 233 | return { |
| 234 | "": [(x['package'], x['instance_id']) for x in result_json['result']], |
| 235 | } |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 236 | finally: |
| 237 | fs.remove(list_file_path) |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 238 | fs.remove(json_file_path) |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 239 | |
| 240 | |
| 241 | def get_platform(): |
| 242 | """Returns ${platform} parameter value. |
| 243 | |
| 244 | Borrowed from |
| 245 | https://chromium.googlesource.com/infra/infra/+/aaf9586/build/build.py#204 |
| 246 | """ |
| 247 | # linux, mac or windows. |
| 248 | platform_variant = { |
| 249 | 'darwin': 'mac', |
| 250 | 'linux2': 'linux', |
| 251 | 'win32': 'windows', |
| 252 | }.get(sys.platform) |
| 253 | if not platform_variant: |
| 254 | raise Error('Unknown OS: %s' % sys.platform) |
| 255 | |
| 256 | # amd64, 386, etc. |
| 257 | machine = platform.machine().lower() |
| 258 | platform_arch = { |
| 259 | 'amd64': 'amd64', |
| 260 | 'i386': '386', |
| 261 | 'i686': '386', |
| 262 | 'x86': '386', |
| 263 | 'x86_64': 'amd64', |
| 264 | }.get(machine) |
| 265 | if not platform_arch: |
| 266 | if machine.startswith('arm'): |
| 267 | platform_arch = 'armv6l' |
| 268 | else: |
| 269 | platform_arch = 'amd64' if sys.maxsize > 2**32 else '386' |
| 270 | return '%s-%s' % (platform_variant, platform_arch) |
| 271 | |
| 272 | |
| 273 | def get_os_ver(): |
| 274 | """Returns ${os_ver} parameter value. |
| 275 | |
| 276 | Examples: 'ubuntu14_04' or 'mac10_9' or 'win6_1'. |
| 277 | |
| 278 | Borrowed from |
| 279 | https://chromium.googlesource.com/infra/infra/+/aaf9586/build/build.py#204 |
| 280 | """ |
| 281 | if sys.platform == 'darwin': |
| 282 | # platform.mac_ver()[0] is '10.9.5'. |
| 283 | dist = platform.mac_ver()[0].split('.') |
| 284 | return 'mac%s_%s' % (dist[0], dist[1]) |
| 285 | |
| 286 | if sys.platform == 'linux2': |
| 287 | # platform.linux_distribution() is ('Ubuntu', '14.04', ...). |
| 288 | dist = platform.linux_distribution() |
| 289 | return '%s%s' % (dist[0].lower(), dist[1].replace('.', '_')) |
| 290 | |
| 291 | if sys.platform == 'win32': |
| 292 | # platform.version() is '6.1.7601'. |
| 293 | dist = platform.version().split('.') |
| 294 | return 'win%s_%s' % (dist[0], dist[1]) |
| 295 | raise Error('Unknown OS: %s' % sys.platform) |
| 296 | |
| 297 | |
| 298 | def render_package_name_template(template): |
| 299 | """Expands template variables in a CIPD package name template.""" |
| 300 | return (template |
| 301 | .lower() # Package names are always lower case |
| 302 | .replace('${platform}', get_platform()) |
| 303 | .replace('${os_ver}', get_os_ver())) |
| 304 | |
| 305 | |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 306 | def _check_response(res, fmt, *args): |
| 307 | """Raises Error if response is bad.""" |
| 308 | if not res: |
| 309 | raise Error('%s: no response' % (fmt % args)) |
| 310 | |
| 311 | if res.get('status') != 'SUCCESS': |
| 312 | raise Error('%s: %s' % ( |
| 313 | fmt % args, |
| 314 | res.get('error_message') or 'status is %s' % res.get('status'))) |
| 315 | |
| 316 | |
| 317 | def resolve_version(cipd_server, package_name, version, timeout=None): |
| 318 | """Resolves a package instance version (e.g. a tag) to an instance id.""" |
| 319 | url = '%s/_ah/api/repo/v1/instance/resolve?%s' % ( |
| 320 | cipd_server, |
| 321 | urllib.urlencode({ |
| 322 | 'package_name': package_name, |
| 323 | 'version': version, |
| 324 | })) |
| 325 | res = net.url_read_json(url, timeout=timeout) |
| 326 | _check_response(res, 'Could not resolve version %s:%s', package_name, version) |
| 327 | instance_id = res.get('instance_id') |
| 328 | if not instance_id: |
| 329 | raise Error('Invalid resolveVersion response: no instance id') |
| 330 | return instance_id |
| 331 | |
| 332 | |
| 333 | def get_client_fetch_url(service_url, package_name, instance_id, timeout=None): |
| 334 | """Returns a fetch URL of CIPD client binary contents. |
| 335 | |
| 336 | Raises: |
| 337 | Error if cannot retrieve fetch URL. |
| 338 | """ |
| 339 | # Fetch the URL of the binary from CIPD backend. |
| 340 | package_name = render_package_name_template(package_name) |
| 341 | url = '%s/_ah/api/repo/v1/client?%s' % (service_url, urllib.urlencode({ |
| 342 | 'package_name': package_name, |
| 343 | 'instance_id': instance_id, |
| 344 | })) |
| 345 | res = net.url_read_json(url, timeout=timeout) |
| 346 | _check_response( |
| 347 | res, 'Could not fetch CIPD client %s:%s',package_name, instance_id) |
| 348 | fetch_url = res.get('client_binary', {}).get('fetch_url') |
| 349 | if not fetch_url: |
| 350 | raise Error('Invalid fetchClientBinary response: no fetch_url') |
| 351 | return fetch_url |
| 352 | |
| 353 | |
| 354 | def _fetch_cipd_client(disk_cache, instance_id, fetch_url, timeoutfn): |
| 355 | """Fetches cipd binary to |disk_cache|. |
| 356 | |
| 357 | Retries requests with exponential back-off. |
| 358 | |
| 359 | Raises: |
| 360 | Error if could not fetch content. |
| 361 | """ |
| 362 | sleep_time = 1 |
| 363 | for attempt in xrange(5): |
| 364 | if attempt > 0: |
| 365 | if timeoutfn() is not None and timeoutfn() < sleep_time: |
| 366 | raise Error('Could not fetch CIPD client: timeout') |
| 367 | logging.warning('Will retry to fetch CIPD client in %ds', sleep_time) |
| 368 | time.sleep(sleep_time) |
| 369 | sleep_time *= 2 |
| 370 | |
| 371 | try: |
| 372 | res = net.url_open(fetch_url, timeout=timeoutfn()) |
| 373 | if res: |
| 374 | disk_cache.write(instance_id, res.iter_content(64 * 1024)) |
| 375 | return |
| 376 | except net.TimeoutError as ex: |
| 377 | raise Error('Could not fetch CIPD client: %s', ex) |
| 378 | except net.NetError as ex: |
| 379 | logging.warning( |
| 380 | 'Could not fetch CIPD client on attempt #%d: %s', attempt + 1, ex) |
| 381 | |
| 382 | raise Error('Could not fetch CIPD client after 5 retries') |
| 383 | |
| 384 | |
| 385 | @contextlib.contextmanager |
vadimsh | 232f5a8 | 2017-01-20 19:23:44 -0800 | [diff] [blame] | 386 | def get_client(service_url, package_name, version, cache_dir, timeout=None): |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 387 | """Returns a context manager that yields a CipdClient. A blocking call. |
| 388 | |
vadimsh | 232f5a8 | 2017-01-20 19:23:44 -0800 | [diff] [blame] | 389 | Upon exit from the context manager, the client binary may be deleted |
| 390 | (if the internal cache is full). |
| 391 | |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 392 | Args: |
vadimsh | 232f5a8 | 2017-01-20 19:23:44 -0800 | [diff] [blame] | 393 | service_url (str): URL of the CIPD backend. |
| 394 | package_name (str): package name template of the CIPD client. |
| 395 | version (str): version of CIPD client package. |
| 396 | cache_dir: directory to store instance cache, version cache |
| 397 | and a hardlink to the client binary. |
| 398 | timeout (int): if not None, timeout in seconds for this function. |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 399 | |
| 400 | Yields: |
| 401 | CipdClient. |
| 402 | |
| 403 | Raises: |
| 404 | Error if CIPD client version cannot be resolved or client cannot be fetched. |
| 405 | """ |
| 406 | timeoutfn = tools.sliding_timeout(timeout) |
| 407 | |
| 408 | package_name = render_package_name_template(package_name) |
| 409 | |
| 410 | # Resolve version to instance id. |
| 411 | # Is it an instance id already? They look like HEX SHA1. |
| 412 | if isolated_format.is_valid_hash(version, hashlib.sha1): |
| 413 | instance_id = version |
iannucci | 6fd57d2 | 2016-08-30 17:02:20 -0700 | [diff] [blame] | 414 | elif ':' in version: # it's an immutable tag |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 415 | # version_cache is {version_digest -> instance id} mapping. |
| 416 | # It does not take a lot of disk space. |
| 417 | version_cache = isolateserver.DiskCache( |
| 418 | unicode(os.path.join(cache_dir, 'versions')), |
| 419 | isolateserver.CachePolicies(0, 0, 300), |
| 420 | hashlib.sha1) |
| 421 | with version_cache: |
maruel | 2e8d0f5 | 2016-07-16 07:51:29 -0700 | [diff] [blame] | 422 | version_cache.cleanup() |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 423 | # Convert |version| to a string that may be used as a filename in disk |
| 424 | # cache by hashing it. |
| 425 | version_digest = hashlib.sha1(version).hexdigest() |
| 426 | try: |
tansell | 9e04a8d | 2016-07-28 09:31:59 -0700 | [diff] [blame] | 427 | with version_cache.getfileobj(version_digest) as f: |
| 428 | instance_id = f.read() |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 429 | except isolateserver.CacheMiss: |
| 430 | instance_id = resolve_version( |
| 431 | service_url, package_name, version, timeout=timeoutfn()) |
| 432 | version_cache.write(version_digest, instance_id) |
iannucci | 6fd57d2 | 2016-08-30 17:02:20 -0700 | [diff] [blame] | 433 | else: # it's a ref |
| 434 | instance_id = resolve_version( |
| 435 | service_url, package_name, version, timeout=timeoutfn()) |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 436 | |
| 437 | # instance_cache is {instance_id -> client binary} mapping. |
| 438 | # It is bounded by 5 client versions. |
| 439 | instance_cache = isolateserver.DiskCache( |
| 440 | unicode(os.path.join(cache_dir, 'clients')), |
| 441 | isolateserver.CachePolicies(0, 0, 5), |
| 442 | hashlib.sha1) |
| 443 | with instance_cache: |
maruel | 2e8d0f5 | 2016-07-16 07:51:29 -0700 | [diff] [blame] | 444 | instance_cache.cleanup() |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 445 | if instance_id not in instance_cache: |
| 446 | logging.info('Fetching CIPD client %s:%s', package_name, instance_id) |
| 447 | fetch_url = get_client_fetch_url( |
| 448 | service_url, package_name, instance_id, timeout=timeoutfn()) |
| 449 | _fetch_cipd_client(instance_cache, instance_id, fetch_url, timeoutfn) |
| 450 | |
| 451 | # A single host can run multiple swarming bots, but ATM they do not share |
| 452 | # same root bot directory. Thus, it is safe to use the same name for the |
| 453 | # binary. |
vadimsh | 232f5a8 | 2017-01-20 19:23:44 -0800 | [diff] [blame] | 454 | cipd_bin_dir = unicode(os.path.join(cache_dir, 'bin')) |
| 455 | binary_path = os.path.join(cipd_bin_dir, 'cipd' + EXECUTABLE_SUFFIX) |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 456 | if fs.isfile(binary_path): |
nodir | 6dfdb2d | 2016-06-14 20:14:08 -0700 | [diff] [blame] | 457 | file_path.remove(binary_path) |
vadimsh | 232f5a8 | 2017-01-20 19:23:44 -0800 | [diff] [blame] | 458 | else: |
| 459 | file_path.ensure_tree(cipd_bin_dir) |
tansell | 9e04a8d | 2016-07-28 09:31:59 -0700 | [diff] [blame] | 460 | |
| 461 | with instance_cache.getfileobj(instance_id) as f: |
| 462 | isolateserver.putfile(f, binary_path, 0511) # -r-x--x--x |
nodir | be642ff | 2016-06-09 15:51:51 -0700 | [diff] [blame] | 463 | |
iannucci | 4d7792a | 2017-03-10 10:30:56 -0800 | [diff] [blame^] | 464 | _ensure_batfile(binary_path) |
| 465 | |
vadimsh | 232f5a8 | 2017-01-20 19:23:44 -0800 | [diff] [blame] | 466 | yield CipdClient( |
| 467 | binary_path, |
| 468 | package_name=package_name, |
| 469 | instance_id=instance_id, |
| 470 | service_url=service_url) |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 471 | |
| 472 | |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 473 | def parse_package_args(packages): |
| 474 | """Parses --cipd-package arguments. |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 475 | |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 476 | Assumes |packages| were validated by validate_cipd_options. |
| 477 | |
| 478 | Returns: |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 479 | A list of [(path, package_name, version), ...] |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 480 | """ |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 481 | result = [] |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 482 | for pkg in packages: |
| 483 | path, name, version = pkg.split(':', 2) |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 484 | if not name: |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 485 | raise Error('Invalid package "%s": package name is not specified' % pkg) |
nodir | 90bc8dc | 2016-06-15 13:35:21 -0700 | [diff] [blame] | 486 | if not version: |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 487 | raise Error('Invalid package "%s": version is not specified' % pkg) |
iannucci | 96fcccc | 2016-08-30 15:52:22 -0700 | [diff] [blame] | 488 | result.append((path, name, version)) |
nodir | ff531b4 | 2016-06-23 13:05:06 -0700 | [diff] [blame] | 489 | return result |