OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2015 The LUCI Authors. All rights reserved. | 2 # Copyright 2015 The LUCI Authors. All rights reserved. |
3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
4 # that can be found in the LICENSE file. | 4 # that can be found in the LICENSE file. |
5 | 5 |
6 """Tool to interact with recipe repositories. | 6 """Tool to interact with recipe repositories. |
7 | 7 |
8 This tool operates on the nearest ancestor directory containing an | 8 This tool operates on the nearest ancestor directory containing an |
9 infra/config/recipes.cfg. | 9 infra/config/recipes.cfg. |
10 """ | 10 """ |
11 | 11 |
12 import argparse | 12 import argparse |
13 import json | 13 import json |
14 import logging | 14 import logging |
15 import os | 15 import os |
16 import subprocess | 16 import subprocess |
17 import sys | 17 import sys |
18 | 18 |
19 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | 19 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
20 sys.path.insert(0, ROOT_DIR) | 20 sys.path.insert(0, ROOT_DIR) |
21 | 21 |
22 from recipe_engine import env | 22 from recipe_engine import env |
23 from recipe_engine import arguments_pb2 | |
24 from google.protobuf import json_format as jsonpb | |
23 | 25 |
24 def get_package_config(args): | 26 def get_package_config(args): |
25 from recipe_engine import package | 27 from recipe_engine import package |
26 | 28 |
27 assert args.package, 'No recipe config (--package) given.' | 29 assert args.package, 'No recipe config (--package) given.' |
28 assert os.path.exists(args.package), ( | 30 assert os.path.exists(args.package), ( |
29 'Given recipes config file %s does not exist.' % args.package) | 31 'Given recipes config file %s does not exist.' % args.package) |
30 return ( | 32 return ( |
31 package.InfraRepoConfig().from_recipes_cfg(args.package), | 33 package.InfraRepoConfig().from_recipes_cfg(args.package), |
32 package.ProtoFile(args.package) | 34 package.ProtoFile(args.package) |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
77 with s.new_log_stream('reason') as l: | 79 with s.new_log_stream('reason') as l: |
78 for line in recipe_result.result['reason'].splitlines(): | 80 for line in recipe_result.result['reason'].splitlines(): |
79 l.write_line(line) | 81 l.write_line(line) |
80 | 82 |
81 if 'status_code' in recipe_result.result: | 83 if 'status_code' in recipe_result.result: |
82 return recipe_result.result['status_code'] | 84 return recipe_result.result['status_code'] |
83 else: | 85 else: |
84 return 0 | 86 return 0 |
85 | 87 |
86 | 88 |
87 def run(package_deps, args): | 89 def run(package_deps, args, op_args): |
88 from recipe_engine import run as recipe_run | 90 from recipe_engine import run as recipe_run |
89 from recipe_engine import loader | 91 from recipe_engine import loader |
90 from recipe_engine import step_runner | 92 from recipe_engine import step_runner |
91 from recipe_engine import stream | 93 from recipe_engine import stream |
92 | 94 |
93 def get_properties_from_args(args): | 95 def get_properties_from_args(args): |
94 properties = dict(x.split('=', 1) for x in args) | 96 properties = dict(x.split('=', 1) for x in args) |
95 for key, val in properties.iteritems(): | 97 for key, val in properties.iteritems(): |
96 try: | 98 try: |
97 properties[key] = json.loads(val) | 99 properties[key] = json.loads(val) |
98 except (ValueError, SyntaxError): | 100 except (ValueError, SyntaxError): |
99 pass # If a value couldn't be evaluated, keep the string version | 101 pass # If a value couldn't be evaluated, keep the string version |
100 return properties | 102 return properties |
101 | 103 |
102 def get_properties_from_file(filename): | 104 def get_properties_from_file(filename): |
103 properties_file = sys.stdin if filename == '-' else open(filename) | 105 properties_file = sys.stdin if filename == '-' else open(filename) |
104 properties = json.load(properties_file) | 106 properties = json.load(properties_file) |
105 assert isinstance(properties, dict) | 107 assert isinstance(properties, dict) |
106 return properties | 108 return properties |
107 | 109 |
108 def get_properties_from_json(props): | 110 def get_properties_from_json(props): |
109 return json.loads(props) | 111 return json.loads(props) |
110 | 112 |
113 def get_properties_from_operational_args(op_args): | |
114 if not op_args.properties.property: | |
115 return None | |
116 return _op_properties_to_dict(op_args.properties.property) | |
117 | |
118 | |
111 arg_properties = get_properties_from_args(args.props) | 119 arg_properties = get_properties_from_args(args.props) |
120 op_properties = get_properties_from_operational_args(op_args) | |
112 assert len(filter(bool, | 121 assert len(filter(bool, |
113 [arg_properties, args.properties_file, args.properties])) <= 1, ( | 122 (arg_properties, args.properties_file, args.properties, |
123 op_properties))) <= 1, ( | |
114 'Only one source of properties is allowed') | 124 'Only one source of properties is allowed') |
115 if args.properties: | 125 if args.properties: |
116 properties = get_properties_from_json(args.properties) | 126 properties = get_properties_from_json(args.properties) |
117 elif args.properties_file: | 127 elif args.properties_file: |
118 properties = get_properties_from_file(args.properties_file) | 128 properties = get_properties_from_file(args.properties_file) |
129 elif op_properties is not None: | |
130 properties = op_properties | |
119 else: | 131 else: |
120 properties = arg_properties | 132 properties = arg_properties |
121 | 133 |
122 properties['recipe'] = args.recipe | 134 properties['recipe'] = args.recipe |
123 | 135 |
124 os.environ['PYTHONUNBUFFERED'] = '1' | 136 os.environ['PYTHONUNBUFFERED'] = '1' |
125 os.environ['PYTHONIOENCODING'] = 'UTF-8' | 137 os.environ['PYTHONIOENCODING'] = 'UTF-8' |
126 | 138 |
127 _, config_file = get_package_config(args) | 139 _, config_file = get_package_config(args) |
128 universe = loader.UniverseView( | 140 universe = loader.UniverseView( |
129 loader.RecipeUniverse( | 141 loader.RecipeUniverse( |
130 package_deps, config_file), package_deps.root_package) | 142 package_deps, config_file), package_deps.root_package) |
131 | 143 |
132 workdir = (args.workdir or | 144 workdir = (args.workdir or |
133 os.path.join(os.path.dirname(os.path.realpath(__file__)), 'workdir')) | 145 os.path.join(os.path.dirname(os.path.realpath(__file__)), 'workdir')) |
134 logging.info('Using %s as work directory' % workdir) | 146 logging.info('Using %s as work directory' % workdir) |
135 if not os.path.exists(workdir): | 147 if not os.path.exists(workdir): |
136 os.makedirs(workdir) | 148 os.makedirs(workdir) |
137 | 149 |
138 old_cwd = os.getcwd() | 150 old_cwd = os.getcwd() |
139 os.chdir(workdir) | 151 os.chdir(workdir) |
140 stream_engine = stream.ProductStreamEngine( | 152 stream_engine = stream.ProductStreamEngine( |
141 stream.StreamEngineInvariants(), | 153 stream.StreamEngineInvariants(), |
142 stream.AnnotatorStreamEngine(sys.stdout, emit_timestamps=args.timestamps)) | 154 stream.AnnotatorStreamEngine( |
155 sys.stdout, emit_timestamps=(args.timestamps or | |
156 op_args.annotation_flags.emit_timestamp))) | |
143 with stream_engine: | 157 with stream_engine: |
144 try: | 158 try: |
145 ret = recipe_run.run_steps( | 159 ret = recipe_run.run_steps( |
146 properties, stream_engine, | 160 properties, stream_engine, |
147 step_runner.SubprocessStepRunner(stream_engine), | 161 step_runner.SubprocessStepRunner(stream_engine), |
148 universe=universe) | 162 universe=universe) |
149 finally: | 163 finally: |
150 os.chdir(old_cwd) | 164 os.chdir(old_cwd) |
151 | 165 |
152 return handle_recipe_return(ret, args.output_result_json, stream_engine) | 166 return handle_recipe_return(ret, args.output_result_json, stream_engine) |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
221 | 235 |
222 def info(args): | 236 def info(args): |
223 from recipe_engine import package | 237 from recipe_engine import package |
224 repo_root, config_file = get_package_config(args) | 238 repo_root, config_file = get_package_config(args) |
225 package_spec = package.PackageSpec.load_proto(config_file) | 239 package_spec = package.PackageSpec.load_proto(config_file) |
226 | 240 |
227 if args.recipes_dir: | 241 if args.recipes_dir: |
228 print package_spec.recipes_path | 242 print package_spec.recipes_path |
229 | 243 |
230 | 244 |
245 # Map of arguments_pb2.Property "value" oneof conversion functions. | |
martiniss
2016/08/10 22:54:45
Add comment saying "Keep in sync with recipe_engin
dnj
2016/08/11 01:21:31
Done.
| |
246 _OP_PROPERTY_CONV = { | |
247 's': lambda prop: prop.s, | |
248 'int': lambda prop: prop.int, | |
249 'uint': lambda prop: prop.uint, | |
250 'd': lambda prop: prop.d, | |
251 'b': lambda prop: prop.b, | |
252 'data': lambda prop: prop.data, | |
253 'map': lambda prop: _op_properties_to_dict(prop.map.property), | |
254 'list': lambda prop: [_op_property_value(v) for v in prop.list.property], | |
255 } | |
256 | |
257 def _op_property_value(prop): | |
258 """Returns the Python-converted value of an arguments_pb2.Property. | |
259 | |
260 Args: | |
261 prop (arguments_pb2.Property): property to convert. | |
262 Returns: The converted value. | |
263 Raises: | |
264 ValueError: If "prop" is incomplete or invalid. | |
265 """ | |
266 typ = prop.WhichOneof('value') | |
267 conv = _OP_PROPERTY_CONV.get(typ) | |
268 if not conv: | |
269 raise ValueError('Unknown property field [%s]' % (typ,)) | |
270 return conv(prop) | |
271 | |
272 | |
273 def _op_properties_to_dict(pmap): | |
274 """Creates a properties dictionary from an arguments_pb2.PropertyMap entry. | |
275 | |
276 Args: | |
277 pmap (arguments_pb2.PropertyMap): Map to convert to dictionary form. | |
278 Returns (dict): A dictionary derived from the properties in "pmap". | |
279 """ | |
280 return dict((k, _op_property_value(pmap[k])) for k in pmap) | |
281 | |
282 | |
231 def main(): | 283 def main(): |
232 from recipe_engine import package | 284 from recipe_engine import package |
233 | 285 |
234 # Super-annoyingly, we need to manually parse for simulation_test since | 286 # Super-annoyingly, we need to manually parse for simulation_test since |
235 # argparse is bonkers and doesn't allow us to forward --help to subcommands. | 287 # argparse is bonkers and doesn't allow us to forward --help to subcommands. |
236 # Save old_args for if we're using bootstrap | 288 # Save old_args for if we're using bootstrap |
237 original_sys_argv = sys.argv[:] | 289 original_sys_argv = sys.argv[:] |
238 if 'simulation_test' in sys.argv: | 290 if 'simulation_test' in sys.argv: |
239 index = sys.argv.index('simulation_test') | 291 index = sys.argv.index('simulation_test') |
240 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] | 292 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] |
(...skipping 18 matching lines...) Expand all Loading... | |
259 parser.add_argument( | 311 parser.add_argument( |
260 '--bootstrap-script', | 312 '--bootstrap-script', |
261 help='Path to the script used to bootstrap this tool (internal use only)') | 313 help='Path to the script used to bootstrap this tool (internal use only)') |
262 parser.add_argument('-O', '--project-override', metavar='ID=PATH', | 314 parser.add_argument('-O', '--project-override', metavar='ID=PATH', |
263 action=ProjectOverrideAction, | 315 action=ProjectOverrideAction, |
264 help='Override a project repository path with a local one.') | 316 help='Override a project repository path with a local one.') |
265 parser.add_argument( | 317 parser.add_argument( |
266 '--use-bootstrap', action='store_true', | 318 '--use-bootstrap', action='store_true', |
267 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' | 319 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' |
268 ' with required python dependencies.') | 320 ' with required python dependencies.') |
321 parser.add_argument( | |
322 '--operational-args-path', action='store', | |
323 help='The path to an operational Arguments file. If provided, this file ' | |
324 'must contain a JSONPB-encoded Arguments protobuf message, and will ' | |
325 'be integrated into the runtime parameters.') | |
269 | 326 |
270 subp = parser.add_subparsers() | 327 subp = parser.add_subparsers() |
271 | 328 |
272 fetch_p = subp.add_parser( | 329 fetch_p = subp.add_parser( |
273 'fetch', | 330 'fetch', |
274 description='Fetch and update dependencies.') | 331 description='Fetch and update dependencies.') |
275 fetch_p.set_defaults(command='fetch') | 332 fetch_p.set_defaults(command='fetch') |
276 | 333 |
277 simulation_test_p = subp.add_parser( | 334 simulation_test_p = subp.add_parser( |
278 'simulation_test', | 335 'simulation_test', |
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
400 info_p = subp.add_parser( | 457 info_p = subp.add_parser( |
401 'info', | 458 'info', |
402 description='Query information about the current recipe package') | 459 description='Query information about the current recipe package') |
403 info_p.set_defaults(command='info') | 460 info_p.set_defaults(command='info') |
404 info_p.add_argument( | 461 info_p.add_argument( |
405 '--recipes-dir', action='store_true', | 462 '--recipes-dir', action='store_true', |
406 help='Get the subpath where the recipes live relative to repository root') | 463 help='Get the subpath where the recipes live relative to repository root') |
407 | 464 |
408 args = parser.parse_args() | 465 args = parser.parse_args() |
409 | 466 |
467 # Load/parse operational arguments. | |
468 op_args = arguments_pb2.Arguments() | |
469 if args.operational_args_path is not None: | |
470 with open(args.operational_args_path) as fd: | |
471 data = fd.read() | |
472 jsonpb.Parse(data, op_args) | |
473 | |
410 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None): | 474 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None): |
411 subprocess.check_call( | 475 subprocess.check_call( |
412 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file', | 476 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file', |
413 'bootstrap/deps.pyl', 'ENV'], | 477 'bootstrap/deps.pyl', 'ENV'], |
414 cwd=os.path.dirname(os.path.realpath(__file__))) | 478 cwd=os.path.dirname(os.path.realpath(__file__))) |
415 | 479 |
416 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1' | 480 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1' |
417 args = sys.argv | 481 args = sys.argv |
418 return subprocess.call( | 482 return subprocess.call( |
419 [os.path.join( | 483 [os.path.join( |
(...skipping 28 matching lines...) Expand all Loading... | |
448 | 512 |
449 if args.command == 'fetch': | 513 if args.command == 'fetch': |
450 # We already did everything in the create() call above. | 514 # We already did everything in the create() call above. |
451 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' | 515 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' |
452 return 0 | 516 return 0 |
453 if args.command == 'simulation_test': | 517 if args.command == 'simulation_test': |
454 return simulation_test(package_deps, args) | 518 return simulation_test(package_deps, args) |
455 elif args.command == 'lint': | 519 elif args.command == 'lint': |
456 return lint(package_deps, args) | 520 return lint(package_deps, args) |
457 elif args.command == 'run': | 521 elif args.command == 'run': |
458 return run(package_deps, args) | 522 return run(package_deps, args, op_args) |
459 elif args.command == 'autoroll': | 523 elif args.command == 'autoroll': |
460 return autoroll(args) | 524 return autoroll(args) |
461 elif args.command == 'depgraph': | 525 elif args.command == 'depgraph': |
462 return depgraph(package_deps, args) | 526 return depgraph(package_deps, args) |
463 elif args.command == 'refs': | 527 elif args.command == 'refs': |
464 return refs(package_deps, args) | 528 return refs(package_deps, args) |
465 elif args.command == 'doc': | 529 elif args.command == 'doc': |
466 return doc(package_deps, args) | 530 return doc(package_deps, args) |
467 elif args.command == 'info': | 531 elif args.command == 'info': |
468 return info(args) | 532 return info(args) |
(...skipping 22 matching lines...) Expand all Loading... | |
491 ret = main() | 555 ret = main() |
492 if not isinstance(ret, int): | 556 if not isinstance(ret, int): |
493 if ret is None: | 557 if ret is None: |
494 ret = 0 | 558 ret = 0 |
495 else: | 559 else: |
496 print >> sys.stderr, ret | 560 print >> sys.stderr, ret |
497 ret = 1 | 561 ret = 1 |
498 sys.stdout.flush() | 562 sys.stdout.flush() |
499 sys.stderr.flush() | 563 sys.stderr.flush() |
500 os._exit(ret) | 564 os._exit(ret) |
OLD | NEW |