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

Side by Side Diff: client/cipd.py

Issue 2847153002: Cache/retrieve extracted CIPD packages in local isolate cache (Closed)
Patch Set: Fix unicode glitch and make assertions less terrible to find Created 3 years, 7 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
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
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698