Writing Advanced Tasks

Difficulty: Intermediate

Time: Approximately 10 minutes

In this exercise you will write a task with metadata.

Prerequisites

Complete the following before you start this lesson:

About Task Metadata

Task metadata files describe task parameters, validate input, and control how tasks are executed. Adding metadata to your tasks helps others use them. You write metadata for a task in JSON and save it with the same name as your task. For example, if you write a task called great_metadata.py its corresponding metadata file is named great_metadata.json.

Writing Your First Task With Metadata

Write a simple task that formats the parameters a user gives it.

Save the following file to Boltdir/site-modules/exercise8/tasks/great_metadata.py:

#!/usr/bin/env python

"""
This script prints the values and types passed to it via standard in.  It will
return a JSON string with a parameters key containing objects that describe
the parameters passed by the user.
"""

import json
import sys

def make_serializable(object):
  if sys.version_info[0] > 2:
    return object
  if isinstance(object, unicode):
    return object.encode('utf-8')
  else:
    return object

data = json.load(sys.stdin)

message = """
Congratulations on writing your metadata!  Here are
the keys and the values that you passed to this task.
"""

result = {'message': message, 'parameters': []}
for key in data:
    k = make_serializable(key)
    v = make_serializable(data[key])
    t = type(data[key]).__name__
    param = {'key': k, 'value': v, 'type': t}
    result['parameters'].append(param)

print(json.dumps(result))

Write the accompanying metadata and save the file to Boltdir/site-modules/exercise8/tasks/great_metadata.json. Specify the parameters as types such as "type": "Integer" which help validate user input as an Integer.

{
    "description": "An exercise in writing great metadata",
    "supports_noop": true,
    "input_method": "stdin",
    "parameters": {
        "name": {
            "description": "The description for the 'name' parameter",
            "type": "String"
        },
        "recursive": {
            "description": "The description for the 'recursive' parameter",
            "type": "Boolean"
        },
        "action": {
            "description": "The description for the 'action' parameter",
            "type": "Enum[stop, start, restart]"
        },
        "timeout": {
            "description": "The description for the 'timeout' parameter",
            "type": "Optional[Integer]"
        }
    }
}

Using Your Task With Metadata

Run ‘bolt task show’ to verify that the task you created appears with its description in the list of available tasks.

bolt task show

The result:

...
exercise8::great_metadata   An exercise in writing great metadata
facts                       Gather system facts
package                     Manage and inspect the state of packages
install_puppet              Install the puppet 5 agent package
...

Run bolt task show <task-name> to view the parameters that your task uses.

bolt task show exercise8::great_metadata

The result:

exercise8::great_metadata - An exercise in writing great metadata

USAGE:
bolt task run --targets <target-name> exercise8::great_metadata name=<value> recursive=<value> action=<value> timeout=<value>

PARAMETERS:
- name: String
    The description for the 'name' parameter
- recursive: Boolean
    The description for the 'recursive' parameter
- action: Enum[stop, start, restart]
    The description for the 'action' parameter
- timeout: Optional[Integer]
    The description for the 'timeout' parameter

MODULE:
project/Boltdir/site-modules/exercise8

Testing Your Task’s Metadata Validation

Bolt can use the types that you have specified in your metadata to validate parameters passed to your task. Run your task with an incorrect value for the action parameter and see what happens.

Run your task and pass the following parameters as a JSON string.

bolt task run exercise8::great_metadata --targets linux --params '{"name":"Popeye","action":"spinach","recursive":true}'

The result:

Task exercise8::great_metadata:
 parameter 'action' expects a match for Enum['restart', 'start', 'stop'], got 'spinach'

Correct the value for the action parameter and run the task again.

bolt task run exercise8::great_metadata --targets target1 --params '{"name":"Popeye","action":"start","recursive":true}'

The result:

Started on target1...
Finished on target1:
  {
    "message": "\nCongratulations on writing your metadata!  Here are\nthe keys and the values that you passed to this task.\n",
    "parameters": [
      {
        "type": "unicode",
        "value": "start",
        "key": "action"
      },
      {
        "type": "unicode",
        "value": "exercise8::great_metadata",
        "key": "_task"
      },
      {
        "type": "unicode",
        "value": "Popeye",
        "key": "name"
      },
      {
        "type": "bool",
        "value": true,
        "key": "recursive"
      }
    ]
  }
Successful on 1 target: target1
Ran on 1 target in 0.97 seconds

Creating a Task That Supports no-operation Mode (noop)

You can write tasks that support no-operation mode (noop). You use noop to see what changes a task would make, but without taking any action.

Create the metadata for the new task and save it to Boltdir/site-modules/exercise8/tasks/file.json:

{
    "description": "Write content to a file.",
    "supports_noop": true,
    "parameters": {
        "filename": {
            "description": "the file to write to",
            "type": "String[1]"
        },
        "content": {
            "description": "The content to write",
            "type": "String"
        }
    }
}

Save the following file to Boltdir/site-modules/exercise8/tasks/file.py. This task uses input from stdin. When a user passes the --noop flag, the JSON object from stdin will contain the _noop key with a value of True.

#!/usr/bin/env python

"""
This script attempts the creation of a file on a target system. It will
return JSON string describing the actions it performed.  If passed "{"_noop": True}"
on stdin it will check to see if it can write the file but not actually write it.
"""

import json
import os
import sys

params = json.load(sys.stdin)
filename = params['filename']
content = params['content']
noop = params.get('_noop', False)

exitcode = 0

def make_error(msg):
  error = {
      "_error": {
          "kind": "file_error",
          "msg": msg,
          "details": {},
      }
  }
  return error

try:
  if noop:
    path = os.path.abspath(os.path.join(filename, os.pardir))
    file_exists = os.access(filename, os.F_OK)
    file_writable = os.access(filename, os.W_OK)
    path_writable = os.access(path, os.W_OK)

    if path_writable == False:
      exitcode = 1
      result = make_error("Path %s is not writable" % path)
    elif file_exists == True and file_writable == False:
      exitcode = 1
      result = make_error("File %s is not writable" % filename)
    else:
      result = { "success": True , '_noop': True }
  else:
    with open(filename, 'w') as fh:
      fh.write(content)
      result = { "success": True }
except Exception as e:
  exitcode = 1
  result = make_error("Could not open file %s: %s" % (filename, str(e)))
print(json.dumps(result))
exit(exitcode)

Test the task with the --noop flag.

bolt task run exercise8::file --targets target1 content=Hello_World filename=/tmp/hello_world --noop

The result:

Started on target1...
Finished on target1:
  {
    "_noop": true,
    "success": true
  }
Successful on 1 target: target1
Ran on 1 target in 0.96 seconds

Run the task again without --noop and see the task create the file successfully.

bolt task run exercise8::file --targets target1 content=Hello_World filename=/tmp/hello_world

The result:

Started on target1...
Finished on target1:
  {
    "success": true
  }
Successful on 1 target: target1
Ran on 1 target in 0.98 seconds

Next Steps

Now that you know how to write task metadata and include the --noop flag you can move on to:

Writing Advanced Plans