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

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

Powered by Google App Engine
This is Rietveld 408576698