Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2012 The LUCI Authors. All rights reserved. | |
|
M-A Ruel
2016/06/06 23:34:50
2012?
nodir
2016/06/07 18:46:35
Done.
| |
| 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 | |
| 7 __version__ = '0.1' | |
| 8 | |
| 9 import hashlib | |
| 10 import logging | |
| 11 import optparse | |
| 12 import platform | |
| 13 import sys | |
| 14 import tempfile | |
| 15 import urllib | |
| 16 | |
| 17 from utils import fs | |
| 18 from utils import net | |
| 19 from utils import subprocess42 | |
| 20 from utils import tools | |
| 21 import isolated_format | |
| 22 import isolateserver | |
| 23 | |
| 24 | |
| 25 class Error(Exception): | |
| 26 """Raised on CIPD errors.""" | |
| 27 | |
| 28 | |
| 29 def add_cipd_options(parser): | |
| 30 group = optparse.OptionGroup(parser, 'CIPD') | |
| 31 group.add_option( | |
| 32 '--cipd-server', | |
| 33 help=( | |
|
M-A Ruel
2016/06/06 23:34:50
() are not needed for str concatenation
nodir
2016/06/07 18:46:35
Done.
| |
| 34 'URL of the CIPD server. Only relevant with --cipd-package. ' | |
| 35 'Default: "%default"'), | |
| 36 default='https://chrome-infra-packages.appspot.com') | |
| 37 group.add_option( | |
| 38 '--cipd-client-package', | |
| 39 help=( | |
| 40 'Package of CIPD client. See --cipd-package for format. ' | |
| 41 'Only relevant with --cipd-package. ' | |
| 42 'Default: "%default"'), | |
| 43 default='infra/tools/cipd/${platform}:latest') | |
| 44 group.add_option( | |
| 45 '--cipd-package', | |
| 46 help=( | |
| 47 'CIPD package to install. ' | |
| 48 'Format: "<package name template>:<version>". ' | |
| 49 'Package name template is a CIPD package name with optional ' | |
| 50 '${platform} and/or ${os_ver} parameters. ' | |
| 51 '${platform} will be expanded to "<os>-<architecture>" and ' | |
| 52 '${os_ver} will be expanded to OS version name. ' | |
| 53 'This option can be specified more than once.'), | |
| 54 action='append') | |
| 55 group.add_option( | |
| 56 '--cipd-cache', | |
| 57 help=( | |
| 58 'CIPD cache directory, separate from isolate cache. ' | |
| 59 'Only relevant with --cipd-package. ' | |
| 60 'Default: "%default".')) | |
| 61 parser.add_option_group(group) | |
| 62 | |
| 63 | |
| 64 def validate_cipd_options(parser, options): | |
| 65 """Calls parser.error on first found error among cipd options.""" | |
| 66 if not options.cipd_package: | |
| 67 return | |
| 68 for p in options.cipd_package: | |
| 69 try: | |
| 70 parse_package(p) | |
| 71 except ValueError as ex: | |
| 72 parser.error('Invalid cipd package %r: %s' % (p, ex)) | |
| 73 | |
| 74 if not options.cipd_server: | |
| 75 parser.error('--cipd-package requires non-empty --cipd-server') | |
| 76 | |
| 77 if not options.cipd_client_package: | |
| 78 parser.error('--cipd-package requires non-empty --cipd-client-package') | |
| 79 try: | |
| 80 parse_package(options.cipd_client_package) | |
| 81 except ValueError as ex: | |
| 82 parser.error( | |
| 83 'Invalid cipd client package %r: %s' % | |
| 84 (options.cipd_client_package, ex)) | |
| 85 | |
| 86 | |
| 87 class CipdClient(object): | |
| 88 """Installs packages.""" | |
| 89 | |
| 90 def __init__(self, binary_path, service_url=None): | |
| 91 """Initializes CipdClient. | |
| 92 | |
| 93 Args: | |
| 94 binary_path (str): path to the CIPD client binary. | |
| 95 service_url (str): if not None, URL of the CIPD backend that overrides | |
| 96 the default one. | |
| 97 """ | |
| 98 self.binary_path = binary_path | |
| 99 self.service_url = service_url | |
| 100 | |
| 101 def ensure(self, site_root, packages, cache_dir=None, timeout=None): | |
| 102 """Ensures that packages installed in |site_root| equals |packages| set. | |
| 103 | |
| 104 Blocking call. | |
| 105 | |
| 106 Args: | |
| 107 site_root (str): where to install packages. | |
| 108 packages (str): list of packages to install, parsable by parse_pacakge(). | |
| 109 cache_dir (str): if set, cache dir for cipd binary own cache. | |
| 110 Typically contains packages and tags. | |
| 111 timeout (int): if not None, timeout in seconds for this function to run. | |
| 112 | |
| 113 Raises: | |
| 114 Error if could not install packages or timed out. | |
| 115 """ | |
| 116 timeouter = tools.Timeouter(timeout) | |
| 117 logging.info('Installing packages %r into %s', packages, site_root) | |
| 118 with tempfile.NamedTemporaryFile(prefix='cipd-ensure-list-') as list_file: | |
|
Vadim Sh.
2016/06/06 21:32:13
keep it under cache_dir or site_root, not in globa
nodir
2016/06/07 18:46:35
Done.
| |
| 119 for p in packages: | |
| 120 pkg, version = parse_package(p) | |
| 121 pkg = render_package_name_template(pkg) | |
| 122 list_file.write('%s %s\n' % (pkg, version)) | |
| 123 list_file.flush() | |
|
Vadim Sh.
2016/06/06 21:32:13
there's а chance this won't work on Windows due to
M-A Ruel
2016/06/06 23:34:50
Vadim is right, this is broken on Windows.
nodir
2016/06/07 18:46:35
Done.
| |
| 124 | |
| 125 cmd = [ | |
| 126 self.binary_path, 'ensure', | |
| 127 '-root', site_root, | |
| 128 '-list', list_file.name, | |
| 129 '-verbose', # this is safe because cipd-ensure does not print a lot | |
| 130 ] | |
| 131 if cache_dir: | |
| 132 cmd += ['-cache-dir', cache_dir] | |
| 133 if self.service_url: | |
| 134 cmd += ['-service-url', self.service_url] | |
| 135 | |
| 136 logging.debug('Running %r', cmd) | |
| 137 process = subprocess42.Popen( | |
| 138 cmd, stdout=subprocess42.PIPE, stderr=subprocess42.PIPE) | |
| 139 output = [] | |
| 140 for pipe_name, line in process.yield_any_line(timeout=timeouter.left): | |
| 141 if pipe_name is None: | |
|
M-A Ruel
2016/06/06 23:34:51
It could also mean that one pipe closed, not neces
nodir
2016/06/07 18:46:34
Done
| |
| 142 raise Error( | |
| 143 'Could not install packages; took more than %d seconds' % timeout) | |
|
Vadim Sh.
2016/06/06 21:32:13
We should probably add this to the cipd client too
nodir
2016/06/07 18:46:35
I started doing that, but it turned out much more
| |
| 144 output.append(line) | |
| 145 if pipe_name == 'stderr': | |
| 146 logging.debug('cipd client: %s', line) | |
| 147 else: | |
| 148 logging.info('cipd client: %s', line) | |
| 149 | |
| 150 exit_code = process.wait(timeout=timeouter.left()) | |
| 151 if exit_code != 0: | |
| 152 raise Error( | |
| 153 'Could not install packages; exit code %d\noutput:%s' % ( | |
| 154 exit_code, '\n'.join(output))) | |
| 155 | |
| 156 | |
| 157 def get_platform(): | |
| 158 """Returns ${platform} parameter value. | |
| 159 | |
| 160 Borrowed from | |
| 161 https://chromium.googlesource.com/infra/infra/+/aaf9586/build/build.py#204 | |
| 162 """ | |
| 163 # linux, mac or windows. | |
| 164 platform_variant = { | |
| 165 'darwin': 'mac', | |
| 166 'linux2': 'linux', | |
| 167 'win32': 'windows', | |
| 168 }.get(sys.platform) | |
| 169 if not platform_variant: | |
| 170 raise Error('Unknown OS: %s' % sys.platform) | |
| 171 | |
| 172 # amd64, 386, etc. | |
| 173 platform_arch = { | |
| 174 'amd64': 'amd64', | |
| 175 'i386': '386', | |
| 176 'i686': '386', | |
| 177 'x86': '386', | |
| 178 'x86_64': 'amd64', | |
| 179 }.get(platform.machine().lower()) | |
|
M-A Ruel
2016/06/06 23:34:50
https://github.com/luci/luci-py/blob/master/appeng
nodir
2016/06/07 18:46:34
that function cannot be imported here because this
| |
| 180 if not platform_arch: | |
| 181 raise Error('Unknown machine arch: %s' % platform.machine()) | |
| 182 return '%s-%s' % (platform_variant, platform_arch) | |
| 183 | |
| 184 | |
| 185 def get_os_ver(): | |
| 186 """Returns ${os_ver} parameter value. | |
| 187 | |
| 188 Examples: 'ubuntu14_04' or 'mac10_9' or 'win6_1'. | |
| 189 | |
| 190 Borrowed from | |
| 191 https://chromium.googlesource.com/infra/infra/+/aaf9586/build/build.py#204 | |
| 192 """ | |
| 193 if sys.platform == 'darwin': | |
| 194 # platform.mac_ver()[0] is '10.9.5'. | |
| 195 dist = platform.mac_ver()[0].split('.') | |
| 196 return 'mac%s_%s' % (dist[0], dist[1]) | |
| 197 | |
| 198 if sys.platform == 'linux2': | |
| 199 # platform.linux_distribution() is ('Ubuntu', '14.04', ...). | |
| 200 dist = platform.linux_distribution() | |
| 201 return '%s%s' % (dist[0].lower(), dist[1].replace('.', '_')) | |
| 202 | |
| 203 if sys.platform == 'win32': | |
| 204 # platform.version() is '6.1.7601'. | |
| 205 dist = platform.version().split('.') | |
| 206 return 'win%s_%s' % (dist[0], dist[1]) | |
| 207 raise Error('Unknown OS: %s' % sys.platform) | |
| 208 | |
| 209 | |
| 210 def render_package_name_template(template): | |
| 211 """Expands template variables in a CIPD package name template.""" | |
| 212 result = template | |
|
M-A Ruel
2016/06/06 23:34:50
this line is not useful.
nodir
2016/06/07 18:46:34
Done.
| |
| 213 result = result.replace('${platform}', get_platform()) | |
| 214 result = result.replace('${os_ver}', get_os_ver()) | |
| 215 return result | |
|
M-A Ruel
2016/06/06 23:34:50
this is not needed either
nodir
2016/06/07 18:46:35
Done
| |
| 216 | |
| 217 | |
| 218 def parse_package(package): | |
| 219 """Parses a package in --cipd-package format. | |
| 220 | |
| 221 Returns: | |
| 222 (package_name_template, version) tuple. | |
| 223 | |
| 224 Raises: | |
| 225 ValueError if package name or version is not specified. | |
| 226 """ | |
| 227 if not package: | |
| 228 raise ValueError('package is not specified') | |
| 229 parts = package.split(':', 1) | |
| 230 if len(parts) != 2: | |
| 231 raise ValueError('version is not specified') | |
| 232 return tuple(parts) | |
| 233 | |
| 234 | |
| 235 def _check_response(res, fmt, *args): | |
| 236 """Raises Error if response is bad.""" | |
| 237 if res and res.get('status') == 'SUCCESS': | |
|
M-A Ruel
2016/06/06 23:34:50
I'd replace the whole function with:
if not res:
nodir
2016/06/07 18:46:35
Done.
| |
| 238 return | |
| 239 | |
| 240 if not res: | |
| 241 reason = 'no response' | |
| 242 else: | |
| 243 reason = res.get('error_message') or 'status is %s' % res.get('status') | |
| 244 raise Error( | |
| 245 '%s: %s' % (fmt & args, reason)) | |
| 246 | |
| 247 | |
| 248 def resolve_version(cipd_server, package_name, version, timeout=None): | |
| 249 """Resolves a package instance version (e.g. a tag) to an instance id.""" | |
| 250 url = '%s/_ah/api/repo/v1/instance/resolve?%s' % ( | |
| 251 cipd_server, | |
| 252 urllib.urlencode({ | |
| 253 'package_name': package_name, | |
| 254 'version': version, | |
| 255 })) | |
| 256 res = net.url_read_json(url, timeout=timeout) | |
| 257 _check_response(res, 'Could not resolve version %s:%s', package_name, version) | |
| 258 instance_id = res.get('instance_id') | |
| 259 if not instance_id: | |
| 260 raise Error('Invalid resolveVersion response: no instance id') | |
| 261 return instance_id | |
| 262 | |
| 263 | |
| 264 def fetch_client_binary(cipd_server, package_name, instance_id, timeout=None): | |
| 265 """Returns a net.HttpResponse with CIPD client binary contents. | |
| 266 | |
| 267 Raises: | |
| 268 Error if cannot fetch the client. | |
| 269 """ | |
| 270 # Fetch the URL of the binary from CIPD backend. | |
| 271 package_name = render_package_name_template(package_name) | |
| 272 url = '%s/_ah/api/repo/v1/client?%s' % (cipd_server, urllib.urlencode({ | |
| 273 'package_name': package_name, | |
| 274 'instance_id': instance_id, | |
| 275 })) | |
| 276 res = net.url_read_json(url, timeout=timeout) | |
| 277 _check_response( | |
| 278 res, 'Could not fetch CIPD client %s:%s',package_name, instance_id) | |
| 279 fetch_url = res.get('client_binary', {}).get('fetch_url') | |
| 280 if not fetch_url: | |
| 281 raise Error('Invalid fetchClientBinary response: no fetch_url') | |
| 282 logging.debug('CIPD client binary fetch URL: %s', fetch_url) | |
| 283 | |
| 284 # Now fetch the actual binary from Google Storage. | |
| 285 res = net.url_open(fetch_url, stream=False) | |
|
M-A Ruel
2016/06/06 23:34:50
that assumes the client binary fits in memory, we'
nodir
2016/06/07 18:46:34
I reworked fetch_client_binary to (url_open() | di
| |
| 286 if res is None: | |
| 287 raise Error( | |
| 288 'Could not fetch CIPD client %s:%s: no response' % | |
| 289 (package_name, instance_id)) | |
| 290 return res | |
| 291 | |
| 292 | |
| 293 def get_client( | |
| 294 service_url, package_name, version, client_cache, version_cache=None, | |
| 295 timeout=None): | |
| 296 """Creates a CipdClient. A blocking call. | |
| 297 | |
| 298 The returned client points to a file in client_cache, thus client_cache | |
| 299 should not be mutated while CipdClient is used, otherwise the binary may be | |
| 300 deleted. | |
| 301 | |
| 302 Args: | |
| 303 service_url (str): URL of the CIPD backend. | |
| 304 package_name (str): package name template of the CIPD client. | |
| 305 version (str): version of CIPD client package. | |
| 306 client_cache (isolateserver.DiskCache): cache for client binaries. | |
| 307 version_cache (isolateserver.LocalCache): if not None, cache for | |
| 308 {client version -> instance id} mapping. | |
| 309 timeout (int): if not None, timeout in seconds for this function. | |
| 310 | |
| 311 Returns: | |
| 312 CipdClient. | |
| 313 | |
| 314 Raises: | |
| 315 Error if CIPD client version cannot be resolved or client cannot be fetched. | |
| 316 """ | |
| 317 timeouter = tools.Timeouter(timeout) | |
| 318 assert isinstance(client_cache, isolateserver.DiskCache) | |
| 319 | |
| 320 package_name = render_package_name_template(package_name) | |
| 321 | |
| 322 # Resolve version to instance id. | |
| 323 # Is it an instance id already? They look like HEX SHA1. | |
| 324 if isolated_format.is_valid_hash(version, hashlib.sha1): | |
| 325 instance_id = version | |
| 326 else: | |
| 327 instance_id = None | |
| 328 version_digest = None | |
| 329 if version_cache: | |
| 330 # Convert |version| to a string that may be used as a filename in disk | |
| 331 # cache by hashing it. | |
| 332 sha1 = hashlib.sha1() | |
| 333 sha1.update(version) | |
| 334 version_digest = sha1.hexdigest() | |
|
M-A Ruel
2016/06/06 23:34:50
version_digest = hashlib.sha1(version).hexdigest()
nodir
2016/06/07 18:46:35
Done.
| |
| 335 try: | |
| 336 instance_id = version_cache.read(version_digest) | |
| 337 except isolateserver.CacheMiss: | |
| 338 pass | |
| 339 if not instance_id: | |
| 340 instance_id = resolve_version( | |
| 341 service_url, package_name, version, timeout=timeouter.left()) | |
| 342 if version_cache: | |
| 343 version_cache.write(version_digest, instance_id) | |
| 344 | |
| 345 # Get the path to the client binary inside client cache. | |
| 346 try: | |
| 347 binary_path = client_cache.item_path(instance_id) | |
| 348 except isolateserver.CacheMiss: | |
| 349 logging.info('Fetching CIPD client %s:%s', package_name, instance_id) | |
| 350 res = fetch_client_binary( | |
| 351 service_url, package_name, instance_id, timeout=timeouter.left()) | |
| 352 client_cache.write(instance_id, res.iter_content(64 * 1024)) | |
|
M-A Ruel
2016/06/06 23:34:50
iter_content() doesn't matter here since stream=Fa
nodir
2016/06/07 18:46:34
changed to streaming
| |
| 353 binary_path = client_cache.item_path(instance_id) | |
| 354 fs.chmod(binary_path, 0711) # -rwx--x--x | |
| 355 logging.info('Fetched CIPD client into %s', binary_path) | |
| 356 return CipdClient(binary_path) | |
| OLD | NEW |