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

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

Powered by Google App Engine
This is Rietveld 408576698