Actions are plugins that perform an action on the server or modify server state. These often apply to a certain record type, but they do not have to. These plugins can be executed via CycleServer’s command-line interface, REST API, or the web UI. When executed from the web UI, they are are often invoked from the record table pages that shows a list of records of a given type. Selecting one or more records and clicking a menu button runs the corresponding plugin on the server with the list of selected records.

Each action is defined by a record of type Application.Action that indicates the plugin to run, as well as metadata about the action.

Application.Action Schema

Attribute Value Type Status Description
Name String Required A unique identifier for this action
ForType String Required The type of target record that these actions can be run on
Plugin String Required The plugin to run for this action
Label String Optional The text that appears in the menu items, etc. for this
action. Default to Name
Order Integer Optional Determines the relative order for this action in lists.
Defaults to 1000
Disabled Boolean Optional If true, this action will not be available. Defaults to
false
RecordScope String Optional (new in 6.4) some: acts on one or more records (default);
one: acts on a single record only; none: does not act on records at all
ConfirmationMessage String Optional If present, this message is shown in the UI.
CommentRequired Boolean Optional If true, the user must include a comment describing the
reason for the change. Defaults to false
JavascriptFunction String Optional If present, the Javascript function with this name is executed.
DefaultAction Boolean Optional If true, this action will be the default item in the
list. If multiple actions set this, only the first will
be made the default. Defaults to false
JavascriptConfigFunction String Optional If given, the Javascript function will be called to
collect additional data.
AdsRequired String Deprecated some: some records must be selected; one: one record must
be selected; none : no records must be selected; any:
the number of records selected is not considered. Defaults to
some. Deprecated in 6.4; replaced by RecordScope.

As a shortcut, an action record is automatically created when a plugin is loaded with an Action attribute set to true. Any of the action attributes can be set in the plugin’s configuration by setting attributes under the Action attribute. This can be achieved in plugin configuration file by using an Action. prefix on the attribute:

Action.ConfirmationMessage = Are you sure?

Note

The syntax in versions before 6.4 used Action_ as the prefix.

To make a plugin work for more than one type, you must define a separate action record for each type you wish to support. This lets you control the text and other settings per type.

API Interface

An action plugin must have an execute method that takes an execution record describing the action to take. The following is the schema for the execution record:

Attribute Value Type Status Description
ForType String Required The type of record affected, to make it easier to write
actions that handle more than one type
Filter Expression Required The records to run the action on. This will always include
a constraint on the type (added automatically if needed).
Action String Automatic The action being run, to make it easier to write a
single plugin for more than one action
Comment String Optional The user-defined reason for this action. Actions may
require a comment if needed.
Key Record Optional (new in 6.4) The set of key-value pairs identifying this record,
if the action has a scope of one (see below)
KeyValues List Optional (new in 6.4) The list of key values identifying this record,
if the action has a scope of one (see below)

The plugin can then use that filter to get the records (or some summary of those records) and perform the action based on them.

Extended Interface

The action interface was extended in the 6.4 release to make certain use cases easier. First, actions defined with a scope of one get both Key and KeyValues defined on their execution record (as well as Filter) when called. For instance, an action plugin to edit a single GridJob instance (with key attributes of Scheduler and JobId) might receive the following execution record:

ForType = "GridJob"
Filter = AdType == "GridJob" && Scheduler === "q" && JobId == 123
Key = [Scheduler = "q"; JobId = 123]
KeyValues = {"q", 123}

On the other hand, an action plugin with a scope of some, that cancels any number of GridJob instances, might receive the following execution record:

ForType = "GridJob"
Filter = AdType == "GridJob" && Executable == "bad_command"

Note that while the action plugin will get all of the values (if appropriate), the user does not need to supply all of them. When the user is calling an action with a scope of one or some, they need only supply one of Filter, Key or KeyValues, depending on what is most convenient for the caller. (It is an error to supply more than one attribute.)

If the action has a scope of some, the caller can supply Key or KeyValues, and these will be converted into the corresponding Filter and removed. The caller could alternately supply Filter, which will be left as is. The action plugin will only receive Filter, regardless of how it is called.

If the action has a scope of one, the caller can supply either Key (which will be translated into KeyValues), or KeyValues (which will be translated into Key). In either case, Filter is also set. The caller can instead supply Filter, but this must match a single record, in which case it will be converted into Key and KeyValues from that record.

The action plugin will always receive all three, regardless of how it is called, so that the receiving action plugin can use the one that is most convenient for it.

If the action has a scope of none, the caller cannot supply any of Filter, Key or KeyValues. The action will receive a record without those attributes defined.

RESTful Interface

Actions can be called by POSTing a record as described above to the URL for the action (set by the URIPatterns attribute or a default). Example: this would run the Grid.RemoveJobs action on records of type GridJob belonging to billg:

POST: /exec/action/Grid.RemoveJobs
Content-Type: text/xml

<?xml version="1.0" encoding="UTF-8"?><c>
  <a n="Filter"><e>Owner == "billg"</e></a>
  <a n="Comment"><s>Removing all my jobs</s></a>
</c>

The record can be in any format understood by CycleServer. To use a different format from the default, use ?input_format=your_format in the URL.

Note that the only required attribute is the Filter. (As of 6.4, you can supply Key or KeyValues instead, as described above). Also, the filter need not contain AdType== ForType, because it will be added if it does not exist.

If you want to support a custom URL or request body for your action, see the section below.

URL-only Interface

