Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(183)

Side by Side Diff: client/cipd.py

Issue 2069903003: swarming: custom cipd package paths (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@cipd-win
Patch Set: fix _validate_cipd_path Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « appengine/swarming/test_env_handlers.py ('k') | client/isolateserver.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
OLDNEW
« no previous file with comments | « appengine/swarming/test_env_handlers.py ('k') | client/isolateserver.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698