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

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

Powered by Google App Engine
This is Rietveld 408576698