Table of Contents
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.
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.
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(¶ms[i])); printf("param %2d: %9u:%-9u, %s\n", i, CONFD_GET_TAG_NS(¶ms[i]), CONFD_GET_TAG_TAG(¶ms[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(¶ms[0])); i = CONFD_GET_BUFSIZE(CONFD_GET_TAG_VALUE(¶ms[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");
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.
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
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.
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.