As a simplification for the common case, in which the record posted only has a Filter attribute, you can POST an empty body to a URL that includes the filter. The filter comes from the standard filter options available on the /db RESTful interface.

Filter Description Example Affects
Key Attributes The key attributes are listed
in order after the URL,
separated by /
/exec/action/Grid.RestartMachine/example.com The machine named
“example.com”
Simple
Matching
Records that have the given
value for the given attribute
/exec/action/Grid.RestartMachine;
Cluster=Dev;Status=Up
All machines where
string(Cluster) == “Dev” &&
string(Status) == “Up”
Full
Expression
Records that match the given
URL-encoded constraint
expression
/exec/action/Grid.RestartMachine?
f=DiskFailures%3E1
All machines where the
DiskFailures attribute is
greater than 1

The simple-matching and full-constraint filters can be combined with each other, but not with the key-attributes filter, since that matches one record by definition.

You can easily make this request with curl, as in this example:

curl -X POST "http://example.com:8080/exec/action/Grid.RestartMachine;Cluster=Dev;Status=Up"

Note: If the request requires authentication you must provide that as well, or alternatively you can create an anonymous action plugin with WebContent=Action and AllowAnonymousAccess=true.

Custom RESTful Interface

CycleServer 6.4 supports custom URLs for your action (ie, one other than /exec/action/{action_name}). First make a plugin that defines an action automatically, and configure it with WebContent="Action" and UriPatterns="/custom/url/here". This will alias your action to /custom/url/here.

Then create a function on your plugin that will parse the request and update the execution object. Name this function for the method it is handling: parse_post(), parse_put(), etc. The most important value to set is one of Filter, Key, or KeyValues. In addition, if your plugin’s configuration does not define an action on it, you must also set the action name (and the type).

The action will be called afterwards with the effective execution record (after doing any substitutions for Filter, Key, or KeyValues). If the request is invalid, call response.setErrorStatus(status, message) and the action will be canceled.

For instance, to make an action that supports canceling a job of type Grid.Job with an empty POST to /api/jobs/cancel?job_id=X, create a config file named job/cancel_action.cfg:

WebContent=action
UriPatterns=/api/jobs/cancel
Action.Name=Cancel:GridJob
Action.ForType=GridJob

(Note that you don’t need to include the Action* configurations if the Action record is defined elsewhere.)

Then include a Python file named job/cancel_action.py:

from application import expressions

def parse_post(req, res, execution):
    job_id = req.parameter("job_id")
    if job_id is None:
        res.setErrorStatus(409, "job_id is required")
    else:
        execution.setExpression("Filter", expressions.attributeEqualsValue("JobId", job_id))

def execute(execution):
    # process the action as normal
    pass

If the job id is not given, the action will be canceled. If it is given, it will be incorporated in the filter.

You can also write a unit test to validate your code:

import actiontest, plugintest
from application import expressions, datastore

class CancelActionTest(actiontest.TestCase):
    def setUp(self):
        super(self.__class__, self).setUp()
        plugintest.loadPlugin("job.cancel_action")

    def test_url(self):
        response, execution = self.parse_post("/api/jobs/cancel?job_id=123")
        self.assertEqual(200, response.status())

        # make sure the filter is constructed properly
        f = expressions.makeFilter(execution.getAsExpression("Filter"))
        self.assertEquals("123", f.extractValueMatch("JobId"))
        # note that this is automatically added on for you
        self.assertEquals("GridJob", f.extractValueMatch("AdType"))

        response, execution = self.parse_post("/api/jobs/cancel")
        self.assertEquals(409, response.status())

This lets you test the request/response handling independent of the actual behavior of the action.

Built-In Actions

CycleServer 6.4 includes virtual Create, Edit, and Delete actions for every type in the system. These are named with the standard convention (eg, Create:Your.Type). When invoked from the UI, Create and Edit render a standard form for creating and editing records of that type, and Delete deletes the records. If you define your own action with that name, it is used in place of the standard action. If you set Disabled=true on the action, then the action will not be be available.

Confirmation

If ConfirmationMessage is defined on an action ad, the message given is displayed to the user when they click on an action link and provides a “Yes/No” dialog box.

Feedback

By default, when the action is executed, the dialog box will say “Performing a/an LABEL on these TYPEPLURAL“. (Note: LABEL comes from the Label attribute on the action and TYPEPLURAL comes from PluralLabel on the type.) To change this, you can add the ActionPhrase attribute to your action, which will be used instead of the above template.

Javascript Actions

Some actions may not directly make any server-side calls, or may require custom Javascript code to run first. To do that, set a JavascriptFunction attribute on the action. In that case, the Javascript function is given a properties object with the following attributes:

Attribute Description
action The action record
type The type record for the target records
filter An expresssion that matches the records to perform the action on
callback A function to call when the action is complete

Example: suppose the JavascriptFunction is “foo.bar.custom”. You would create a Javascript plugin called “foo.bar” (ie, foo/bar.js) and put a function in it called “foo.bar.custom”:

foo.bar.custom = function(properties) {
   alert("Running " + properties.action.get("Name") + " for " + properties.filter);
   callback();
}

Javascript Parameterization

There is a simpler way to perform an action with additional information from the user. Define JavascriptConfigFunction on the action to be the name of a function which gets more information, or just gets confirmation:

foo.bar.custom$ = function(properties, command)
{
    var result = prompt("What value to use?");
    command.set("Value", value);
    deferred.resolve();
}

Any attributes added to command are included in the record sent to the server.