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

Side by Side Diff: recipe_engine/run.py

Issue 2846703003: [recipes.py] move run arg parsing to its module. (Closed)
Patch Set: fix nits 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
« no previous file with comments | « recipe_engine/depgraph.py ('k') | recipe_engine/unittests/run_test.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 # 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 """Entry point for fully-annotated builds. 5 """Entry point for fully-annotated builds.
6 6
7 This script is part of the effort to move all builds to annotator-based 7 This script is part of the effort to move all builds to annotator-based
8 systems. Any builder configured to use the AnnotatorFactory.BaseFactory() 8 systems. Any builder configured to use the AnnotatorFactory.BaseFactory()
9 found in scripts/master/factory/annotator_factory.py executes a single 9 found in scripts/master/factory/annotator_factory.py executes a single
10 AddAnnotatedScript step. That step (found in annotator_commands.py) calls 10 AddAnnotatedScript step. That step (found in annotator_commands.py) calls
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after
68 import logging 68 import logging
69 import os 69 import os
70 import sys 70 import sys
71 import traceback 71 import traceback
72 72
73 from . import loader 73 from . import loader
74 from . import recipe_api 74 from . import recipe_api
75 from . import recipe_test_api 75 from . import recipe_test_api
76 from . import types 76 from . import types
77 from . import util 77 from . import util
78
79 from . import env
80
81 import argparse # this is vendored
82 import subprocess42
83
78 from . import result_pb2 84 from . import result_pb2
79 import subprocess42 85
86 from google.protobuf import json_format as jsonpb
80 87
81 88
82 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) 89 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
83 90
84 91
85 # TODO(martiniss): Remove this 92 # TODO(martiniss): Remove this
86 RecipeResult = collections.namedtuple('RecipeResult', 'result') 93 RecipeResult = collections.namedtuple('RecipeResult', 'result')
87 94
95
88 # TODO(dnj): Replace "properties" with a generic runtime instance. This instance 96 # TODO(dnj): Replace "properties" with a generic runtime instance. This instance
89 # will be used to seed recipe clients and expanded to include managed runtime 97 # will be used to seed recipe clients and expanded to include managed runtime
90 # entities. 98 # entities.
91 def run_steps(properties, stream_engine, step_runner, universe_view, 99 def run_steps(properties, stream_engine, step_runner, universe_view,
92 engine_flags=None, emit_initial_properties=False): 100 engine_flags=None, emit_initial_properties=False):
93 """Runs a recipe (given by the 'recipe' property). 101 """Runs a recipe (given by the 'recipe' property).
94 102
95 Args: 103 Args:
96 properties: a dictionary of properties to pass to the recipe. The 104 properties: a dictionary of properties to pass to the recipe. The
97 'recipe' property defines which recipe to actually run. 105 'recipe' property defines which recipe to actually run.
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
169 # Run the steps emitted by a recipe via the engine, emitting annotations 177 # Run the steps emitted by a recipe via the engine, emitting annotations
170 # into |stream| along the way. 178 # into |stream| along the way.
171 return engine.run(recipe_script, api, properties) 179 return engine.run(recipe_script, api, properties)
172 180
173 181
174 # Return value of run_steps and RecipeEngine.run. Just a container for the 182 # Return value of run_steps and RecipeEngine.run. Just a container for the
175 # literal return value of the recipe. 183 # literal return value of the recipe.
176 # TODO(martiniss): remove this 184 # TODO(martiniss): remove this
177 RecipeResult = collections.namedtuple('RecipeResult', 'result') 185 RecipeResult = collections.namedtuple('RecipeResult', 'result')
178 186
187
179 class RecipeEngine(object): 188 class RecipeEngine(object):
180 """ 189 """
181 Knows how to execute steps emitted by a recipe, holds global state such as 190 Knows how to execute steps emitted by a recipe, holds global state such as
182 step history and build properties. Each recipe module API has a reference to 191 step history and build properties. Each recipe module API has a reference to
183 this object. 192 this object.
184 193
185 Recipe modules that are aware of the engine: 194 Recipe modules that are aware of the engine:
186 * properties - uses engine.properties. 195 * properties - uses engine.properties.
187 * step - uses engine.create_step(...), and previous_step_result. 196 * step - uses engine.create_step(...), and previous_step_result.
188 """ 197 """
(...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after
427 results.append( 436 results.append(
428 loader._invoke_with_properties( 437 loader._invoke_with_properties(
429 run_recipe, properties, recipe_script.PROPERTIES, 438 run_recipe, properties, recipe_script.PROPERTIES,
430 properties.keys())) 439 properties.keys()))
431 except TypeError as e: 440 except TypeError as e:
432 raise TypeError( 441 raise TypeError(
433 "Got %r while trying to call recipe %s with properties %r" % ( 442 "Got %r while trying to call recipe %s with properties %r" % (
434 e, recipe, properties)) 443 e, recipe, properties))
435 444
436 return results 445 return results
446
447
448 def add_subparser(parser):
449 def properties_file_type(filename):
450 with (sys.stdin if filename == '-' else open(filename)) as f:
451 obj = json.load(f)
452 if not isinstance(obj, dict):
453 raise argparse.ArgumentTypeError(
454 'must contain a JSON object, i.e. `{}`.')
455 return obj
456
457 def parse_prop(prop):
458 key, val = prop.split('=', 1)
459 try:
460 val = json.loads(val)
461 except (ValueError, SyntaxError):
462 pass # If a value couldn't be evaluated, keep the string version
463 return {key: val}
464
465 def properties_type(value):
466 obj = json.loads(value)
467 if not isinstance(obj, dict):
468 raise argparse.ArgumentTypeError('must contain a JSON object, i.e. `{}`.')
469 return obj
470
471 run_p = parser.add_parser(
472 'run',
473 description='Run a recipe locally')
474
475 run_p.add_argument(
476 '--workdir',
477 type=os.path.abspath,
478 help='The working directory of recipe execution')
479 run_p.add_argument(
480 '--output-result-json',
481 type=os.path.abspath,
482 help='The file to write the JSON serialized returned value \
483 of the recipe to')
484 run_p.add_argument(
485 '--timestamps',
486 action='store_true',
487 help='If true, emit CURRENT_TIMESTAMP annotations. '
488 'Default: false. '
489 'CURRENT_TIMESTAMP annotation has one parameter, current time in '
490 'Unix timestamp format. '
491 'CURRENT_TIMESTAMP annotation will be printed at the beginning and '
492 'end of the annotation stream and also immediately before each '
493 'STEP_STARTED and STEP_CLOSED annotations.',
494 )
495 prop_group = run_p.add_mutually_exclusive_group()
496 prop_group.add_argument(
497 '--properties-file',
498 dest='properties',
499 type=properties_file_type,
500 help=('A file containing a json blob of properties. '
501 'Pass "-" to read from stdin'))
502 prop_group.add_argument(
503 '--properties',
504 type=properties_type,
505 help='A json string containing the properties')
506
507 run_p.add_argument(
508 'recipe',
509 help='The recipe to execute')
510 run_p.add_argument(
511 'props',
512 nargs=argparse.REMAINDER,
513 type=parse_prop,
514 help='A list of property pairs; e.g. mastername=chromium.linux '
515 'issue=12345. The property value will be decoded as JSON, but if '
516 'this decoding fails the value will be interpreted as a string.')
517
518 run_p.set_defaults(command='run', properties={}, func=main)
519
520
521 def handle_recipe_return(recipe_result, result_filename, stream_engine,
522 engine_flags):
523 if engine_flags and engine_flags.use_result_proto:
524 return new_handle_recipe_return(
525 recipe_result, result_filename, stream_engine)
526
527 if 'recipe_result' in recipe_result.result:
528 result_string = json.dumps(
529 recipe_result.result['recipe_result'], indent=2)
530 if result_filename:
531 with open(result_filename, 'w') as f:
532 f.write(result_string)
533 with stream_engine.make_step_stream('recipe result') as s:
534 with s.new_log_stream('result') as l:
535 l.write_split(result_string)
536
537 if 'traceback' in recipe_result.result:
538 with stream_engine.make_step_stream('Uncaught Exception') as s:
539 with s.new_log_stream('exception') as l:
540 for line in recipe_result.result['traceback']:
541 l.write_line(line)
542
543 if 'reason' in recipe_result.result:
544 with stream_engine.make_step_stream('Failure reason') as s:
545 with s.new_log_stream('reason') as l:
546 for line in recipe_result.result['reason'].splitlines():
547 l.write_line(line)
548
549 if 'status_code' in recipe_result.result:
550 return recipe_result.result['status_code']
551 else:
552 return 0
553
554
555 def new_handle_recipe_return(result, result_filename, stream_engine):
556 if result_filename:
557 with open(result_filename, 'w') as fil:
558 fil.write(jsonpb.MessageToJson(
559 result, including_default_value_fields=True))
560
561 if result.json_result:
562 with stream_engine.make_step_stream('recipe result') as s:
563 with s.new_log_stream('result') as l:
564 l.write_split(result.json_result)
565
566 if result.HasField('failure'):
567 f = result.failure
568 if f.HasField('exception'):
569 with stream_engine.make_step_stream('Uncaught Exception') as s:
570 s.add_step_text(f.human_reason)
571 with s.new_log_stream('exception') as l:
572 for line in f.exception.traceback:
573 l.write_line(line)
574 # TODO(martiniss): Remove this code once calling code handles these states
575 elif f.HasField('timeout'):
576 with stream_engine.make_step_stream('Step Timed Out') as s:
577 with s.new_log_stream('timeout_s') as l:
578 l.write_line(f.timeout.timeout_s)
579 elif f.HasField('step_data'):
580 with stream_engine.make_step_stream('Invalid Step Data Access') as s:
581 with s.new_log_stream('step') as l:
582 l.write_line(f.step_data.step)
583
584 with stream_engine.make_step_stream('Failure reason') as s:
585 with s.new_log_stream('reason') as l:
586 l.write_split(f.human_reason)
587
588 return 1
589
590 return 0
591
592
593 # Map of arguments_pb2.Property "value" oneof conversion functions.
594 #
595 # The fields here should be kept in sync with the "value" oneof field names in
596 # the arguments_pb2.Arguments.Property protobuf message.
597 _OP_PROPERTY_CONV = {
598 's': lambda prop: prop.s,
599 'int': lambda prop: prop.int,
600 'uint': lambda prop: prop.uint,
601 'd': lambda prop: prop.d,
602 'b': lambda prop: prop.b,
603 'data': lambda prop: prop.data,
604 'map': lambda prop: _op_properties_to_dict(prop.map.property),
605 'list': lambda prop: [_op_property_value(v) for v in prop.list.property],
606 }
607
608
609 def _op_property_value(prop):
610 """Returns the Python-converted value of an arguments_pb2.Property.
611
612 Args:
613 prop (arguments_pb2.Property): property to convert.
614 Returns: The converted value.
615 Raises:
616 ValueError: If 'prop' is incomplete or invalid.
617 """
618 typ = prop.WhichOneof('value')
619 conv = _OP_PROPERTY_CONV.get(typ)
620 if not conv:
621 raise ValueError('Unknown property field [%s]' % (typ,))
622 return conv(prop)
623
624
625 def _op_properties_to_dict(pmap):
626 """Creates a properties dictionary from an arguments_pb2.PropertyMap entry.
627
628 Args:
629 pmap (arguments_pb2.PropertyMap): Map to convert to dictionary form.
630 Returns (dict): A dictionary derived from the properties in 'pmap'.
631 """
632 return dict((k, _op_property_value(pmap[k])) for k in pmap)
633
634
635 def main(package_deps, args):
636 from recipe_engine import step_runner
637 from recipe_engine import stream
638 from recipe_engine import stream_logdog
639
640 config_file = args.package
641
642 if args.props:
643 for p in args.props:
644 args.properties.update(p)
645
646 def get_properties_from_operational_args(op_args):
647 if not op_args.properties.property:
648 return None
649 return _op_properties_to_dict(op_args.properties.property)
650
651 op_args = args.operational_args
652 op_properties = get_properties_from_operational_args(op_args)
653 if args.properties and op_properties:
654 raise ValueError(
655 'Got operational args properties as well as CLI properties.')
656
657 properties = op_properties
658 if not properties:
659 properties = args.properties
660
661 properties['recipe'] = args.recipe
662
663 properties = util.strip_unicode(properties)
664
665 os.environ['PYTHONUNBUFFERED'] = '1'
666 os.environ['PYTHONIOENCODING'] = 'UTF-8'
667
668 universe_view = loader.UniverseView(
669 loader.RecipeUniverse(
670 package_deps, config_file), package_deps.root_package)
671
672 # TODO(iannucci): this is horrible; why do we want to set a workdir anyway?
673 # Shouldn't the caller of recipes just CD somewhere if they want a different
674 # workdir?
675 workdir = (args.workdir or
676 os.path.join(SCRIPT_PATH, os.path.pardir, 'workdir'))
677 logging.info('Using %s as work directory' % workdir)
678 if not os.path.exists(workdir):
679 os.makedirs(workdir)
680
681 old_cwd = os.getcwd()
682 os.chdir(workdir)
683
684 # Construct our stream engines. We may want to share stream events with more
685 # than one StreamEngine implementation, so we will accumulate them in a
686 # "stream_engines" list and compose them into a MultiStreamEngine.
687 def build_annotation_stream_engine():
688 return stream.AnnotatorStreamEngine(
689 sys.stdout,
690 emit_timestamps=(args.timestamps or
691 op_args.annotation_flags.emit_timestamp))
692
693 stream_engines = []
694 if op_args.logdog.streamserver_uri:
695 logging.debug('Using LogDog with parameters: [%s]', op_args.logdog)
696 stream_engines.append(stream_logdog.StreamEngine(
697 streamserver_uri=op_args.logdog.streamserver_uri,
698 name_base=(op_args.logdog.name_base or None),
699 dump_path=op_args.logdog.final_annotation_dump_path,
700 ))
701
702 # If we're teeing, also fold in a standard annotation stream engine.
703 if op_args.logdog.tee:
704 stream_engines.append(build_annotation_stream_engine())
705 else:
706 # Not using LogDog; use a standard annotation stream engine.
707 stream_engines.append(build_annotation_stream_engine())
708 multi_stream_engine = stream.MultiStreamEngine.create(*stream_engines)
709
710 emit_initial_properties = op_args.annotation_flags.emit_initial_properties
711 engine_flags = op_args.engine_flags
712
713 # Have a top-level set of invariants to enforce StreamEngine expectations.
714 with stream.StreamEngineInvariants.wrap(multi_stream_engine) as stream_engine:
715 try:
716 ret = run_steps(
717 properties, stream_engine,
718 step_runner.SubprocessStepRunner(stream_engine, engine_flags),
719 universe_view, engine_flags=engine_flags,
720 emit_initial_properties=emit_initial_properties)
721 finally:
722 os.chdir(old_cwd)
723
724 return handle_recipe_return(
725 ret, args.output_result_json, stream_engine, engine_flags)
OLDNEW
« no previous file with comments | « recipe_engine/depgraph.py ('k') | recipe_engine/unittests/run_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698