| OLD | NEW |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 """Fetches CIPD client and installs packages.""" | 5 """Fetches CIPD client and installs packages.""" |
| 6 | 6 |
| 7 __version__ = '0.1' | 7 __version__ = '0.2' |
| 8 | 8 |
| 9 import collections |
| 9 import contextlib | 10 import contextlib |
| 11 import json |
| 10 import hashlib | 12 import hashlib |
| 11 import logging | 13 import logging |
| 12 import optparse | 14 import optparse |
| 13 import os | 15 import os |
| 14 import platform | 16 import platform |
| 15 import sys | 17 import sys |
| 16 import tempfile | 18 import tempfile |
| 17 import time | 19 import time |
| 18 import urllib | 20 import urllib |
| 19 | 21 |
| (...skipping 11 matching lines...) Expand all Loading... |
| 31 | 33 |
| 32 | 34 |
| 33 class Error(Exception): | 35 class Error(Exception): |
| 34 """Raised on CIPD errors.""" | 36 """Raised on CIPD errors.""" |
| 35 | 37 |
| 36 | 38 |
| 37 def add_cipd_options(parser): | 39 def add_cipd_options(parser): |
| 38 group = optparse.OptionGroup(parser, 'CIPD') | 40 group = optparse.OptionGroup(parser, 'CIPD') |
| 39 group.add_option( | 41 group.add_option( |
| 40 '--cipd-server', | 42 '--cipd-server', |
| 41 help='URL of the CIPD server. Only relevant with --cipd-package.') | 43 help='URL of the CIPD server. Only relevant with --cipd-package-list.') |
| 42 group.add_option( | 44 group.add_option( |
| 43 '--cipd-client-package', | 45 '--cipd-client-package', |
| 44 help='Package of CIPD client. See --cipd-package for format. ' | 46 help='Package name of CIPD client with optional parameters described in ' |
| 45 'Only relevant with --cipd-package. ' | 47 '--cipd-package-list help. ' |
| 48 'Only relevant with --cipd-package-list. ' |
| 46 'Default: "%default"', | 49 'Default: "%default"', |
| 47 default='infra/tools/cipd/${platform}:latest') | 50 default='infra/tools/cipd/${platform}') |
| 48 group.add_option( | 51 group.add_option( |
| 49 '--cipd-package', | 52 '--cipd-client-version', |
| 50 help='CIPD package to install. ' | 53 help='Version of CIPD client. ' |
| 51 'Format: "<package name template>:<version>". ' | 54 'Only relevant with --cipd-package-list. ' |
| 52 'Package name template is a CIPD package name with optional ' | 55 'Default: "%default"', |
| 53 '${platform} and/or ${os_ver} parameters. ' | 56 default='latest') |
| 57 group.add_option( |
| 58 '--cipd-package-list', |
| 59 help='Path to file that contains the list of CIPD packages to install. ' |
| 60 'It should be a JSON object with property "packages" which is a list' |
| 61 'of package JSON objects. Each package must have "package_name" and ' |
| 62 '"version" properties, and may have "path" property. ' |
| 63 '"package_name" may have ${platform} and/or ${os_ver} parameters. ' |
| 54 '${platform} will be expanded to "<os>-<architecture>" and ' | 64 '${platform} will be expanded to "<os>-<architecture>" and ' |
| 55 '${os_ver} will be expanded to OS version name. ' | 65 '${os_ver} will be expanded to OS version name. ' |
| 56 'This option can be specified more than once.', | 66 '"path" is destination directory relative to run_dir, ' |
| 57 action='append') | 67 'defaults to ".".' |
| 68 ) |
| 58 group.add_option( | 69 group.add_option( |
| 59 '--cipd-cache', | 70 '--cipd-cache', |
| 60 help='CIPD cache directory, separate from isolate cache. ' | 71 help='CIPD cache directory, separate from isolate cache. ' |
| 61 'Only relevant with --cipd-package. ' | 72 'Only relevant with --cipd-package. ' |
| 62 'Default: "%default".', | 73 'Default: "%default".', |
| 63 default='') | 74 default='') |
| 64 parser.add_option_group(group) | 75 parser.add_option_group(group) |
| 65 | 76 |
| 66 | 77 |
| 67 def validate_cipd_options(parser, options): | 78 def validate_cipd_options(parser, options): |
| 68 """Calls parser.error on first found error among cipd options.""" | 79 """Calls parser.error on first found error among cipd options.""" |
| 69 if not options.cipd_package: | 80 if not options.cipd_package_list: |
| 70 return | 81 return |
| 71 for p in options.cipd_package: | |
| 72 try: | |
| 73 parse_package(p) | |
| 74 except ValueError as ex: | |
| 75 parser.error('Invalid cipd package %r: %s' % (p, ex)) | |
| 76 | |
| 77 if not options.cipd_server: | 82 if not options.cipd_server: |
| 78 parser.error('--cipd-package requires non-empty --cipd-server') | 83 parser.error('--cipd-package-list requires non-empty --cipd-server') |
| 79 | 84 |
| 80 if not options.cipd_client_package: | 85 if not options.cipd_client_package: |
| 81 parser.error('--cipd-package requires non-empty --cipd-client-package') | |
| 82 try: | |
| 83 parse_package(options.cipd_client_package) | |
| 84 except ValueError as ex: | |
| 85 parser.error( | 86 parser.error( |
| 86 'Invalid cipd client package %r: %s' % | 87 '--cipd-package-list requires non-empty --cipd-client-package') |
| 87 (options.cipd_client_package, ex)) | 88 if not options.cipd_client_version: |
| 89 parser.error( |
| 90 '--cipd-package-list requires non-empty --cipd-client-version') |
| 88 | 91 |
| 89 | 92 |
| 90 class CipdClient(object): | 93 class CipdClient(object): |
| 91 """Installs packages.""" | 94 """Installs packages.""" |
| 92 | 95 |
| 93 def __init__(self, binary_path, service_url=None): | 96 def __init__(self, binary_path, service_url=None): |
| 94 """Initializes CipdClient. | 97 """Initializes CipdClient. |
| 95 | 98 |
| 96 Args: | 99 Args: |
| 97 binary_path (str): path to the CIPD client binary. | 100 binary_path (str): path to the CIPD client binary. |
| 98 service_url (str): if not None, URL of the CIPD backend that overrides | 101 service_url (str): if not None, URL of the CIPD backend that overrides |
| 99 the default one. | 102 the default one. |
| 100 """ | 103 """ |
| 101 self.binary_path = binary_path | 104 self.binary_path = binary_path |
| 102 self.service_url = service_url | 105 self.service_url = service_url |
| 103 | 106 |
| 104 def ensure( | 107 def ensure( |
| 105 self, site_root, packages, cache_dir=None, tmp_dir=None, timeout=None): | 108 self, site_root, packages, cache_dir=None, tmp_dir=None, timeout=None): |
| 106 """Ensures that packages installed in |site_root| equals |packages| set. | 109 """Ensures that packages installed in |site_root| equals |packages| set. |
| 107 | 110 |
| 108 Blocking call. | 111 Blocking call. |
| 109 | 112 |
| 110 Args: | 113 Args: |
| 111 site_root (str): where to install packages. | 114 site_root (str): where to install packages. |
| 112 packages (str): list of packages to install, parsable by parse_pacakge(). | 115 packages: list of (package_template, version) tuples. |
| 113 cache_dir (str): if set, cache dir for cipd binary own cache. | 116 cache_dir (str): if set, cache dir for cipd binary own cache. |
| 114 Typically contains packages and tags. | 117 Typically contains packages and tags. |
| 115 tmp_dir (str): if not None, dir for temp files. | 118 tmp_dir (str): if not None, dir for temp files. |
| 116 timeout (int): if not None, timeout in seconds for this function to run. | 119 timeout (int): if not None, timeout in seconds for this function to run. |
| 117 | 120 |
| 118 Raises: | 121 Raises: |
| 119 Error if could not install packages or timed out. | 122 Error if could not install packages or timed out. |
| 120 """ | 123 """ |
| 121 timeoutfn = tools.sliding_timeout(timeout) | 124 timeoutfn = tools.sliding_timeout(timeout) |
| 122 logging.info('Installing packages %r into %s', packages, site_root) | 125 logging.info('Installing packages %r into %s', packages, site_root) |
| 123 | 126 |
| 124 list_file_handle, list_file_path = tempfile.mkstemp( | 127 list_file_handle, list_file_path = tempfile.mkstemp( |
| 125 dir=tmp_dir, prefix=u'cipd-ensure-list-', suffix='.txt') | 128 dir=tmp_dir, prefix=u'cipd-ensure-list-', suffix='.txt') |
| 126 try: | 129 try: |
| 127 try: | 130 try: |
| 128 for p in packages: | 131 for pkg, version in packages: |
| 129 pkg, version = parse_package(p) | |
| 130 pkg = render_package_name_template(pkg) | 132 pkg = render_package_name_template(pkg) |
| 131 os.write(list_file_handle, '%s %s\n' % (pkg, version)) | 133 os.write(list_file_handle, '%s %s\n' % (pkg, version)) |
| 132 finally: | 134 finally: |
| 133 os.close(list_file_handle) | 135 os.close(list_file_handle) |
| 134 | 136 |
| 135 cmd = [ | 137 cmd = [ |
| 136 self.binary_path, 'ensure', | 138 self.binary_path, 'ensure', |
| 137 '-root', site_root, | 139 '-root', site_root, |
| 138 '-list', list_file_path, | 140 '-list', list_file_path, |
| 139 '-verbose', # this is safe because cipd-ensure does not print a lot | 141 '-verbose', # this is safe because cipd-ensure does not print a lot |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 229 | 231 |
| 230 | 232 |
| 231 def render_package_name_template(template): | 233 def render_package_name_template(template): |
| 232 """Expands template variables in a CIPD package name template.""" | 234 """Expands template variables in a CIPD package name template.""" |
| 233 return (template | 235 return (template |
| 234 .lower() # Package names are always lower case | 236 .lower() # Package names are always lower case |
| 235 .replace('${platform}', get_platform()) | 237 .replace('${platform}', get_platform()) |
| 236 .replace('${os_ver}', get_os_ver())) | 238 .replace('${os_ver}', get_os_ver())) |
| 237 | 239 |
| 238 | 240 |
| 239 def parse_package(package): | |
| 240 """Parses a package in --cipd-package format. | |
| 241 | |
| 242 Returns: | |
| 243 (package_name_template, version) tuple. | |
| 244 | |
| 245 Raises: | |
| 246 ValueError if package name or version is not specified. | |
| 247 """ | |
| 248 if not package: | |
| 249 raise ValueError('package is not specified') | |
| 250 parts = package.split(':', 1) | |
| 251 if len(parts) != 2: | |
| 252 raise ValueError('version is not specified') | |
| 253 return tuple(parts) | |
| 254 | |
| 255 | |
| 256 def _check_response(res, fmt, *args): | 241 def _check_response(res, fmt, *args): |
| 257 """Raises Error if response is bad.""" | 242 """Raises Error if response is bad.""" |
| 258 if not res: | 243 if not res: |
| 259 raise Error('%s: no response' % (fmt % args)) | 244 raise Error('%s: no response' % (fmt % args)) |
| 260 | 245 |
| 261 if res.get('status') != 'SUCCESS': | 246 if res.get('status') != 'SUCCESS': |
| 262 raise Error('%s: %s' % ( | 247 raise Error('%s: %s' % ( |
| 263 fmt % args, | 248 fmt % args, |
| 264 res.get('error_message') or 'status is %s' % res.get('status'))) | 249 res.get('error_message') or 'status is %s' % res.get('status'))) |
| 265 | 250 |
| (...skipping 126 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 392 | 377 |
| 393 # A single host can run multiple swarming bots, but ATM they do not share | 378 # A single host can run multiple swarming bots, but ATM they do not share |
| 394 # same root bot directory. Thus, it is safe to use the same name for the | 379 # same root bot directory. Thus, it is safe to use the same name for the |
| 395 # binary. | 380 # binary. |
| 396 binary_path = unicode(os.path.join(cache_dir, 'cipd' + EXECUTABLE_SUFFIX)) | 381 binary_path = unicode(os.path.join(cache_dir, 'cipd' + EXECUTABLE_SUFFIX)) |
| 397 if fs.isfile(binary_path): | 382 if fs.isfile(binary_path): |
| 398 file_path.remove(binary_path) | 383 file_path.remove(binary_path) |
| 399 instance_cache.hardlink(instance_id, binary_path, 0511) # -r-x--x--x | 384 instance_cache.hardlink(instance_id, binary_path, 0511) # -r-x--x--x |
| 400 | 385 |
| 401 yield CipdClient(binary_path) | 386 yield CipdClient(binary_path) |
| 387 |
| 388 |
| 389 def parse_package_list_file(path): |
| 390 """Returns a map {site_root_path: [(package, version)]} read from file. |
| 391 |
| 392 Slashes in site_root_path are replaced with os.path.sep. |
| 393 """ |
| 394 with open(path) as f: |
| 395 try: |
| 396 parsed = json.load(f) |
| 397 except ValueError as ex: |
| 398 raise Error('Invalid package list file: %s' % ex) |
| 399 |
| 400 packages = collections.defaultdict(list) |
| 401 for package in parsed.get('packages') or []: |
| 402 path = package.get('path') or '.' |
| 403 path = path.replace('/', os.path.sep) |
| 404 |
| 405 name = package.get('package_name') |
| 406 if not name: |
| 407 raise Error('Invalid package list file: package name is not specified') |
| 408 version = package.get('version') |
| 409 if not version: |
| 410 raise Error('Invalid package list file: package version is not specified') |
| 411 packages[path].append((name, version)) |
| 412 return packages |
| OLD | NEW |