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

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: Automatically hook up path in Go bootstrap env. 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
« no previous file with comments | « bootstrap/cipd/cipd_linux_amd64.txt ('k') | go/bootstrap.py » ('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 json
9 import logging
10 import os
11 import platform
12 import stat
13 import subprocess
14 import sys
15 import tempfile
16 import time
17 import urllib
18 import urllib2
19
20
21 # The path to the "infra/bootstrap/" directory.
22 BOOTSTRAP_DIR = os.path.dirname(os.path.abspath(__file__))
23 # The path to the "infra/" directory.
24 ROOT = os.path.dirname(BOOTSTRAP_DIR)
25 # The path where CIPD install lists are stored.
26 CIPD_LIST_DIR = os.path.join(BOOTSTRAP_DIR, 'cipd')
27 # Default binary install root.
28 DEFAULT_INSTALL_ROOT = os.path.join(ROOT, 'bin')
29
30 # The default URL of the CIPD backend service.
31 CIPD_BACKEND_URL = 'https://chrome-infra-packages.appspot.com'
32
33 # Map of CIPD configuration based on the current architecture/platform. If a
34 # platform is not listed here, the bootstrap will be a no-op.
35 #
36 # This is keyed on the platform's (system, machine).
37 ARCH_CONFIG_MAP = {
38 ('Linux', 'x86_64'): {
39 'cipd_package': 'infra/tools/cipd/linux-amd64',
40 'cipd_package_version': '99439270562c887c887b7538ed634ed3a3c0dc83',
Vadim Sh. 2015/09/24 16:50:32 since you are using resolve_cipd_instance_id you c
dnj 2015/09/24 17:19:35 Ah okay. I'll still use the raw instance ID to avo
41 'cipd_install_lists': [
42 'cipd_linux_amd64.txt',
43 ],
44 },
45 }
46
47
48 class CipdError(Exception):
49 """Raised by install_cipd_client on fatal error."""
50
51
52 def get_platform_config():
53 key = (platform.system(), platform.machine())
54 return key, ARCH_CONFIG_MAP.get(key)
55
56
57 def dump_json(obj):
58 """Pretty-formats object to JSON."""
59 return json.dumps(obj, indent=2, sort_keys=True, separators=(',',':'))
60
61
62 def ensure_directory(path):
63 # Ensure the parent directory exists.
64 if os.path.isdir(path):
65 return
66 if os.path.exists(path):
67 raise ValueError("Target file's directory [%s] exists, but is not a "
68 "directory." % (path,))
69 logging.debug('Creating directory: [%s]', path)
70 os.makedirs(path)
71
72
73 def write_file(path, data):
Vadim Sh. 2015/09/24 16:50:32 this is Linux only :( On Windows os.rename when 'p
dnj 2015/09/24 17:19:35 Oh good point. I don't really see a need to make t
74 """Puts a file on the disk."""
75 dirname = os.path.dirname(path)
76 ensure_directory(dirname)
77
78 # Write the file.
79 temp_file = None
80 try:
81 fd, temp_file = tempfile.mkstemp(dir=dirname)
82 with os.fdopen(fd, 'w') as f:
83 f.write(data)
84 os.rename(temp_file, path)
85 temp_file = None
86 finally:
87 if temp_file is not None:
88 os.unlink(temp_file)
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 resolve_cipd_instance_id(cipd_backend, package, version):
105 resp = cipd_backend.call_api(
106 'repo/v1/instance/resolve',
107 package_name=package,
108 version=version)
109 return resp['instance_id']
110
111
112 def install_cipd_client(cipd_backend, config, root):
113 package = config['cipd_package']
114 instance_id = resolve_cipd_instance_id(
115 cipd_backend,
116 package,
117 config.get('cipd_package_version', 'latest'))
118 logging.info('Installing CIPD client [%s] ID [%s]', package, instance_id)
119
120 # Is this the version that's already installed?
121 cipd_client = CipdClient(cipd_backend, os.path.join(root, 'cipd'))
122 current = cipd_client.read_tag()
123 if current == (package, instance_id) and os.path.isfile(cipd_client.path):
124 logging.info('CIPD client already installed.')
125 return cipd_client
126
127 # Get the client binary URL.
128 client_info = cipd_backend.call_api(
129 'repo/v1/client',
130 package_name=package,
131 instance_id=instance_id)
132 logging.info('CIPD client binary info:\n%s', dump_json(client_info))
133
134 status, raw_client_data = fetch_url(client_info['client_binary']['fetch_url'])
135 if status != 200:
136 logging.error('Failed to fetch CIPD client binary (HTTP status %d)', status)
137 return None
138
139 digest = hashlib.sha1(raw_client_data).hexdigest()
140 if digest != client_info['client_binary']['sha1']:
141 logging.error('CIPD client hash mismatch (%s != %s)', digest,
142 client_info['client_binary']['sha1'])
143 return None
144
145 write_file(cipd_client.path, raw_client_data)
146 os.chmod(cipd_client.path, 0755)
147 cipd_client.write_tag(package, instance_id)
148 return cipd_client
149
150
151 def execute(*cmd):
152 if not logging.getLogger().isEnabledFor(logging.DEBUG):
153 code = subprocess.call(cmd)
154 else:
155 # Execute the process, passing STDOUT/STDERR through our logger.
156 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
157 stderr=subprocess.STDOUT)
158 for line in proc.stdout:
159 logging.debug('%s: %s', cmd[0], line.rstrip())
160 code = proc.wait()
161 if code:
162 logging.error('Process failed with exit code: %d', code)
163 return code
164
165
166 class CipdBackend(object):
167 def __init__(self, url):
168 self.url = url
169
170 def call_api(self, endpoint, **query):
171 """Sends GET request to CIPD backend, parses JSON response."""
172 url = '%s/_ah/api/%s' % (self.url, endpoint)
173 if query:
174 url += '?' + urllib.urlencode(sorted(query.iteritems()), True)
175 status, body = fetch_url(url)
176 if status != 200:
177 raise CipdError('Server replied with HTTP %d' % status)
178 try:
179 body = json.loads(body)
180 except ValueError:
181 raise CipdError('Server returned invalid JSON')
182 status = body.get('status')
183 if status != 'SUCCESS':
184 m = body.get('error_message') or '<no error message>'
185 raise CipdError('Server replied with error %s: %s' % (status, m))
186 return body
187
188
189 class CipdClient(object):
190
191 # Filename for the CIPD package/instance_id tag.
192 _TAG_NAME = '.cipd_client_version'
193
194 def __init__(self, cipd_backend, path):
195 self.cipd_backend = cipd_backend
196 self.path = path
197
198 def exists(self):
199 return os.path.isfile(self.path)
200
201 def ensure(self, list_path, root):
202 assert os.path.isfile(list_path)
203 assert os.path.isdir(root)
204 self.call(
205 'ensure',
206 '-list', list_path,
207 '-root', root,
208 '-service-url', self.cipd_backend.url)
209
210 def call(self, *args):
211 if execute(self.path, *args):
212 raise CipdError('Failed to execute CIPD client: %s', ' '.join(args))
213
214 def write_tag(self, package, instance_id):
215 write_tag_file(self._cipd_tag_path, {
216 'package': package,
217 'instance_id': instance_id,
218 })
219
220 def read_tag(self):
221 tag = read_tag_file(self._cipd_tag_path)
222 if tag is None:
223 return None, None
224 return tag.get('package'), tag.get('instance_id')
225
226 @property
227 def _cipd_tag_path(self):
228 return os.path.join(os.path.dirname(self.path), self._TAG_NAME)
229
230
231 def fetch_url(url, headers=None):
232 """Sends GET request (with retries).
233 Args:
234 url: URL to fetch.
235 headers: dict with request headers.
236 Returns:
237 (200, reply body) on success.
238 (HTTP code, None) on HTTP 401, 403, or 404 reply.
239 Raises:
240 Whatever urllib2 raises.
241 """
242 req = urllib2.Request(url)
243 req.add_header('User-Agent', 'infra-install-cipd-packages')
244 for k, v in (headers or {}).iteritems():
245 req.add_header(str(k), str(v))
246 i = 0
247 while True:
248 i += 1
249 try:
250 logging.debug('GET %s', url)
251 return 200, urllib2.urlopen(req, timeout=60).read()
Vadim Sh. 2015/09/24 16:50:32 This thing doesn't validate SSL cert :( Consider
dnj 2015/09/24 17:19:35 grosssssssssssss, forgot about that. Done. I had
252 except Exception as e:
253 if isinstance(e, urllib2.HTTPError):
254 logging.error('Failed to fetch %s, server returned HTTP %d', url,
255 e.code)
256 if e.code in (401, 403, 404):
257 return e.code, None
258 else:
259 logging.exception('Failed to fetch %s', url)
260 if i == 20:
261 raise
262 logging.info('Retrying in %d sec.', i)
263 time.sleep(i)
264
265
266 def main(argv):
267 parser = argparse.ArgumentParser('Installs CIPD bootstrap packages.')
268 parser.add_argument('--cipd-backend-url', metavar='URL',
269 default=CIPD_BACKEND_URL,
270 help='Specify the CIPD backend URL (default is %(default)s)')
271 parser.add_argument('-d', '--cipd-root-dir', metavar='PATH',
272 default=DEFAULT_INSTALL_ROOT,
273 help='Specify the root CIPD package installation directory.')
274 parser.add_argument('-v', '--verbose', action='count', default=0,
275 help='Increase logging verbosity. Can be specified multiple times.')
276
277 args = parser.parse_args(argv)
278
279 # Setup logging verbosity.
280 if args.verbose == 0:
281 level = logging.WARNING
282 elif args.verbose == 1:
283 level = logging.INFO
284 else:
285 level = logging.DEBUG
286 logging.getLogger().setLevel(level)
287
288 # Make sure our root directory exists.
289 root = os.path.abspath(args.cipd_root_dir)
290 ensure_directory(root)
291
292 platform_key, config = get_platform_config()
293 if not config:
294 logging.info('No bootstrap configuration for platform [%s].', platform_key)
295 return 0
296
297 cipd_backend = CipdBackend(args.cipd_backend_url)
298 cipd = install_cipd_client(cipd_backend, config, root)
299 if not cipd:
300 logging.error('Failed to install CIPD client.')
301 return 1
302 assert cipd.exists()
303
304 for l in config.get('cipd_install_lists', ()):
305 cipd.ensure(os.path.join(CIPD_LIST_DIR, l), root)
306 return 0
307
308
309 if __name__ == '__main__':
310 logging.basicConfig()
311 logging.getLogger().setLevel(logging.INFO)
312 sys.exit(main(sys.argv[1:]))
OLDNEW
« no previous file with comments | « bootstrap/cipd/cipd_linux_amd64.txt ('k') | go/bootstrap.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698