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

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