Writing Tasks for Taskotron

While the eventual target for most tasks will be to run in a production system, one of libtaskotron’s features is that it can be run without the overhead of a full production-like system.

To learn more about what can be done with taskotron, see What Can I Do With Taskotron?.

Warning

Taskotron is still very young. At the moment, this document is both the direction that we want to take in the near future and features that currently exist in libtaskotron.

If you follow this tutorial, be aware that things will be changing. Do not expect libtaskotron to have a stable API yet and be aware that the interfaces may change drastically with little notice.

Once the interfaces stabilize a bit more, this warning will be edited or removed.

In order to write tasks, you must Install Libtaskotron on your local machine if you aren’t running from git.

Examples

examplebodhi

examplebodhi task git repository

The examplebodhi task is a trivial example which takes an update and determines a PASS/FAIL results in a pseudo-random fashion. This task wasn’t designed to be used in production but is useful as a starting point and for development purposes.

rpmlint

rpmlint task git repository

rpmlint is the simplest of the production tasks. It takes a koji build and runs rpmlint on it, reporting results as TAP13.

Creating a New Task

While running rpmlint is an easy and trivial example, that doesn’t help with creating something new. To write a new task, you’ll need some information:

  • What type of object will the task be run against?
  • What will be the name of the task?
  • What are the dependencies?

The taskotron runner works on specially formatted YAML files (see Taskotron Task YAML Format for details) which describe the task that is to be executed. In addition to the task YAML file, any other code or resources needed for task execution should be accessible by the runner.

A directory layout could look like:

mytask/
    mytask.yml
    mytask.py
    someresource.txt

Note

Taskotron was designed to work with tasks stored in git repostitories and while this is not strictly required for the local execution case, it is highly recommended that you limit tasks to one per directory.

Non-Executed Task Information

For this example, let’s write a task that runs on bodhi ids. Looking at the non-executed task data needed in mytask.yml:

---
name: mytask
desc: do something stupendous
maintainer: somefasuser

input:
    args:
        - arch
        - bodhi_id

environment:
    rpm:
        - libtaskotron

Executed Task Information

Let’s have this new task do the following:

  • download the rpms contained in the indicated bodhi update
  • run the function run_mytask in mytask.py using a list of the downloaded rpms as input
  • report the results to resultsdb
# this is a trivial example that doesn't do everything we want it to do, more
# details will be added later in the example
def run_mytask(rpmfiles, bodhi_id):
    print "Running mytask on %s" % bodhi_id
task:
    # we can use bodhi_id and arch as variables here because they're defined
    # for us by the runner which also verifies that all required input listed
    # above is present before executing the task
    - name: download bodhi update
      bodhi:
          action: download
          bodhi_id: ${bodhi_id}
          arch: ${arch}
      export: bodhi_downloads


    # notice how the third item under python is 'rpmfiles' and this matches
    # the arg name in mytask.py. Anything outside of 'file' and 'callable'
    # are passed in as kwargs to the specified python callable.
    - name: run mytask
      python:
          file: mytask.py
          callable: run_mytask
          rpmfiles: ${bodhi_downloads}
          bodhi_id: ${bodhi_id}
      export: mytask_output


    # this assumes that the output from mytask is valid TAP
    - name: report mytask results to resultsdb
      resultsdb:
          results: ${mytask_output}
          checkname: 'mytask'

We can execute this new task using:

$ python runtask.py -i foo-1.2-3.fc99 -t bodhi_id -a x86_64 ../task-mytask/mytask.yml

The output from this task should end with:

Running mytask on foo-1.2-3.fc99.x86_64.rpm

[libtaskotron:logger.py:34] 2014-05-14 21:30:30 CRITICAL Traceback (most recent call last):
  File "runtask.py", line 4, in <module>
    runner.main()
  File "/home/tflink/code/taskotron/libtaskotron/libtaskotron/runner.py", line 192, in main
    task_runner.run()
  File "/home/tflink/code/taskotron/libtaskotron/libtaskotron/runner.py", line 31, in run
    self.do_actions()
  File "/home/tflink/code/taskotron/libtaskotron/libtaskotron/runner.py", line 105, in do_actions
    self.do_single_action(action)
  File "/home/tflink/code/taskotron/libtaskotron/libtaskotron/runner.py", line 94, in do_single_action
    self.envdata)
  File "/home/tflink/code/taskotron/libtaskotron/libtaskotron/directives/resultsdb_directive.py", line 136, in process
    raise TaskotronDirectiveError("Failed to load 'results': %s"  % e.message)
TaskotronDirectiveError: Failed to load 'results': Failed to parse TAP contents: Missing plan in the TAP source

We’re not actually passing TAP output from our task, so let’s get that fixed.

Actually Doing Something

The Python code for mytask.py above doesn’t really do anything at all, so let’s make it do something:

  • Change the task to create a result from the input bodhi update id and list all of the rpms contained in that update as details.

There is a bit of complexity to the TAP13 specification but generating TAP from python tasks in Taskotron isn’t very difficult.

Our original task looked like:

# this is a trivial example that doesn't do everything we want it to do, more
# details will be added later in the example
def run_mytask(rpmfiles):
    for rpmfile in rpmfiles:
        print "Running mytask on %s" % rpmfile

To create valid TAP, we want to populate a libtaskotron.check.CheckDetail object and use libtaskotron.check.export_TAP() to generate valid TAP13 with the data required for reporting.

from libtaskotron import check

def run_mytask(rpmfiles, bodhi_id):
    """run through all passed in rpmfiles and emit TAP13 saying they all passed"""

    print "Running mytask on %s" % bodhi_id

    details = []
    result = 'PASSED'
    summary = 'mycheck %s for %s' % (result, bodhi_id)
    detail = check.CheckDetail(bodhi_id, check.ReportType.BODHI_UPDATE,
                                result, summary)
    for rpmfile in rpmfiles:
        detail.store(rpmfile, False)

    return check.export_TAP(detail)

Now if we run the task we get output that ends with:

[libtaskotron:resultsdb_directive.py:123] 2014-05-15 09:17:44 INFO    Reporting to ResultsDB is disabled.
INFO:libtaskotron:Reporting to ResultsDB is disabled.
[libtaskotron:runner.py:198] 2014-05-15 09:17:44 INFO
TAP version 13
1..1
ok - $CHECKNAME for Bodhi update openvswitch-2.1.2-1.fc19
  ---
  details:
    output: "foo-1.2-3.fc99.x86_64.rpm"
  item: foo-1.2-3.fc99
  outcome: PASSED
  summary: mycheck PASSED for foo-1.2-3.fc99
  type: bodhi_update
  ...

Note

Reporting is disabled in the default configuration for Taskotron. While it is possible to set up a local resultsdb instance to check reporting, we want to make it easier than that and will update these docs when that easier method is available.

For now, if the resultsdb directive doesn’t throw exceptions, it’s reasonable to assume that the TAP output was constructed correctly and is reasonably valid.