OLD | NEW |
---|---|
(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:])) | |
OLD | NEW |