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

Side by Side Diff: scripts/slave/annotated_run.py

Issue 1468053008: Add LogDog bootstrapping to `annotated_run.py`. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Fixes? Created 5 years 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 | Annotate | Revision Log
« no previous file with comments | « no previous file | scripts/slave/gce.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 import argparse 6 import argparse
7 import collections 7 import collections
8 import contextlib 8 import contextlib
9 import json 9 import json
10 import logging 10 import logging
11 import os 11 import os
12 import platform 12 import platform
13 import shutil 13 import shutil
14 import socket 14 import socket
15 import subprocess 15 import subprocess
16 import sys 16 import sys
17 import tempfile 17 import tempfile
18 18
19 19
20 # Install Infra build environment. 20 # Install Infra build environment.
21 BUILD_ROOT = os.path.dirname(os.path.dirname(os.path.dirname( 21 BUILD_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
22 os.path.abspath(__file__)))) 22 os.path.abspath(__file__))))
23 sys.path.insert(0, os.path.join(BUILD_ROOT, 'scripts')) 23 sys.path.insert(0, os.path.join(BUILD_ROOT, 'scripts'))
24 24
25 from common import annotator 25 from common import annotator
26 from common import chromium_utils 26 from common import chromium_utils
27 from common import env 27 from common import env
28 from common import master_cfg_utils 28 from common import master_cfg_utils
29 from slave import gce
29 30
30 # Logging instance. 31 # Logging instance.
31 LOGGER = logging.getLogger('annotated_run') 32 LOGGER = logging.getLogger('annotated_run')
32 33
34 # Return codes used by Butler/Annotee to indicate their failure (as opposed to
35 # a forwarded return code from the underlying process).
36 LOGDOG_ERROR_RETURNCODES = (
37 # Butler runtime error.
38 250,
39 # Annotee runtime error.
40 251,
41 )
42
43 # Whitelist of {master}=>[{builder}|WHITELIST_ALL] whitelisting specific masters
44 # and builders for experimental LogDog/Annotee export.
45 LOGDOG_WHITELIST_MASTER_BUILDERS = {
46 }
47
48 # Sentinel value that, if present in master config, matches all builders
49 # underneath that master.
50 WHITELIST_ALL = '*'
51
52 # Configuration for a Pub/Sub topic.
53 PubSubConfig = collections.namedtuple('PubSubConfig', ('project', 'topic'))
33 54
34 # RecipeRuntime will probe this for values. 55 # RecipeRuntime will probe this for values.
35 # - First, (system, platform) 56 # - First, (system, platform)
36 # - Then, (system,) 57 # - Then, (system,)
37 # - Finally, (), 58 # - Finally, (),
38 PLATFORM_CONFIG = { 59 PLATFORM_CONFIG = {
39 # All systems. 60 # All systems.
40 (): {}, 61 (): {
62 'logdog_pubsub': PubSubConfig(
63 project='luci-logdog',
64 topic='chrome-infra-beta',
65 ),
66 },
41 67
42 # Linux 68 # Linux
43 ('Linux',): { 69 ('Linux',): {
44 'run_cmd': ['/opt/infra-python/run.py'], 70 'run_cmd': ['/opt/infra-python/run.py'],
71 'credential_paths': (
72 # XXX: Get this right?
73 '/opt/infra/service_accounts',
74 ),
75 'logdog_butler_streamserver_gen': lambda d: os.path.join(d, 'butler.sock'),
45 }, 76 },
46 77
47 # Mac OSX 78 # Mac OSX
48 ('Darwin',): { 79 ('Darwin',): {
49 'run_cmd': ['/opt/infra-python/run.py'], 80 'run_cmd': ['/opt/infra-python/run.py'],
50 }, 81 },
51 82
52 # Windows 83 # Windows
53 ('Windows',): { 84 ('Windows',): {
54 'run_cmd': ['C:\\infra-python\\ENV\\Scripts\\python.exe', 85 'run_cmd': ['C:\\infra-python\\ENV\\Scripts\\python.exe',
55 'C:\\infra-python\\run.py'], 86 'C:\\infra-python\\run.py'],
56 }, 87 },
57 } 88 }
58 89
59 90
60 # Config is the runtime configuration used by `annotated_run.py` to bootstrap 91 # Config is the runtime configuration used by `annotated_run.py` to bootstrap
61 # the recipe engine. 92 # the recipe engine.
62 Config = collections.namedtuple('Config', ( 93 Config = collections.namedtuple('Config', (
63 'run_cmd', 94 'run_cmd',
95 'logdog_pubsub',
96 'logdog_butler_streamserver_gen',
97 'credential_paths',
64 )) 98 ))
65 99
66 100
67 def get_config(): 101 def get_config():
68 """Returns (Config): The constructed Config object. 102 """Returns (Config): The constructed Config object.
69 103
70 The Config object is constructed from: 104 The Config object is constructed from:
71 - Cascading the PLATFORM_CONFIG fields together based on current 105 - Cascading the PLATFORM_CONFIG fields together based on current
72 OS/Architecture. 106 OS/Architecture.
73 107
74 Raises: 108 Raises:
75 KeyError: if a required configuration key/parameter is not available. 109 KeyError: if a required configuration key/parameter is not available.
76 """ 110 """
77 # Cascade the platform configuration. 111 # Cascade the platform configuration.
78 p = (platform.system(), platform.processor()) 112 p = (platform.system(), platform.processor())
79 platform_config = {} 113 platform_config = {}
80 for i in xrange(len(p)+1): 114 for i in xrange(len(p)+1):
81 platform_config.update(PLATFORM_CONFIG.get(p[:i], {})) 115 platform_config.update(PLATFORM_CONFIG.get(p[:i], {}))
82 116
83 # Construct runtime configuration. 117 # Construct runtime configuration.
84 return Config( 118 return Config(
85 run_cmd=platform_config.get('run_cmd'), 119 run_cmd=platform_config.get('run_cmd'),
120 logdog_pubsub=platform_config.get('logdog_pubsub'),
121 logdog_butler_streamserver_gen=platform_config.get(
122 'logdog_butler_streamserver_gen'),
123 credential_paths=platform_config.get('credential_paths', ()),
86 ) 124 )
87 125
88 126
89 def ensure_directory(*path): 127 def ensure_directory(*path):
90 path = os.path.join(*path) 128 path = os.path.join(*path)
91 if not os.path.isdir(path): 129 if not os.path.isdir(path):
92 os.makedirs(path) 130 os.makedirs(path)
93 return path 131 return path
94 132
95 133
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
137 LOGGER.debug('Cleaning up temporary directory [%s].', basedir) 175 LOGGER.debug('Cleaning up temporary directory [%s].', basedir)
138 try: 176 try:
139 chromium_utils.RemoveDirectory(basedir) 177 chromium_utils.RemoveDirectory(basedir)
140 except Exception: 178 except Exception:
141 LOGGER.exception('Failed to clean up temporary directory [%s].', 179 LOGGER.exception('Failed to clean up temporary directory [%s].',
142 basedir) 180 basedir)
143 else: 181 else:
144 LOGGER.warning('(--leak) Leaking temporary directory [%s].', basedir) 182 LOGGER.warning('(--leak) Leaking temporary directory [%s].', basedir)
145 183
146 184
185 class LogDogNotBootstrapped(Exception):
186 pass
187
188
189 class LogDogBootstrapError(Exception):
190 pass
191
192
193 def is_executable(path):
194 return os.path.isfile(path) and os.access(path, os.X_OK)
195
196
197 def ensure_directory(*path):
198 path = os.path.join(*path)
199 if not os.path.isdir(path):
200 os.makedirs(path)
201 return path
202
203
204 def _run_command(cmd, **kwargs):
205 dry_run = kwargs.pop('dry_run', False)
206
207 LOGGER.debug('Executing command: %s', cmd)
208 if dry_run:
209 LOGGER.info('(Dry Run) Not executing command.')
210 return 0, ''
211 proc = subprocess.Popen(cmd, stderr=subprocess.STDOUT)
212 stdout, _ = proc.communicate()
213
214 LOGGER.debug('Process [%s] returned [%d] with output:\n%s',
215 cmd, proc.returncode, stdout)
216 return proc.returncode, stdout
217
218
219 def _check_command(*args, **kwargs):
220 rv, stdout = _run_command(args, **kwargs)
221 if rv != 0:
222 raise ValueError('Process exited with non-zero return code (%d)' % (rv,))
223 return stdout
224
225
226 class RecipeRuntime(object):
227 """RecipeRuntime is the platform-specific runtime enviornment.
228
229 The runtime is loaded with a set of read-only attributes that are a
230 combination of plaetform and runtime values used in the setup and execution of
231 the recipe engine.
232 """
233
234 _SENTINEL = object()
235
236 def __init__(self, **kwargs):
237 self._attrs = kwargs
238
239 @classmethod
240 @contextlib.contextmanager
241 def enter(cls, leak, **kw):
242 """Enters the annotated_run environment.
243
244 This creates a temporary directory for this annotation run that is
245 automatically cleaned up. It returns a RecipeRuntime object containing a
246 combination of the supplied keyword arguments and the platform-specific
247 configuration.
248
249 Args:
250 leak (bool): If true, don't clean up the temporary directory on exit.
251 kw (dict): Key/value pairs to add as attributes to the RecipeRuntime.
252 """
253 # Build our platform attributes.
254 p = (platform.system(), platform.processor())
255 attrs = {}
256 for i in xrange(len(p)+1):
257 attrs.update(PLATFORM_CONFIG.get(p[:i], {}))
258 attrs.update(kw)
259
260 basedir = ensure_directory(os.getcwd(), '.recipe_runtime')
261 try:
262 tdir = tempfile.mkdtemp(dir=basedir)
263 LOGGER.debug('Using temporary directory [%s].', tdir)
264
265 attrs['workdir'] = tdir
266 yield cls(**attrs)
267 finally:
268 if basedir and os.path.isdir(basedir):
269 if not leak:
270 LOGGER.debug('Cleaning up temporary directory [%s].', basedir)
271 try:
272 # TODO(pgervais): use infra_libs.rmtree instead.
273 shutil.rmtree(basedir)
274 except Exception:
275 LOGGER.exception('Failed to clean up temporary directory [%s].',
276 basedir)
277 else:
278 LOGGER.warning('(--leak) Leaking temporary directory [%s].', basedir)
279
280 def __getattr__(self, key):
281 # Class methods/variables.
282 value = getattr(super(RecipeRuntime, self), key, self._SENTINEL)
283 if value is not self._SENTINEL:
284 return value
285
286 value = getattr(self, 'get')(key, self._SENTINEL)
287 if value is not self._SENTINEL:
288 return value
289 raise KeyError(key)
290
291 def get(self, key, default=None):
292 value = self._attrs.get(key, self._SENTINEL)
293 if value is not self._SENTINEL:
294 return value
295 return default
296
297 def __str__(self):
298 return str(self._attrs)
299
300
301 def _get_service_account_json(opts, credential_paths):
302 """Returns (str/None): If specified, the path to the service account JSON.
303
304 This method probes the local environemnt and returns a (possibly empty) list
305 of arguments to add to the Butler command line for authentication.
306
307 If we're running on a GCE instance, no arguments will be returned, as GCE
308 service account is implicitly authenticated. If we're running on Baremetal,
309 a path to those credentials will be returned.
310
311 Args:
312 rt (RecipeRuntime): The runtime environment.
313 Raises:
314 |LogDogBootstrapError| if no credentials could be found.
315 """
316 path = opts.get('service_account_json')
317 if path:
318 return path
319
320 if gce.Authenticator.is_gce():
321 LOGGER.info('Running on GCE. No credentials necessary.')
322 return None
323
324 for credential_path in credential_paths:
325 candidate = os.path.join(credential_path, 'logdog_service_account.json')
326 if os.path.isfile(candidate):
327 return candidate
328
329 raise LogDogBootstrapError('Could not find service account credentials. '
330 'Tried: %s' % (credential_paths,))
331
332
333 def _logdog_bootstrap(tempdir, config, opts, cmd):
334 """Executes the recipe engine, bootstrapping it through LogDog/Annotee.
335
336 This method executes the recipe engine, bootstrapping it through
337 LogDog/Annotee so its output and annotations are streamed to LogDog. The
338 bootstrap is configured to tee the annotations through STDOUT/STDERR so they
339 will still be sent to BuildBot.
340
341 The overall setup here is:
342 [annotated_run.py] => [logdog_butler] => [logdog_annotee] => [recipes.py]
343
344 Args:
345 config (Config): Recipe runtime configuration.
346 opts (argparse.Namespace): Command-line options.
347 cmd (list): The recipe runner command list to bootstrap.
348
349 Returns (int): The return code of the recipe runner process.
350
351 Raises:
352 LogDogNotBootstrapped: if the recipe engine was not executed because the
353 LogDog bootstrap requirements are not available.
354 LogDogBootstrapError: if there was an error bootstrapping the recipe runner
355 through LogDog.
356 """
357 bootstrap_dir = ensure_directory(tempdir, 'logdog_bootstrap')
358 butler, annotee = opts.logdog_butler_path, opts.logdog_annotee_path
359
360 if not is_executable(annotee):
361 raise LogDogNotBootstrapped('Annotee is not executable: %s' % (annotee,))
362 if not is_executable(butler):
363 raise LogDogNotBootstrapped('Butler is not executable: %s' % (butler,))
364
365 # Determine LogDog verbosity.
366 logdog_verbose = []
367 if opts.logdog_verbose == 0:
368 pass
369 elif opts.logdog_verbose == 1:
370 logdog_verbose.extend('-log_level=info')
371 else:
372 logdog_verbose.extend('-log_level=debug')
373
374 service_account_args = []
375 service_account_json = _get_service_account_json(
376 opts, config.credential_paths)
377 if service_account_json:
378 service_account_args += ['-service-account-json', service_account_json]
379
380 streamserver_uri_gen = config.logdog_butler_streamserver_gen
381 if not streamserver_uri_gen:
382 raise LogDogBootstrapError('No streamserver URI generator.')
383 streamserver_uri = streamserver_uri_gen(tempdir)
384
385 # Dump Annotee command to JSON.
386 cmd_json = os.path.join(bootstrap_dir, 'annotee_cmd.json')
387 with open(cmd_json, 'w') as fd:
388 json.dump(cmd, fd)
389
390 cmd = [
391 # Butler Command.
392 butler,
393 ] + logdog_verbose + service_account_args + [
394 '-output', 'gcps,project="%s",topic="%s"' % (config.logdog_pubsub.project,
395 config.logdog_pubsub.topic),
396 'run',
397 '-streamserver-uri', streamserver_uri,
398 '--',
399
400 # Annotee Command.
401 annotee,
402 ] + logdog_verbose + [
403 '-json-args-path', cmd_json,
404 ]
405 rv, _ = _run_command(cmd, dry_run=opts.dry_run)
406 if rv in LOGDOG_ERROR_RETURNCODES:
407 raise LogDogBootstrapError('LogDog Error (%d)' % (rv,))
408 return rv
409
410
411 def _assert_logdog_whitelisted(mastername, buildername):
412 """Asserts that the runtime environment is whitelisted for LogDog bootstrap.
413
414 Args:
415 mastername (str): The master name string.
416 buildername (str): The builder name.
417 Raises:
418 LogDogNotBootstrapped: if the runtime is not whitelisted.
419 """
420 if not all((mastername, buildername)):
421 raise LogDogNotBootstrapped('Required mastername/buildername is not set.')
422
423 # Key on mastername.
424 bdict = LOGDOG_WHITELIST_MASTER_BUILDERS.get(mastername)
425 if bdict is not None:
426 # Key on buildername.
427 if WHITELIST_ALL in bdict or buildername in bdict:
428 LOGGER.info('Whitelisted master %s, builder %s.',
429 mastername, buildername)
430 return
431 raise LogDogNotBootstrapped('Master %s, builder %s is not whitelisted.' % (
432 mastername, buildername))
433
434
147 def get_recipe_properties(workdir, build_properties, 435 def get_recipe_properties(workdir, build_properties,
148 use_factory_properties_from_disk): 436 use_factory_properties_from_disk):
149 """Constructs the recipe's properties from buildbot's properties. 437 """Constructs the recipe's properties from buildbot's properties.
150 438
151 This retrieves the current factory properties from the master_config 439 This retrieves the current factory properties from the master_config
152 in the slave's checkout (no factory properties are handed to us from the 440 in the slave's checkout (no factory properties are handed to us from the
153 master), and merges in the build properties. 441 master), and merges in the build properties.
154 442
155 Using the values from the checkout allows us to do things like change 443 Using the values from the checkout allows us to do things like change
156 the recipe and other factory properties for a builder without needing 444 the recipe and other factory properties for a builder without needing
(...skipping 127 matching lines...) Expand 10 before | Expand all | Expand 10 after
284 help='factory properties in b64 gz JSON format') 572 help='factory properties in b64 gz JSON format')
285 parser.add_argument('--keep-stdin', action='store_true', default=False, 573 parser.add_argument('--keep-stdin', action='store_true', default=False,
286 help='don\'t close stdin when running recipe steps') 574 help='don\'t close stdin when running recipe steps')
287 parser.add_argument('--master-overrides-slave', action='store_true', 575 parser.add_argument('--master-overrides-slave', action='store_true',
288 help='use the property values given on the command line from the master, ' 576 help='use the property values given on the command line from the master, '
289 'not the ones looked up on the slave') 577 'not the ones looked up on the slave')
290 parser.add_argument('--use-factory-properties-from-disk', 578 parser.add_argument('--use-factory-properties-from-disk',
291 action='store_true', default=False, 579 action='store_true', default=False,
292 help='use factory properties loaded from disk on the slave') 580 help='use factory properties loaded from disk on the slave')
293 581
582 group = parser.add_argument_group('LogDog Bootstrap')
583 group.add_argument('-V', '--logdog-verbose',
584 action='count', default=0,
585 help='Increase LogDog verbosity. This can be specified multiple times.')
586 group.add_argument('-f', '--logdog-force', action='store_true',
587 help='Force LogDog bootstrapping, even if the system is not configured.')
588 group.add_argument('--logdog-butler-path',
589 help='Path to the LogDog Butler. If empty, one will be probed/downloaded '
590 'from CIPD.')
591 group.add_argument('--logdog-annotee-path',
592 help='Path to the LogDog Annotee. If empty, one will be '
593 'probed/downloaded from CIPD.')
594 group.add_argument('--logdog-service-account-json',
595 help='Path to the service account JSON. If one is not provided, the '
596 'local system credentials will be used.')
597
294 return parser.parse_args(argv) 598 return parser.parse_args(argv)
295 599
296 600
297 def update_scripts(): 601 def update_scripts():
298 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): 602 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'):
299 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') 603 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS')
300 return False 604 return False
301 605
302 stream = annotator.StructuredAnnotationStream() 606 stream = annotator.StructuredAnnotationStream()
303 607
(...skipping 166 matching lines...) Expand 10 before | Expand all | Expand 10 after
470 with open(props_file, 'w') as fh: 774 with open(props_file, 'w') as fh:
471 json.dump(properties, fh) 775 json.dump(properties, fh)
472 cmd = [ 776 cmd = [
473 sys.executable, '-u', recipe_runner, 777 sys.executable, '-u', recipe_runner,
474 'run', 778 'run',
475 '--workdir=%s' % os.getcwd(), 779 '--workdir=%s' % os.getcwd(),
476 '--properties-file=%s' % props_file, 780 '--properties-file=%s' % props_file,
477 properties['recipe'], 781 properties['recipe'],
478 ] 782 ]
479 783
480 status, _ = _run_command(cmd, dry_run=opts.dry_run) 784 status = None
785 try:
786 if not opts.logdog_force:
787 _assert_logdog_whitelisted(config.mastername, config.buildername)
788 status = _logdog_bootstrap(tdir, config, opts, cmd)
789 except LogDogNotBootstrapped as e:
790 LOGGER.info('Not bootstrapped: %s', e.message)
791 except LogDogBootstrapError as e:
792 LOGGER.warning('Could not bootstrap LogDog: %s', e.message)
793 except Exception:
794 LOGGER.exception('Exception while bootstrapping LogDog.')
795 finally:
796 if status is None:
797 LOGGER.info('Not using LogDog. Invoking `annotated_run.py` directly.')
798 status, _ = _run_command(cmd, dry_run=opts.dry_run)
481 799
482 return status 800 return status
483 801
484 802
485 def shell_main(argv): 803 def shell_main(argv):
486 if update_scripts(): 804 if update_scripts():
487 # Re-execute with the updated annotated_run.py. 805 # Re-execute with the updated annotated_run.py.
488 rv, _ = _run_command([sys.executable] + argv) 806 rv, _ = _run_command([sys.executable] + argv)
489 return rv 807 return rv
490 else: 808 else:
491 return main(argv[1:]) 809 return main(argv[1:])
492 810
493 811
494 if __name__ == '__main__': 812 if __name__ == '__main__':
495 logging.basicConfig(level=logging.INFO) 813 logging.basicConfig(level=logging.INFO)
496 sys.exit(shell_main(sys.argv)) 814 sys.exit(shell_main(sys.argv))
OLDNEW
« no previous file with comments | « no previous file | scripts/slave/gce.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698