Writing juju charms with action support

September 19 2015

Ever since version 1.23, juju support actions - a convenient mechanism of running predefined commands on the managed service. And they are a very useful tool for managing services when configuration changes and relations aren’t enough. Using actions is documented here, so I’m going to talk about writing and testing action-enabled charms.

Defining actions

Before we start implementing actions, we need to tell juju about the actions that are going to be exposed by the charm. This is done in the actions.yaml file, which is basically a yaml map of action names to their properties:

add-user:
  description: Create a new user.
  params:
    user:
      type: string
      description: Name of the user.
    key:
      type: string
      descripion: Public key of the user.
  required: [user, key]
  additionalProperties: false

Here add-user is the name of the action. actions.yaml is parsed according to json-schema, which means that it supports some rather interesting parameter types in addition to the standard types of boolean, integer, number, string. The additional types are object and array. A quick example of how the object type is used can be found here.

Once you’ve defined your actions and deployed your charm, running juju action defined [service] should yield something like this: Juju actions list

Action scripts

Each action is implemented by a script in the actions directory in the charm. To retrieve parameters specified by the user and return results, action scripts have three tools available:

  • action-get
  • action-set
  • action-fail

action-get

action-get [key] is used to retrieve the parameters set by the user when invoking the action. It can take the key as a paremeter or return all parameters set. It’s also useful that the output format can be set using the flag --format to one of json, yaml or smart.

(...)/charm# action-get --format json
{"repo":"somerepo","user":"someuser"}

action-set

action-set key=value is used to return action results to the user. It can be called multiple times during the execution of the action and the end result will be a combination of all the keys set. Though if you set the same key multiple times, only the last value will be returned. Return values can also be grouped into objects:

(...)/charm# action-set bar.a=5 bar.b=6 bar.c="some result"

will result it

➜  ~ % juju action fetch 3b7e105d-686c-461b-873b-70df686c5c97
results:
  bar:
    a: "5"
    b: "6"
    c: some result
status: completed
timing:
completed: 2015-09-21 04:15:04 -0400 -0400
enqueued: 2015-09-21 04:10:45 -0400 -0400
started: 2015-09-21 04:10:47 -0400 -0400

action-fail

Unlike hook executions, returning a non-zero return value from an action script will not result in the action being marked as failed. the charm author needs to execute action-fail ["message"] to mark the action as failed and provide a failure message. Any values set by action-set before marking the action as failed will be returned to the caller.

Actions in charmhelpers

The charmhelpers python library provides a clean way of writing actions as well. Actions can be grouped into on python package for execution using the hookenv.Hooks tool and function decorators:

sys.path.insert(0, os.path.join(os.environ['CHARM_DIR'], 'lib'))
from charmhelpers.core import (
    hookenv,
    host,
)

@hooks.hook("add-user")
@user_action
def add_user(user):
    """
    Add a user.
    """
    key = hookenv.action_get("key")
    psw = ''.join(random.choice(string.ascii_uppercase + string.digits)
                  for _ in range(16))
    host.adduser(user, password=psw, shell='/usr/bin/git-shell')
    host.add_user_to_group(user, GIT_GROUP)
    host.mkdir("/home/%s/.ssh" % (user), owner=user,
               group=user, perms=0o700, force=True)
    host.write_file("/home/%s/.ssh/authorized_keys" % (user), key,
                    owner=user, group=user, perms=0o600)

if __name__ == "__main__":
    hooks.execute(sys.argv)

action-get, action-set and action-fail can be accessed using the hookenv.action_get, hookenv.action_set and hookenv.action_fail methods.

    key = hookenv.action_get("key")
    hookenv.action_set({'users': ', '.join(users)})

Testing actions using amulet

If you want to get your charm community approved, tests are a requirement. The amulet.Deployment class exposes the methods corresponding to the juju cli commands:

  • deployment.action_defined(service)
  • deployment.action_do(unit, action, action_args={})
  • deployment.action_fetch(action_id, timeout=600)

For an example of how charm actions can be tested using amulet, check out my git charm.