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

Side by Side Diff: bootstrap/install_cipd_packages.py

Issue 1364983002: Add CIPD binary installation to DEPS runhooks. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Fixed cipd command line comment. Created 5 years, 2 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
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright 2015 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 import argparse
7 import hashlib
8 import httplib
9 import json
10 import logging
11 import os
12 import platform
13 import re
14 import socket
15 import ssl
16 import stat
17 import subprocess
18 import sys
19 import tempfile
20 import time
21 import urllib
22 import urllib2
23
24
25 # The path to the "infra/bootstrap/" directory.
26 BOOTSTRAP_DIR = os.path.dirname(os.path.abspath(__file__))
27 # The path to the "infra/" directory.
28 ROOT = os.path.dirname(BOOTSTRAP_DIR)
29 # The path where CIPD install lists are stored.
30 CIPD_LIST_DIR = os.path.join(BOOTSTRAP_DIR, 'cipd')
31 # Default binary install root.
32 DEFAULT_INSTALL_ROOT = os.path.join(ROOT, 'bin')
33
34 # Path to CA certs bundle file to use by default.
35 DEFAULT_CERT_FILE = os.path.join(ROOT, 'data', 'cacert.pem')
36
37 # The default URL of the CIPD backend service.
38 CIPD_BACKEND_URL = 'https://chrome-infra-packages.appspot.com'
39
40 # Regular expression that matches CIPD raw instance IDs.
41 RE_CIPD_INSTANCE_ID = re.compile(r'^[0-9a-f]{40}$')
42
43 # Map of CIPD configuration based on the current architecture/platform. If a
44 # platform is not listed here, the bootstrap will be a no-op.
45 #
46 # This is keyed on the platform's (system, machine).
47 #
48 # It is ideal to use a raw `instance_id` as the version to avoid an unnecessary
49 # CIPD server round-trip lookup. This can be obtained for a given package via:
50 # $ cipd resolve \
51 # infra/tools/cipd/ \
52 # -version=git_revision:ed808139130f0ea8fd52fc64fd9ec47d150a2e49"
53 ARCH_CONFIG_MAP = {
54 ('Linux', 'x86_64'): {
55 'cipd_package': 'infra/tools/cipd/linux-amd64',
56 'cipd_package_version': '99439270562c887c887b7538ed634ed3a3c0dc83',
57 'cipd_install_lists': [
58 'cipd_linux_amd64.txt',
59 ],
60 },
61 }
62
63
64 class CipdError(Exception):
65 """Raised by install_cipd_client on fatal error."""
66
67
68 def get_platform_config():
69 key = (platform.system(), platform.machine())
70 return key, ARCH_CONFIG_MAP.get(key)
71
72
73 def dump_json(obj):
74 """Pretty-formats object to JSON."""
75 return json.dumps(obj, indent=2, sort_keys=True, separators=(',',':'))
76
77
78 def ensure_directory(path):
79 # Ensure the parent directory exists.
80 if os.path.isdir(path):
81 return
82 if os.path.exists(path):
83 raise ValueError("Target file's directory [%s] exists, but is not a "
84 "directory." % (path,))
85 logging.debug('Creating directory: [%s]', path)
86 os.makedirs(path)
87
88
89 def write_binary_file(path, data):
90 """Writes a binary file to the disk."""
91 ensure_directory(os.path.dirname(path))
92 with open(path, 'wb') as fd:
93 fd.write(data)
94
95
96 def write_tag_file(path, obj):
97 with open(path, 'w') as fd:
98 json.dump(obj, fd, sort_keys=True, indent=2)
99
100
101 def read_tag_file(path):
102 try:
103 with open(path, 'r') as fd:
104 return json.load(fd)
105 except (IOError, ValueError):
106 return None
107
108
109 def is_cipd_instance_id(value):
110 return RE_CIPD_INSTANCE_ID.match(value) is not None
111
112
113 def resolve_cipd_instance_id(cipd_backend, package, version):
114 if is_cipd_instance_id(version):
115 return version
116
117 resp = cipd_backend.call_api(
118 'repo/v1/instance/resolve',
119 package_name=package,
120 version=version)
121 return resp['instance_id']
122
123
124 def install_cipd_client(cipd_backend, config, root):
125 package = config['cipd_package']
126 instance_id = resolve_cipd_instance_id(
127 cipd_backend,
128 package,
129 config.get('cipd_package_version', 'latest'))
130 logging.info('Installing CIPD client [%s] ID [%s]', package, instance_id)
131
132 # Is this the version that's already installed?
133 cipd_client = CipdClient(cipd_backend, os.path.join(root, 'cipd'))
134 current = cipd_client.read_tag()
135 if current == (package, instance_id) and os.path.isfile(cipd_client.path):
136 logging.info('CIPD client already installed.')
137 return cipd_client
138
139 # Get the client binary URL.
140 client_info = cipd_backend.call_api(
141 'repo/v1/client',
142 package_name=package,
143 instance_id=instance_id)
144 logging.info('CIPD client binary info:\n%s', dump_json(client_info))
145
146 status, raw_client_data = fetch_url(client_info['client_binary']['fetch_url'])
147 if status != 200:
148 logging.error('Failed to fetch CIPD client binary (HTTP status %d)', status)
149 return None
150
151 digest = hashlib.sha1(raw_client_data).hexdigest()
152 if digest != client_info['client_binary']['sha1']:
153 logging.error('CIPD client hash mismatch (%s != %s)', digest,
154 client_info['client_binary']['sha1'])
155 return None
156
157 write_binary_file(cipd_client.path, raw_client_data)
158 os.chmod(cipd_client.path, 0755)
159 cipd_client.write_tag(package, instance_id)
160 return cipd_client
161
162
163 def execute(*cmd):
164 if not logging.getLogger().isEnabledFor(logging.DEBUG):
165 code = subprocess.call(cmd)
166 else:
167 # Execute the process, passing STDOUT/STDERR through our logger.
168 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
169 stderr=subprocess.STDOUT)
170 for line in proc.stdout:
171 logging.debug('%s: %s', cmd[0], line.rstrip())
172 code = proc.wait()
173 if code:
174 logging.error('Process failed with exit code: %d', code)
175 return code
176
177
178 class CipdBackend(object):
179 def __init__(self, url):
180 self.url = url
181
182 def call_api(self, endpoint, **query):
183 """Sends GET request to CIPD backend, parses JSON response."""
184 url = '%s/_ah/api/%s' % (self.url, endpoint)
185 if query:
186 url += '?' + urllib.urlencode(sorted(query.iteritems()), True)
187 status, body = fetch_url(url)
188 if status != 200:
189 raise CipdError('Server replied with HTTP %d' % status)
190 try:
191 body = json.loads(body)
192 except ValueError:
193 raise CipdError('Server returned invalid JSON')
194 status = body.get('status')
195 if status != 'SUCCESS':
196 m = body.get('error_message') or '<no error message>'
197 raise CipdError('Server replied with error %s: %s' % (status, m))
198 return body
199
200
201 class CipdClient(object):
202
203 # Filename for the CIPD package/instance_id tag.
204 _TAG_NAME = '.cipd_client_version'
205
206 def __init__(self, cipd_backend, path):
207 self.cipd_backend = cipd_backend
208 self.path = path
209
210 def exists(self):
211 return os.path.isfile(self.path)
212
213 def ensure(self, list_path, root):
214 assert os.path.isfile(list_path)
215 assert os.path.isdir(root)
216 self.call(
217 'ensure',
218 '-list', list_path,
219 '-root', root,
220 '-service-url', self.cipd_backend.url)
221
222 def call(self, *args):
223 if execute(self.path, *args):
224 raise CipdError('Failed to execute CIPD client: %s', ' '.join(args))
225
226 def write_tag(self, package, instance_id):
227 write_tag_file(self._cipd_tag_path, {
228 'package': package,
229 'instance_id': instance_id,
230 })
231
232 def read_tag(self):
233 tag = read_tag_file(self._cipd_tag_path)
234 if tag is None:
235 return None, None
236 return tag.get('package'), tag.get('instance_id')
237
238 @property
239 def _cipd_tag_path(self):
240 return os.path.join(os.path.dirname(self.path), self._TAG_NAME)
241
242
243 def fetch_url(url, headers=None):
244 """Sends GET request (with retries).
245 Args:
246 url: URL to fetch.
247 headers: dict with request headers.
248 Returns:
249 (200, reply body) on success.
250 (HTTP code, None) on HTTP 401, 403, or 404 reply.
251 Raises:
252 Whatever urllib2 raises.
253 """
254 req = urllib2.Request(url)
255 req.add_header('User-Agent', 'infra-install-cipd-packages')
256 for k, v in (headers or {}).iteritems():
257 req.add_header(str(k), str(v))
258 i = 0
259 while True:
260 i += 1
261 try:
262 logging.debug('GET %s', url)
263 return 200, urllib2.urlopen(req, timeout=60).read()
264 except Exception as e:
265 if isinstance(e, urllib2.HTTPError):
266 logging.error('Failed to fetch %s, server returned HTTP %d', url,
267 e.code)
268 if e.code in (401, 403, 404):
269 return e.code, None
270 else:
271 logging.exception('Failed to fetch %s', url)
272 if i == 20:
273 raise
274 logging.info('Retrying in %d sec.', i)
275 time.sleep(i)
276
277
278 def setup_urllib2_ssl(cacert):
279 """Configures urllib2 to validate SSL certs.
280 See http://stackoverflow.com/a/14320202/3817699.
281 """
282 cacert = os.path.abspath(cacert)
283 assert os.path.isfile(cacert)
284
285 class ValidHTTPSConnection(httplib.HTTPConnection):
286 default_port = httplib.HTTPS_PORT
287 def __init__(self, *args, **kwargs):
288 httplib.HTTPConnection.__init__(self, *args, **kwargs)
289 def connect(self):
290 sock = socket.create_connection(
291 (self.host, self.port), self.timeout, self.source_address)
292 if self._tunnel_host:
293 self.sock = sock
294 self._tunnel()
295 self.sock = ssl.wrap_socket(
296 sock, ca_certs=cacert, cert_reqs=ssl.CERT_REQUIRED)
297 class ValidHTTPSHandler(urllib2.HTTPSHandler):
298 def https_open(self, req):
299 return self.do_open(ValidHTTPSConnection, req)
300 urllib2.install_opener(urllib2.build_opener(ValidHTTPSHandler))
301
302
303 def main(argv):
304 parser = argparse.ArgumentParser('Installs CIPD bootstrap packages.')
305 parser.add_argument('-v', '--verbose', action='count', default=0,
306 help='Increase logging verbosity. Can be specified multiple times.')
307 parser.add_argument('--cipd-backend-url', metavar='URL',
308 default=CIPD_BACKEND_URL,
309 help='Specify the CIPD backend URL (default is %(default)s)')
310 parser.add_argument('-d', '--cipd-root-dir', metavar='PATH',
311 default=DEFAULT_INSTALL_ROOT,
312 help='Specify the root CIPD package installation directory.')
313 parser.add_argument('--cacert', metavar='PATH', default=DEFAULT_CERT_FILE,
314 help='Path to cacert.pem file with CA root certificates bundle (default '
315 'is %(default)s)')
316
317 opts = parser.parse_args(argv)
318
319 # Setup logging verbosity.
320 if opts.verbose == 0:
321 level = logging.WARNING
322 elif opts.verbose == 1:
323 level = logging.INFO
324 else:
325 level = logging.DEBUG
326 logging.getLogger().setLevel(level)
327
328 # Configure `urllib2` to validate SSL certificates.
329 logging.debug('CA certs bundle: %s', opts.cacert)
330 setup_urllib2_ssl(opts.cacert)
331
332 # Make sure our root directory exists.
333 root = os.path.abspath(opts.cipd_root_dir)
334 ensure_directory(root)
335
336 platform_key, config = get_platform_config()
337 if not config:
338 logging.info('No bootstrap configuration for platform [%s].', platform_key)
339 return 0
340
341 cipd_backend = CipdBackend(opts.cipd_backend_url)
342 cipd = install_cipd_client(cipd_backend, config, root)
343 if not cipd:
344 logging.error('Failed to install CIPD client.')
345 return 1
346 assert cipd.exists()
347
348 for l in config.get('cipd_install_lists', ()):
349 cipd.ensure(os.path.join(CIPD_LIST_DIR, l), root)
Vadim Sh. 2015/09/24 18:20:29 actually that will probably not work as you expect
dnj 2015/09/24 18:23:04 Oh I see, so one list file per CIPD root? Currentl
350 return 0
351
352
353 if __name__ == '__main__':
354 logging.basicConfig()
355 logging.getLogger().setLevel(logging.INFO)
356 sys.exit(main(sys.argv[1:]))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698