| # Copyright 2017 The LUCI Authors. All rights reserved. |
| # Use of this source code is governed under the Apache License, Version 2.0 |
| # that can be found in the LICENSE file. |
| |
| |
| """A simplistic method for running steps generated by an external script. |
| |
| This module was created before there was a way to put recipes directly into |
| another repo. It is not recommended to use this, and it will be removed in the |
| near future. |
| """ |
| |
| |
| from recipe_engine import recipe_api |
| |
| class GeneratorScriptApi(recipe_api.RecipeApi): |
| ALLOWED_KEYS = frozenset([ |
| # supported by step module |
| 'name', 'cmd', 'env', 'cwd', 'allow_subannotations', |
| |
| # implemented by GeneratorScriptApi |
| 'always_run', 'outputs_presentation_json', |
| ]) |
| |
| class UnknownKey(recipe_api.StepFailure): |
| def __init__(self, step_name, bad_keys): |
| reason = 'Step(%r) generated step with bad keys %r' % ( |
| step_name, bad_keys) |
| super(GeneratorScriptApi.UnknownKey, self).__init__( |
| reason) |
| self.bad_keys = frozenset(bad_keys) |
| |
| class MalformedStepList(recipe_api.StepFailure): |
| def __init__(self, step_name): |
| super(GeneratorScriptApi.MalformedStepList, self).__init__( |
| 'Step(%r) generated non-list JSON output' % (step_name,)) |
| |
| class MalformedCmd(recipe_api.StepFailure): |
| def __init__(self, step_name): |
| super(GeneratorScriptApi.MalformedCmd, self).__init__( |
| 'Step(%r) generated step with "cmd" containing non-strings' % ( |
| step_name,)) |
| |
| def __call__(self, path_to_script, *args): |
| """Run a script and generate the steps emitted by that script. |
| |
| The script will be invoked with --output-json /path/to/file.json. The script |
| is expected to exit 0 and write steps into that file. Once the script |
| outputs all of the steps to that file, the recipe will read the steps from |
| that file and execute them in order. Any *args specified will be |
| additionally passed to the script. |
| |
| The step data is formatted as a list of JSON objects. Each object |
| corresponds to one step, and contains the following keys: |
| * name: the name of this step. |
| * cmd: a list of strings that indicate the command to run (e.g. argv) |
| * env: a {key:value} dictionary of the environment variables to override. |
| every value is formatted with the current environment with the python |
| % operator, so a value of "%(PATH)s:/some/other/path" would resolve to |
| the current PATH value, concatenated with ":/some/other/path" |
| * cwd: an absolute path to the current working directory for this script. |
| * always_run: a bool which indicates that this step should run, even if |
| some previous step failed. |
| * outputs_presentation_json: a bool which indicates that this step will |
| emit a presentation json file. If this is True, the cmd will be extended |
| with a `--presentation-json /path/to/file.json`. This file will be used |
| to update the step's presentation on the build status page. The file |
| will be expected to contain a single json object, with any of the |
| following keys: |
| * logs: {logname: [lines]} specifies one or more auxillary logs. |
| * links: {link_name: link_content} to add extra links to the step. |
| * step_summary_text: A string to set as the step summary. |
| * step_text: A string to set as the step text. |
| * properties: {prop: value} build_properties to add to the build |
| status page. Note that these are write-only: The only way to read |
| them is via the status page. There is intentionally no mechanism to |
| read them back from inside of the recipes. |
| * allow_subannotations: allow this step to emit legacy buildbot |
| subannotations. If you don't know what this is, you shouldn't use it. If |
| you know what it is, you also shouldn't use it. |
| """ |
| f = '--output-json' |
| step_name = 'gen step(%s)' % self.m.path.basename(path_to_script) |
| |
| with self.m.context(cwd=self.m.path['checkout']): |
| if str(path_to_script).endswith('.py'): |
| step_result = self.m.python( |
| step_name, |
| path_to_script, list(args) + [f, self.m.json.output()]) |
| else: |
| step_result = self.m.step( |
| step_name, |
| [path_to_script,] + list(args) + [f, self.m.json.output()]) |
| new_steps = step_result.json.output |
| if not isinstance(new_steps, list): |
| # pylint: disable=nonstandard-exception |
| raise self.MalformedStepList(step_name) |
| |
| failed_steps = [] |
| for step in new_steps: |
| diff = set(step) - self.ALLOWED_KEYS |
| if diff: |
| # pylint: disable=nonstandard-exception |
| raise self.UnknownKey(step_name, diff) |
| |
| cmd = step['cmd'] |
| if not all(isinstance(arg, basestring) for arg in cmd): |
| # pylint: disable=nonstandard-exception |
| raise self.MalformedCmd(step_name) |
| |
| outputs_json = step.pop('outputs_presentation_json', False) |
| if outputs_json: |
| # This step has requested a JSON file which the binary that |
| # it invokes can write to, so provide it with one. |
| cmd.extend(['--presentation-json', self.m.json.output(False)]) |
| |
| if not failed_steps or step.get('always_run'): |
| try: |
| cwd = self.m.path.abs_to_path(step['cwd']) if 'cwd' in step else None |
| with self.m.context(env=step.get('env'), cwd=cwd): |
| self.m.step( |
| step['name'], cmd, |
| allow_subannotations=bool(step.get( |
| 'allow_subannotations', False)), |
| ) |
| except self.m.step.StepFailure: |
| failed_steps.append(step['name']) |
| finally: |
| step_result = self.m.step.active_result |
| if outputs_json: |
| p = step_result.presentation |
| j = step_result.json.output |
| |
| if j: |
| p.logs.update(j.get('logs', {})) |
| p.links.update(j.get('links', {})) |
| p.step_summary_text = j.get('step_summary_text', '') |
| p.step_text = j.get('step_text', '') |
| p.properties.update(j.get('properties', {})) |
| |
| if failed_steps: |
| raise self.m.step.StepFailure( |
| "the following steps in %s failed: %s" % |
| (step_name, failed_steps)) |