Chapter 11. Actions

Table of Contents

11.1. Introduction
11.2. Action as a Callback
11.3. Action as an Executable
11.4. Related functionality

11.1. Introduction

When we want to define operations that do not affect the configuration data store, we can use the tailf:action statement in the YANG data model. The action definition specifies how the action is invoked, including input and output parameters (if any). Once defined, the action is available for invocation from all of NETCONF, CLI and Web UI. The action can be implemented either as a callback function or as an executable. Action support is also discussed in Chapter 15, The NETCONF Server, Chapter 16, The CLI agent, and WebUI.

11.2. Action as a Callback

To specify that the action is implemented as a callback in an application Daemon, that registers with ConfD via the C API described in the confd_lib_dp(3) manual page, we use the tailf:actionpoint statement.

The application must register the callback by calling this function:

int confd_register_action_cbs(struct confd_daemon_ctx *dx,
 const struct confd_action_cbs *acb);
 

The struct confd_action_cbs is defined as:

struct confd_action_cbs {
    char actionpoint[MAX_CALLPOINT_LEN];
    int (*init)(struct confd_user_info *uinfo);
    int (*abort)(struct confd_user_info *uinfo);
    int (*action)(struct confd_user_info *uinfo,
                  struct xml_tag *name,
                  confd_hkeypath_t *kp,
                  confd_tag_value_t *params,
                  int nparams);
    int (*command)(struct confd_user_info *uinfo,
                   char *path, int argc, char **argv);
    int (*completion)(struct confd_user_info *uinfo,
                      int cli_style, char *token, int completion_char,
                      confd_hkeypath_t *kp,
                      char *cmdpath, char *cmdparam_id,
                      struct confd_qname *simpleType, char *extra);
    void *cb_opaque;        /* private user data */
};

The actionpoint element gives the name of the actionpoint from the data model, and the init and action elements must point to two callback functions that are called in sequence when the action is invoked. In the init() callback, we must associate a worker socket with the action. This socket will be used for the invocation of the action() callback, which actually carries out the action. Thus in a multi threaded application, actions can be dispatched to different threads.

The action() callback is invoked with parameters pertaining to the action, in particular a hashed Keypath that can identify a particular list instance that the action should be applied to, and an array giving the input parameters. The parameters have the form of an XML instance document conforming to the specification in the input statement in the data model, and are represented as described for the Tagged Value Array format in the section called “XML STRUCTURES” in the confd_types(3) manual page. If the action should return any data values, it must call confd_action_reply_values() with an array of values in the same form, conforming to the specification in the output statement in the data model.

Unlike the callbacks for data and validation, there is no transaction associated with an action callback. However an action is always associated with a user session (NETCONF, CLI, etc), and only one action at a time can be invoked from a given user session.

See the section called “CONFD ACTIONS” in the confd_lib_dp(3) manual page for additional information about the action callbacks.

11.2.1. Example

As an example, we will look at one of several actions implemented in intro/7-c_actions in the ConfD examples collection. The data model defines a list of servers, and an action that allows us to request that a server is reset at some point in time. First, we specify the action in the data model like this:

    list server {
      key name;
      max-elements 64;
      leaf name {
        tailf:cli-allow-range;
        type string;
      }
      tailf:action reset {
        tailf:actionpoint reboot-point;
        input {
          leaf when {
            type string;
            mandatory true;
          }
        }
        output {
          leaf time {
            type string;
            mandatory true;
          }
        }
      }
    }

Our implementation must have an init() callback and an action() callback. The init() callback is straightforward:

static int init_action(struct confd_user_info *uinfo)
{
    int ret = CONFD_OK;

    printf("init_action called\n");
    confd_action_set_fd(uinfo, workersock);
    return ret;
}

