OLD | NEW |
---|---|
1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
4 | 4 |
5 """Fetches CIPD client and installs packages.""" | 5 """Fetches CIPD client and installs packages.""" |
6 | 6 |
7 import contextlib | 7 import contextlib |
8 import hashlib | 8 import hashlib |
9 import json | 9 import json |
10 import logging | 10 import logging |
11 import optparse | 11 import optparse |
12 import os | 12 import os |
13 import platform | 13 import platform |
14 import re | |
15 import shutil | |
14 import sys | 16 import sys |
15 import tempfile | 17 import tempfile |
16 import time | 18 import time |
17 import urllib | 19 import urllib |
18 | 20 |
19 from utils import file_path | 21 from utils import file_path |
20 from utils import fs | 22 from utils import fs |
21 from utils import net | 23 from utils import net |
22 from utils import subprocess42 | 24 from utils import subprocess42 |
23 from utils import tools | 25 from utils import tools |
26 import isolate | |
kjlubick
2017/05/03 18:28:33
This introduces a dependency cycle that I have no
| |
24 import isolated_format | 27 import isolated_format |
25 import isolateserver | 28 import isolateserver |
26 | 29 |
27 | 30 |
28 # .exe on Windows. | 31 # .exe on Windows. |
29 EXECUTABLE_SUFFIX = '.exe' if sys.platform == 'win32' else '' | 32 EXECUTABLE_SUFFIX = '.exe' if sys.platform == 'win32' else '' |
30 | 33 |
31 | 34 |
32 if sys.platform == 'win32': | 35 if sys.platform == 'win32': |
33 def _ensure_batfile(client_path): | 36 def _ensure_batfile(client_path): |
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
133 package_name (str): the CIPD package name for the client itself. | 136 package_name (str): the CIPD package name for the client itself. |
134 instance_id (str): the CIPD instance_id for the client itself. | 137 instance_id (str): the CIPD instance_id for the client itself. |
135 service_url (str): if not None, URL of the CIPD backend that overrides | 138 service_url (str): if not None, URL of the CIPD backend that overrides |
136 the default one. | 139 the default one. |
137 """ | 140 """ |
138 self.binary_path = binary_path | 141 self.binary_path = binary_path |
139 self.package_name = package_name | 142 self.package_name = package_name |
140 self.instance_id = instance_id | 143 self.instance_id = instance_id |
141 self.service_url = service_url | 144 self.service_url = service_url |
142 | 145 |
146 def _ensure_from_isolate(self, root, subdir, isolated_cipd, isolate_cache): | |
147 if not isolate_cache: | |
148 logging.info('Not ensuring cipd from isolate cache isolate_cache is not' | |
149 'defined: %s', isolate_cache) | |
150 return False | |
151 try: | |
152 with open(isolated_cipd , 'r') as f: | |
153 digest = str(f.read()) | |
154 try: | |
155 content = isolate_cache.getfileobj(digest).read() | |
156 except Exception as e: | |
157 logging.warning('Could not find isolated file in cache with digest ' | |
158 '%s: %s', digest, e) | |
159 return False | |
160 | |
161 #FIXME(kjlubick): Don't assume sha1 | |
162 sha_1 = isolated_format.SUPPORTED_ALGOS['sha-1'] | |
163 ifile = isolated_format.IsolatedFile(digest, sha_1) | |
164 ifile.load(content) | |
165 | |
166 subdir = os.path.join(root, subdir) | |
167 file_path.ensure_tree(subdir) | |
168 files = ifile.data.get(u'files', {}) | |
169 for f in files.keys(): | |
170 props = files.get(f, None) | |
171 if not props: | |
172 logging.warning('Problem getting info for %s', f) | |
173 return False | |
174 file_mode = props.get('m', None) | |
175 if file_mode: | |
176 # Ignore all bits apart from the user | |
177 file_mode &= 0700 | |
178 | |
179 dstpath = os.path.join(subdir, f) | |
180 file_path.ensure_tree(os.path.dirname(dstpath)) | |
181 digest = props.get('h', None) | |
182 if not digest: | |
183 logging.warning('Hash can\'t be empty %s', f) | |
184 return False | |
185 srcpath = isolate_cache.getfileobj(digest).name | |
186 | |
187 file_path.link_file(unicode(dstpath), unicode(srcpath), | |
188 file_path.HARDLINK_WITH_FALLBACK) | |
189 | |
190 if file_mode is not None: | |
191 fs.chmod(dstpath, file_mode) | |
192 except Exception as e: | |
193 logging.warning('Could not ensure cipd package from isolate %s', e) | |
194 | |
195 return True | |
196 | |
197 | |
198 def _isolate_cipd(self, root, packages, isolate_cache, cipd_cache): | |
199 logging.debug('_isolate_cipd(%s, %s, %s, %s)', root, packages, | |
200 isolate_cache, cipd_cache) | |
201 if not isolate_cache or not os.path.isdir(cipd_cache): | |
202 logging.info('Not putting cipd into isolate cache because one of the' | |
203 'caches is empty: %s, %s', isolate_cache, cipd_cache) | |
204 return | |
205 for subdir, shafile in packages.iteritems(): | |
206 isolated_file = os.path.join(cipd_cache, shafile[:-5]) | |
207 complete_state = isolate.CompleteState.load_files(isolated_file) | |
208 | |
209 subdir = os.path.join(root, subdir) | |
210 | |
211 infiles = isolated_format.expand_directories_and_symlinks( | |
212 subdir, ['./'], [], False, True) | |
213 | |
214 complete_state.saved_state.update_isolated(None, infiles, True, None) | |
215 complete_state.saved_state.root_dir = subdir | |
216 # Collapse symlinks to remove CIPD symlinking AND to simplify other code. | |
217 complete_state.files_to_metadata('', True) | |
218 complete_state.save_files() | |
219 | |
220 for infile in infiles: | |
221 digest = complete_state.saved_state.files[infile].get('h', '') | |
222 if not digest: | |
223 logging.warning('No digest found in saved state %s:%s', infile, | |
224 complete_state.saved_state.files[infile]) | |
225 continue | |
226 with open(os.path.join(subdir, infile) , 'r') as f: | |
227 isolate_cache.write(digest, f) | |
228 | |
229 with open(isolated_file , 'r') as f: | |
230 content = f.read() | |
231 digest = complete_state.saved_state.algo(content).hexdigest() | |
232 isolate_cache.write(digest, content) | |
233 | |
234 with open(os.path.join(cipd_cache, shafile), 'w') as sf: | |
235 sf.write(digest) | |
236 | |
237 | |
238 | |
143 def ensure( | 239 def ensure( |
144 self, site_root, packages, cache_dir=None, tmp_dir=None, timeout=None): | 240 self, site_root, packages, cache_dir=None, tmp_dir=None, timeout=None, |
241 isolate_cache=None): | |
145 """Ensures that packages installed in |site_root| equals |packages| set. | 242 """Ensures that packages installed in |site_root| equals |packages| set. |
146 | 243 |
147 Blocking call. | 244 Blocking call. |
148 | 245 |
246 Attempts to use the isolate cache to store the unzipped cipd files, keeping | |
247 a .isolated file in the cipd cache_dir | |
248 | |
149 Args: | 249 Args: |
150 site_root (str): where to install packages. | 250 site_root (str): where to install packages. |
151 packages: dict of subdir -> list of (package_template, version) tuples. | 251 packages: dict of subdir -> list of (package_template, version) tuples. |
152 cache_dir (str): if set, cache dir for cipd binary own cache. | 252 cache_dir (str): if set, cache dir for cipd binary own cache. |
153 Typically contains packages and tags. | 253 Typically contains packages and tags. |
154 tmp_dir (str): if not None, dir for temp files. | 254 tmp_dir (str): if not None, dir for temp files. |
155 timeout (int): if not None, timeout in seconds for this function to run. | 255 timeout (int): if not None, timeout in seconds for this function to run. |
156 | 256 |
157 Returns: | 257 Returns: |
158 Pinned packages in the form of {subdir: [(package_name, package_id)]}, | 258 Pinned packages in the form of {subdir: [(package_name, package_id)]}, |
159 which correspond 1:1 with the input packages argument. | 259 which correspond 1:1 with the input packages argument. |
160 | 260 |
161 Raises: | 261 Raises: |
162 Error if could not install packages or timed out. | 262 Error if could not install packages or timed out. |
163 """ | 263 """ |
164 timeoutfn = tools.sliding_timeout(timeout) | 264 timeoutfn = tools.sliding_timeout(timeout) |
165 logging.info('Installing packages %r into %s', packages, site_root) | 265 logging.info('Installing packages %r into %s', packages, site_root) |
266 logging.info('Cache dir %s', cache_dir) | |
166 | 267 |
167 ensure_file_handle, ensure_file_path = tempfile.mkstemp( | 268 ensure_file_handle, ensure_file_path = tempfile.mkstemp( |
168 dir=tmp_dir, prefix=u'cipd-ensure-file-', suffix='.txt') | 269 dir=tmp_dir, prefix=u'cipd-ensure-file-', suffix='.txt') |
169 json_out_file_handle, json_file_path = tempfile.mkstemp( | 270 json_out_file_handle, json_file_path = tempfile.mkstemp( |
170 dir=tmp_dir, prefix=u'cipd-ensure-result-', suffix='.json') | 271 dir=tmp_dir, prefix=u'cipd-ensure-result-', suffix='.json') |
171 os.close(json_out_file_handle) | 272 os.close(json_out_file_handle) |
172 | 273 if cache_dir: |
274 file_path.ensure_tree(unicode(cache_dir)) | |
275 to_isolate = {} | |
276 from_isolate = {} | |
173 try: | 277 try: |
174 try: | 278 try: |
175 for subdir, pkgs in sorted(packages.iteritems()): | 279 for subdir, pkgs in sorted(packages.iteritems()): |
176 if '\n' in subdir: | 280 if '\n' in subdir: |
177 raise Error( | 281 raise Error( |
178 'Could not install packages; subdir %r contains newline' % subdir) | 282 'Could not install packages; subdir %r contains newline' % subdir) |
283 | |
284 versions = [p[1] for p in pkgs] | |
285 isolated_cipd = '%s.%s.isolated.sha1' % (subdir, | |
286 '_'.join(versions)) | |
287 abs_isolated_cipd = os.path.join(cache_dir, isolated_cipd) | |
288 if (os.path.isfile(abs_isolated_cipd) and | |
289 self._ensure_from_isolate(site_root, subdir, abs_isolated_cipd, | |
290 isolate_cache)): | |
291 from_isolate[unicode(subdir)] = pkgs | |
292 continue | |
293 to_isolate[subdir] = isolated_cipd | |
179 os.write(ensure_file_handle, '@Subdir %s\n' % (subdir,)) | 294 os.write(ensure_file_handle, '@Subdir %s\n' % (subdir,)) |
180 for pkg, version in pkgs: | 295 for pkg, version in pkgs: |
181 pkg = render_package_name_template(pkg) | 296 pkg = render_package_name_template(pkg) |
182 os.write(ensure_file_handle, '%s %s\n' % (pkg, version)) | 297 os.write(ensure_file_handle, '%s %s\n' % (pkg, version)) |
298 | |
183 finally: | 299 finally: |
184 os.close(ensure_file_handle) | 300 os.close(ensure_file_handle) |
185 | 301 |
302 # to_isolate is the packages that we need to ensure from CIPD and then | |
303 # isolate. Thus, if this is empty, we don't need to get anything from | |
304 # CIPD because they were successfully pulled from isolate. Thus return | |
305 # from_isolate, the pinned packages that we pulled from_isolate | |
306 if not to_isolate: | |
307 return from_isolate | |
308 | |
186 cmd = [ | 309 cmd = [ |
187 self.binary_path, 'ensure', | 310 self.binary_path, 'ensure', |
188 '-root', site_root, | 311 '-root', site_root, |
189 '-ensure-file', ensure_file_path, | 312 '-ensure-file', ensure_file_path, |
190 '-verbose', # this is safe because cipd-ensure does not print a lot | 313 '-verbose', # this is safe because cipd-ensure does not print a lot |
191 '-json-output', json_file_path, | 314 '-json-output', json_file_path, |
192 ] | 315 ] |
193 if cache_dir: | 316 if cache_dir: |
194 cmd += ['-cache-dir', cache_dir] | 317 cmd += ['-cache-dir', cache_dir] |
195 if self.service_url: | 318 if self.service_url: |
(...skipping 16 matching lines...) Expand all Loading... | |
212 if pipe_name == 'stderr': | 335 if pipe_name == 'stderr': |
213 logging.debug('cipd client: %s', line) | 336 logging.debug('cipd client: %s', line) |
214 else: | 337 else: |
215 logging.info('cipd client: %s', line) | 338 logging.info('cipd client: %s', line) |
216 | 339 |
217 exit_code = process.wait(timeout=timeoutfn()) | 340 exit_code = process.wait(timeout=timeoutfn()) |
218 if exit_code != 0: | 341 if exit_code != 0: |
219 raise Error( | 342 raise Error( |
220 'Could not install packages; exit code %d\noutput:%s' % ( | 343 'Could not install packages; exit code %d\noutput:%s' % ( |
221 exit_code, '\n'.join(output))) | 344 exit_code, '\n'.join(output))) |
345 | |
346 self._isolate_cipd(site_root, to_isolate, isolate_cache, cache_dir) | |
347 | |
222 with open(json_file_path) as jfile: | 348 with open(json_file_path) as jfile: |
223 result_json = json.load(jfile) | 349 result_json = json.load(jfile) |
224 return { | 350 from_isolate.update({ |
225 subdir: [(x['package'], x['instance_id']) for x in pins] | 351 subdir: [(x['package'], x['instance_id']) for x in pins] |
226 for subdir, pins in result_json['result'].iteritems() | 352 for subdir, pins in result_json['result'].iteritems() |
227 } | 353 }) |
354 return from_isolate | |
228 finally: | 355 finally: |
229 fs.remove(ensure_file_path) | 356 fs.remove(ensure_file_path) |
230 fs.remove(json_file_path) | 357 fs.remove(json_file_path) |
231 | 358 |
232 | 359 |
233 def get_platform(): | 360 def get_platform(): |
234 """Returns ${platform} parameter value. | 361 """Returns ${platform} parameter value. |
235 | 362 |
236 Borrowed from | 363 Borrowed from |
237 https://chromium.googlesource.com/infra/infra/+/aaf9586/build/build.py#204 | 364 https://chromium.googlesource.com/infra/infra/+/aaf9586/build/build.py#204 |
(...skipping 234 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
472 """ | 599 """ |
473 result = [] | 600 result = [] |
474 for pkg in packages: | 601 for pkg in packages: |
475 path, name, version = pkg.split(':', 2) | 602 path, name, version = pkg.split(':', 2) |
476 if not name: | 603 if not name: |
477 raise Error('Invalid package "%s": package name is not specified' % pkg) | 604 raise Error('Invalid package "%s": package name is not specified' % pkg) |
478 if not version: | 605 if not version: |
479 raise Error('Invalid package "%s": version is not specified' % pkg) | 606 raise Error('Invalid package "%s": version is not specified' % pkg) |
480 result.append((path, name, version)) | 607 result.append((path, name, version)) |
481 return result | 608 return result |
OLD | NEW |