I.e. it just tells ConfD that we want the previously connected worker socket to be used for the invocation of the action() callback. The calls to printf(3) in these callback functions are of course only for the purpose of illustration when we run the example. The action() callback, called do_action(), has a twist: we are using the same callback for multiple actions in the data model, and the code looks at the name parameter (the argument to tailf:action in the data model) to decide what to do:

/* This is the action callback function.  In this example, we have a
   single function for all four actions. */
static int do_action(struct confd_user_info *uinfo,
                     struct xml_tag *name,
                     confd_hkeypath_t *kp,
                     confd_tag_value_t *params,
                     int n)
{
    confd_tag_value_t reply[3];
    int i, k;
    char buf[BUFSIZ];
    char *p;

    printf("do_action called\n");

    for (i = 0; i < n; i++) {
        confd_pp_value(buf, sizeof(buf), CONFD_GET_TAG_VALUE(&params[i]));
        printf("param %2d: %9u:%-9u, %s\n", i, CONFD_GET_TAG_NS(&params[i]),
               CONFD_GET_TAG_TAG(&params[i]), buf);
    }

    switch (name->tag) {

Our "reset" action is handled in this branch:

    case config_reset:
        printf("reset\n");
        p = CONFD_GET_CBUFPTR(CONFD_GET_TAG_VALUE(&params[0]));
        i = CONFD_GET_BUFSIZE(CONFD_GET_TAG_VALUE(&params[0]));
        strncpy(buf, p, i);
        buf[i] = 0;
        strcat(buf, "-result");
        i = 0;
        CONFD_SET_TAG_STR(&reply[i], config_time, buf); i++;
        confd_action_reply_values(uinfo, reply, i);
        break;

The when leaf from the input statement in the data model is the first and only parameter, and is available to the callback in params[0]. A real implementation of "reset" would analyze the string, e.g. "now" for immediate reset or a date and time for some point in the future when the reset should be carried out. Here we just append "-result" to the string and return that as a reply for the time leaf in the output statement, by setting the first element in our reply[] array and calling confd_action_reply_values(). Finally we must register the callbacks:

    /* register the action handler callback */
    memset(&acb, 0, sizeof(acb));
    strcpy(acb.actionpoint, "reboot-point");
    acb.init = init_action;
    acb.action = do_action;
    acb.abort = abort_action;

    if (confd_register_action_cbs(dctx, &acb) != CONFD_OK)
        fail("Couldn't register action callbacks");

11.2.2. Using Threads

If we have long-running action callbacks (e.g. file download), it will typically be necessary to use multi-threading for the daemon that handles the callbacks. Without threads, only one invocation of a given action callback can be running at any point in time. Thus if the same action is requested from another user session, the request will block until the currently running invocation has completed.

Even if we do not want to allow multiple invocations of an action callback to run in parallel, having one thread for the control socket and one for the worker socket will make it possible to return an error from the action init() callback when we are "busy" with a running action, instead of having the user wait for the currently running action to complete. It will also allow us to handle other control socket requests promptly. In the general case, where we do want to handle multiple action callback requests in parallel, we need to use multiple worker sockets, with one thread handling each worker socket. We also need one thread to handle the control socket, dispatching the callback requests to the different worker sockets.

The strategy to use for creating and allocating the worker sockets and threads is up to the application, based on the needs for responsiveness in the user interface, resource usage requirements, and other application-specific considerations. We can set up a fixed pool of sockets and threads on startup, or we can connect worker sockets and spawn threads dynamically on demand, as well as close worker sockets and terminate threads that are no longer in use.

The intro/9-c_threads example in the ConfD examples collection demonstrates one such strategy, where worker sockets/threads for action callbacks are created on demand, giving each user session that requests an action its own dedicated action worker thread. The user session stop() callback (see confd_register_usess_cb() in the confd_lib_dp(3) manual page) is used to mark sockets/threads as "idle" and available for assignment as action workers for other user sessions. With this strategy we may need as many threads as there can be concurrent user sessions - by default there is no limit on this, but such limits can be configured in confd.conf (e.g. /confdConfig/sessionLimits/maxSessions for the total number of concurrent user sessions across all northbound interfaces), see the confd.conf(5) manual page.

11.3. Action as an Executable

To specify that the action is implemented as a standalone executable (this could be either a compiled program or a script), that is run to completion on each action invocation, we use the tailf:exec statement. This has several substatements specifying how the executable should be invoked - for the full details, see the tailf_yang_extensions(5) manual page:

tailf:args

A space separated list of argument strings to pass to the executable when it is invoked by ConfD. It may contain variables on the form $(variablename), that are expanded before the command is executed. E.g.

tailf:args "-p $(path)";

will result in the first argument being "-p" and the second being the CLI form (space-separated elements) of the path leading to the action's parent container.

tailf:uid

The user id to use when running the executable.

tailf:gid

The group id to use when running the executable.

tailf:wd

The working directory to use when running the executable. If not specified, the home directory of the user invoking the action is used, except for the case of a CLI session invoked via the confd_cli command - then the directory where confd_cli was invoked is used.

tailf:global-no-duplicate

Specifies that only one instance with the name that is given as argument can be run at any time in the system.

tailf:interrupt

Specifies which signal to use to interrupt the executable when the action invocation is interrupted.

The input parameters are passed to the executable as arguments (following those specified by tailf:args) in the same general form as for a callback invocation. Each tag-value pair results in two arguments, the first is the tag name as a string, the second is the value in string form. The special elements used to indicate the start and end of a list entry or container and a typeless leaf (i.e. C_XMLBEGIN, C_XMLEND, and C_XMLTAG in the C API) have the "values" __BEGIN, __END, and __LEAF. If the action has output parameters, their values should be printed on standard output in the same form.

If the execution is successful, the executable should exit with code 0. Otherwise it may print error information on standard output before exiting with a non-zero code. The error information can be either a free-form string (corresponding to the confd_action_seterr() function for the callback) or structured information corresponding to the NETCONF form described in the section called “EXTENDED ERROR REPORTING” in the confd_lib_lib(3) manual page. It could for example print this on standard output:

error-tag resource-denied
error-message "out of memory"

The error-tag element is required in this case.

In the CLI the action is not paginated by default and will only do so if it is piped to more.

        joe@io> example_action | more
      

11.3.1. Example

Another action in intro/7-c_actions in the examples collection is implemented as a Perl script. Here the data model defines a list of hosts, and an action that allows us to send ping requests to a host. We specify the action in the data model like this:

    list host {
      key name;
      leaf name {
        type string;
      }
      tailf:action ping {
        tailf:exec "./ping.pl" {
          tailf:args "-c $(context) -p $(path)";
        }
        input {
          leaf count {
            type int32;
            default "3";
          }
        }
        output {
          leaf header {
            type string;
          }
          list response {
            leaf data {
              type string;
            }
          }
          container statistics {
            leaf packet {
              type string;
            }
            leaf time {
              type string;
            }
          }
        }
      }
    }

The "-c $(context) -p $(path)" argument for the tailf:args statement has the effect that the context (cli, netconf, etc) and the data model path will be passed to the script, followed by the count parameter from the input statement in the data model. This parameter may have been given by the user or defaulted according to the data model. Thus, if a host called "earth" exists in the configuration, and we use the J-CLI and type this in operational mode:

request config host earth ping count 5

Then the script will be invoked as:

./ping.pl -c cli -p 'config host earth' count 5

The script starts by parsing these arguments, in particular picking up the last word of the -p value for the host name (stored in the $host variable) and the argument for the count parameter (stored in the $count variable):

while ($#ARGV >= 0) {
    if ($ARGV[0] eq "-c") {
        $context = $ARGV[1];
        shift; shift;
    } elsif ($ARGV[0] eq "-p") {
        @path = split(' ', $ARGV[1]);
        shift; shift;
    } elsif ($ARGV[0] eq "count") {
        $count = $ARGV[1];
        shift; shift;
    } else {
        &fail("Unknown argument " . $ARGV[0]);
    }
}
$host = $path[$#path];

In this example, the input parameters are very simple, just a single leaf. If we have lists or containers for input in the data model, the script will receive them with __BEGIN and __END "values", as shown for the output below.

Having collected the required parameters, and taking some OS dependencies into account, the script proceeds to run the actual ping command, collecting the output (standard output and standard error) in the $out variable, and checking the exit code. On a non-zero exit code (indicating failure), the fail function will just print the output from ping on standard output and exit with code 1.

$ENV{'PATH'} = "/bin:/usr/bin:/sbin:/usr/sbin:" . $ENV{'PATH'};
if (`uname -s` eq "SunOS\n") {
    $cmd = "ping -s $host 56 $count";
} else {
    $cmd = "ping -c $count $host";
}
$out = `$cmd 2>&1`;
if ($? != 0) {
    &fail($out);
}

If the execution of ping is successful, the script splits the output into lines and generates a reply according to the output statement in the data model: each leaf is output with the leaf name followed by the value of the leaf, and __BEGIN and __END "values" indicate the the start and end of each entry in the response list, as well as the start and end of the statistics container:

@result = split('\n', $out);
print "header 'Invoked from " . $context . ": " . $result[0] . "'\n";
for ($i = 0; $i < $count; $i++) {
    print "response __BEGIN data '" . $result[$i+1] . "' response __END\n";
}
$packets = $result[$#result-1];
$times = $result[$#result];
print "statistics __BEGIN\n";
print "packet '" . $packets . "' time '" . $times . "'\n";
print "statistics __END\n";

exit 0;

If we run the script interactively, using the command line above, we will get output that looks something like this:

$ ./ping.pl -c cli -p 'config host earth' count 5
header 'Invoked from cli: PING earth.tail-f.com (192.168.1.42): 56 data bytes'
response __BEGIN data '64 bytes from 192.168.1.42: icmp_seq=0 ttl=64 time=0.187 ms' response __END
response __BEGIN data '64 bytes from 192.168.1.42: icmp_seq=1 ttl=64 time=0.150 ms' response __END
response __BEGIN data '64 bytes from 192.168.1.42: icmp_seq=2 ttl=64 time=0.208 ms' response __END
response __BEGIN data '64 bytes from 192.168.1.42: icmp_seq=3 ttl=64 time=0.205 ms' response __END
response __BEGIN data '64 bytes from 192.168.1.42: icmp_seq=4 ttl=64 time=0.204 ms' response __END
statistics __BEGIN
packet '5 packets transmitted, 5 packets received, 0.0% packet loss' time 'round-trip min/avg/max/stddev = 0.150/0.191/0.208/0.022 ms'
statistics __END
        

ConfD will parse this output and deliver the data to the requesting northbound agent. Since the values include whitespace, they must be enclosed in quotes - either single quotes (') as in this example or double quotes (") can be used. Arbitrary whitespace can be used to separate node names and values.

11.4. Related functionality

The action invocation mechanism is also used for some other related purposes:

  • A NETCONF RPC (see Section 15.5, “Extending the NETCONF Server” in Chapter 15, The NETCONF Server) can be specified to invoke a callback or an executable (where ConfD translates the XML), via the tailf:actionpoint and tailf:exec statements, respectively. This is implemented via invocation of the action() callback, or running of an executable, exactly as described above. (When the tailf:raw-xml statement is used with tailf:exec, the argument and result passing described above is not applicable.)

  • The CLI can invoke "capi callbacks" for either complete CLI commands or command completion functionality (see Chapter 16, The CLI agent). This is implemented via invocation of command() and completion() callbacks, respectively, that are registered by the application in the same way as an action() callback. See the section called “CONFD ACTIONS” in the confd_lib_dp(3) manual page for the details.