Chapter 10. Transformations, Hooks, Hidden Data and Symlinks

Table of Contents

10.1. Introduction
10.2. Transformation Control Flow
10.3. An Example
10.4. AAA Transform
10.5. Other Use Cases for Transformations
10.6. Hooks
10.7. Hidden Data
10.8. tailf:symlink

10.1. Introduction

When building new variants of an old product, we often have a situation where we have large amounts of application code, which reads configuration data from some datastore and we do not want to make any changes to the application code, we merely wish to expose a different view of the same configuration data.

Another common situation is when we have application code which requires more configuration data than we wish to expose through the northbound management interfaces. The application reads and use a number of configuration items that do not make sense to expose through the different management interfaces.

In general, when the actual configuration data differs from what we wish to expose as management data, we can use a variety of different techniques in ConfD.

In this chapter we describe the following four different techniques that all are used to somehow show different views of the system.

  • Transforms are used to show a portion of the system in a different way than the original data model.

  • Hooks are used to execute user code whenever a part of the configuration is changed.

  • Hidden Data is means to hide parts of the configuration.

  • Symlinks are pointers in the data model effectively making one part of the data model appear at a different place.

Some or several of the above features are often the required trick when we wish to accomplish certain modifications of the data model. However, combining these features, can make the system complex. A clean YANG data model is easy to understand - whereas a system consisting of hidden transformations with symlinks here and there, can very easy become hard to understand. So - use these features with caution.

10.2. Transformation Control Flow

ConfD uses the data model, i.e. the YANG files to render all the northbound interfaces, the layout of the datamodel is exactly reflected in the CLI and the Web UI. Thus, unfortunately, we may sometimes be forced to manipulate the data model in order to have a desired look and feel in the CLI or the Web UI. In these cases, a transform may be required trick.

A transformation works as follows. We start out with the YANG model we wish to transform. We may add tailf:export maapi to that YANG module. This makes the YANG model invisible to all management agents except MAAPI. Although invisible, the YANG model is loaded into the system and regardless of whether the YANG model is populated through cdb or an external database it is fully operational and accessible through the MAAPI APIs as well as through the CDB APIs (assuming the YANG model is populated by CDB).

Following that, we write another YANG model which represents the management data we do wish to expose to the northbound management agents. This YANG model has a callpoint which uses the attribute tailf:transform. Finally we must write a program which acts as an external database, serving data for a callpoint. This program should use the MAAPI API to read and write the real YANG model data which is used by the managed objects.

10.3. An Example

Assume we have the following YANG model:

Example 10.1. full.yang

container full {
  leaf firstname {
    type string;
    default George;
  }
  leaf a_number {
    type int64;
    default 42;
  }
  leaf b_number {
    type int64;
    default 7;
  }
  container servers {
    list server {
      key name;
      max-elements 64;
      leaf name {
        type string;
      }
      leaf ip {
        type inet:host;
        mandatory true;
      }
      leaf port {
        type inet:port-number;
        mandatory true;
      }
    }
  }
}
       

For some reason we think that this YANG model is way too complicated to expose through the management interfaces. We have also invested time and energy in various applications that read and use precisely this data and we do not want to change any of those applications.

What we want to do is to expose a YANG model which looks like:

Example 10.2. small.yang

container small {
  container servers {
    tailf:callpoint transcp {
        tailf:transform true;
    }
    list server {
      key name;
      max-elements 64;
      leaf name {
        type string;
      }
    }
  }
}
         

I.e. skip all the first toplevel elements, and skip the ip and the port. We write C code which derives both. Also note the transformation callpoint.

When ConfD needs to read and write data in the small.yang YANG model, it will use the callpoint transcp. I.e. it will invoke the installed callback functions for that callpoint. The main difference between a normal callpoint and a transformation callpoint is when write and when validation occurs. In a transformation callpoint the write operations occur before validation. This means that any data that is written through MAAPI in the actual transform, will also be validated.

Similar to callpoints for external data, we need a worker socket a control socket and registered callbacks. Our main() function together with some global variables would look like:

#include "full.h"  /* generated .h files */
#include "small.h"

static struct confd_daemon_ctx *dctx;
static int ctlsock;
static int workersock;
struct confd_trans_cbs tcb;
struct confd_data_cbs  data;
static int maapi_socket;


int main()
{

    struct in_addr in;
    struct sockaddr_in addr;

    confd_init("MYNAME", stderr, debuglevel);
    if ((dctx = confd_init_daemon("mydaemon")) == NULL)
        confd_fatal("Failed to initialize confd\n");

    if ((ctlsock = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
        confd_fatal("Failed to open ctlsocket\n");

    inet_aton("127.0.0.1", &in);
    addr.sin_addr.s_addr = in.s_addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(CONFD_PORT);
    confd_load_schemas((struct sockaddr*)&addr,sizeof (struct sockaddr_in))l
    if (confd_connect(dctx, ctlsock, CONTROL_SOCKET, (struct sockaddr*)&addr,
                      sizeof (struct sockaddr_in)) < 0)
        confd_fatal("Failed to confd_connect() to confd \n");

    if ((workersock = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
        confd_fatal("Failed to open workersocket\n");
    if (confd_connect(dctx, workersock, WORKER_SOCKET,(struct sockaddr*)&addr,
                      sizeof (struct sockaddr_in)) < 0)
        confd_fatal("Failed to confd_connect() to confd \n");


    tcb.init = init_transformation;
    tcb.finish = stop_transformation;
    confd_register_trans_cb(dctx, &tcb);


    data.get_elem = get_elem;
    data.get_next = get_next;
    data.set_elem = set_elem;
    data.create   = create;
    data.remove   = dbremove;
    data.exists_optional = NULL;
    strcpy(data.callpoint, "transcp");

    if (confd_register_data_cb(dctx, &data) != CONFD_OK)
        confd_fatal("Failed to register data cb \n");

    if (confd_register_done(dctx) != CONFD_OK)
        confd_fatal("Failed to complete registration \n");

    setup_maapi_sock(&maapi_socket));

    .........
       

The above is precisely the same setup as when we register callbacks for an external database with the only exception that the callpoint uses tailf:transform true.

The code to establish the MAAPI socket is just a call to maapi_connect().

The difference comes in the implementation of the data callbacks, with an external database, we have the data to deliver, in the case of a transformation callpoint, we don't have the data. The data resides inside ConfD and we can read and write that data inside the same transaction using maapi_attach().

The initialization looks like:

static int init_transformation(struct confd_trans_ctx *tctx)
{
    maapi_attach(maapi_socket, full__ns, tctx);
    confd_trans_set_fd(tctx, workersock);
    return CONFD_OK;
}

static int stop_transformation(struct confd_trans_ctx *tctx)
{
    if (tctx->t_opaque != NULL) {
        struct maapi_cursor *mc = (struct maapi_cursor *)tctx->t_opaque;
        maapi_destroy_cursor(mc);
        free(tctx->t_opaque);
    }
    maapi_detach(maapi_socket, tctx);
    return CONFD_OK;
}
       

Whenever a transaction starts, we get called - as usual - in our init() callback and whenever the transaction terminates, regardless of the outcome of the transaction, we get called in our finish() callback. Here we attach to the executing transaction using maapi_attach() in the init() callback. We will use the attached MAAPI socket with the right transaction handle in all our data processing callbacks. In the finish() callback we need to release any memory used for a MAAPI cursor, see get_next() below.

The get_elem() callback is interesting. The path we get queried with is /small/servers/server{key}/name which doesn't exist in the real database. What does exist as proper data though is the path /full/servers/server{key}/name and we use the MAAPI socket to read that value from the "hidden" YANG model.

static int get_elem(struct confd_trans_ctx *tctx,
                    confd_hkeypath_t *keypath)

{

    confd_value_t v;
    confd_value_t *leaf = &(keypath->v[0][0]);
    confd_value_t *vp = &(keypath->v[1][0]);
    switch (CONFD_GET_XMLTAG(leaf)) {
    case small_name:
        if (maapi_get_elem(maapi_socket, tctx->thandle, &v,
                           "/full/servers/server{%x}/name",  vp) == CONFD_OK) {
            confd_data_reply_value(tctx, &v);
            free(CONFD_GET_BUFPTR(&v));
            return CONFD_OK;
        }
        else if (confd_errno == CONFD_ERR_NOEXISTS) {
            fprintf(stderr, "\nNOT FOUND \n");
            confd_data_reply_not_found(tctx);
            return CONFD_OK;
        }
        else {
            fprintf (stderr, "errno = %d\n", confd_errno);
            return CONFD_ERR;
        }
    default:
        return CONFD_ERR;
    }
}
       

It is important that we check confd_errno to distinguish between the real error cases and the case where the element doesn't exist. Also note how we format the path to the maapi_get_elem() call using the second element in the keypath. This will be the key since the path will be /small/servers/server{key}/name.

set_elem() will never be called since our MO server only contains a key and no other elements.

The callback get_next() is also interesting. Here we utilize a MAAPI cursor to iterate through the different "full" servers. Since the MAAPI cursor must exist across calls to get_next(), and we must be able to handle multiple transactions (from different user sessions) in parallel, we allocate the cursor dynamically and use the t_opaque element in the transaction context to keep track of it.

static int get_next(struct confd_trans_ctx *tctx,
                    confd_hkeypath_t *keypath,
                    long next)
{
    struct maapi_cursor *mc;

    if (next == -1) {
        if (tctx->t_opaque == NULL) {
            /* allocate the cursor */
            mc = (struct maapi_cursor *)malloc(sizeof(struct maapi_cursor));
            tctx->t_opaque = mc;
        } else {
            /* re-use previously allocated cursor */
            mc = (struct maapi_cursor *)tctx->t_opaque;
            maapi_destroy_cursor(mc);
        }
        maapi_init_cursor(maapi_socket, tctx->thandle, mc,
                          "/full/servers/server");
    } else {
        mc = (struct maapi_cursor *)tctx->t_opaque;
    }
    maapi_get_next(mc);
    if (mc->n == 0) {
        confd_data_reply_next_key(tctx, NULL, -1, -1);
        return CONFD_OK;
    }
    confd_data_reply_next_key(tctx, &(mc->keys[0]), 1, 1);
    return CONFD_OK;
}
       

Finally we have the delete() and create() callbacks. The delete() callback is completely straightforward where we simply delete the same element from the hidden "full" YANG model. The create() callback needs to do a bit of work. The "full" YANG model contains two elements that are not part of the "small" YANG model. Thus when a manager creates a new element in the "small" YANG model, it is the responsibility of our code here to create the corresponding element in the "full" YANG model, but also to populate the additional two elements with values. If we fail to do that the commit will fail since values in the "full" YANG model are unset.

The code to delete and create:

static int dbremove(struct confd_trans_ctx *tctx,
                    confd_hkeypath_t *keypath)
{
    maapi_delete(maapi_socket, tctx->thandle,
                 "/full/servers/server{%x}", &(keypath->v[0][0]));
    return CONFD_OK;
}


static int create(struct confd_trans_ctx *tctx,
                  confd_hkeypath_t *keypath)
{
    /* this is where we have to do extra, we need to also populate */
    /* the ip and ports fields in full.cs */
    char buf[BUFSIZ];
    confd_value_t *key = &(keypath->v[0][0]);
    struct servent *srv;
    maapi_create(maapi_socket, tctx->thandle,
                 "/full/servers/server{%x}", key);
    maapi_set_elem2(maapi_socket, tctx->thandle,
                    "0.0.0.0",
                    "/full/servers/server{%x}/ip",key);
    /* NUL terminate string */
    memcpy(buf, CONFD_GET_BUFPTR(key), CONFD_GET_BUFSIZE(key));
    buf[CONFD_GET_BUFSIZE(key)] = 0;

    if ((srv = getservbyname(buf, NULL)) == NULL) {
        char tbuf[BUFSIZ];
        sprintf(tbuf, "Unknown service %s", buf);
        confd_trans_seterr(tctx, tbuf);
        return CONFD_ERR;
    }
    sprintf(buf, "%d", srv->s_port);
    maapi_set_elem2(maapi_socket, tctx->thandle, buf,
                    "/full/servers/server{%x}/port",key);
    return CONFD_OK;
}
       

10.4. AAA Transform

The ConfD AAA YANG model is a very good example of where we may wish to expose a different set of configuration items to the management stations than what exists in the AAA YANG model tailf-aaa.yang. The AAA system is described in Chapter 14, The AAA infrastructure.

The data in the AAA YANG model is used by ConfD itself and all that data including the fairly complicated authorization rules must be there for ConfD to read. We think that very few devices wish to expose e.g. the authorization rules from tailf-aaa.yang to end users. The solution to this is to use a transformation.

In the ConfD examples collection, we have an example which exposes a very simple AAA model. The simple AAA YANG model looks as:

Example 10.3. users.yang

module users {
  namespace "http://www.example.com/ns/users";
  prefix u;

  import tailf-common {
    prefix tailf;
  }

  typedef Role {
    type enumeration {
        enum admin;
        enum oper;
    }
  }
  typedef passwdStr {
    type tailf:md5-digest-string {
    }
  }
  container users {
    tailf:callpoint simple_aaa {
        tailf:transform true;
    }
    list user {
      key name;
      max-elements 64;
      leaf name {
        type string;
      }
      leaf password {
        type passwdStr;
        mandatory true;
      }
      leaf role {
        type Role;
        mandatory true;
      }
    }
  }
}
       

This YANG model just exposes a list of users. Each user has a password and an enum indicating the role of the user. There are only two static roles to choose from, admin and oper.

If we also have intimate knowledge of the datamodel we are using, it is possible to generate static authorization rules.

Let's take a look at the get_elem() callback. The task of this callback is to use MAAPI in order to populate the simple YANG model from above.

static int get_elem(struct confd_trans_ctx *tctx,
                    confd_hkeypath_t *keypath)

{

    confd_value_t v;
    confd_value_t *leaf = &(keypath->v[0][0]);
    confd_value_t *vp = &(keypath->v[1][0]);
    switch (CONFD_GET_XMLTAG(leaf)) {
    case aaa_simple_name:
        if (maapi_get_elem(
                maapi_socket, tctx->thandle, &v,
                "/aaa/authentication/users/user{%x}/name",  vp) == CONFD_OK) {
            confd_data_reply_value(tctx, &v);
            free(CONFD_GET_BUFPTR(&v));
            return CONFD_OK;
        }
        else if (confd_errno == CONFD_ERR_NOEXISTS) {
            confd_data_reply_not_found(tctx);
            return CONFD_OK;
        }
        else {
            printf ("errno = %d\n", confd_errno);
            return CONFD_ERR;
        }
    case aaa_simple_password:
        if (maapi_get_elem(
                maapi_socket, tctx->thandle, &v,
                "/aaa/authentication/users/user{%x}/password", vp)==CONFD_OK) {
            confd_data_reply_value(tctx, &v);
            free(CONFD_GET_BUFPTR(&v));
            return CONFD_OK;
        }
        else if (confd_errno == CONFD_ERR_NOEXISTS) {
            confd_data_reply_not_found(tctx);
            return CONFD_OK;
        }
        else {
            fprintf (stderr, "errno = %d\n", confd_errno);
            return CONFD_ERR;
        }
    case aaa_simple_role: {
        int ret;
        char users[BUFSIZ];
        char user[256];
        memcpy(&user[0], CONFD_GET_BUFPTR(vp), CONFD_GET_BUFSIZE(vp));
        user[CONFD_GET_BUFSIZE(vp)] = 0;

        ret = maapi_get_str_elem(
            maapi_socket, tctx->thandle, users, BUFSIZ,
            "/aaa/authentication/groups/group{admin}/users");
        if (strstr(users, user) != NULL) {
            CONFD_SET_ENUM_VALUE(&v, aaa_simple_admin);
            confd_data_reply_value(tctx, &v);
            return CONFD_OK;
        }
        else {
            maapi_get_str_elem(
                maapi_socket, tctx->thandle, users, BUFSIZ,
                "/aaa/authentication/groups/group{oper}/users");
            if (strstr(users, user) != NULL) {
                CONFD_SET_ENUM_VALUE(&v, aaa_simple_oper);
                confd_data_reply_value(tctx, &v);
                return CONFD_OK;
            }
        }
        /* user not part of any group at all */
        confd_data_reply_not_found(tctx);
        return CONFD_OK;
    }
    default:
        confd_fatal("Unexpected switch tag %d\n",
                    CONFD_GET_XMLTAG(leaf));
    }
    return CONFD_ERR;
}
       

10.5. Other Use Cases for Transformations

We may also envision a use case where we wish to expose more data than is available in the YANG model. In this case the task of the transformation would be to aggregate the data and write into MAAPI.

Yet another use case for transformations would be when we wish to expose two variants of the same config, one for novices and one for experts. In this case we have the full YANG model with all the details exposed to experts and a simplified version which fills in many reasonable default values and possibly also derives data, exposed to novices. The YANG model for novices would then be populated by a transformation.

One common transformation is to logically move an entire subtree of some data model to some other place. For example, ConfD's AAA data model is named /aaa, but suppose we want to access it through /system/advanced/aaa instead. This can be done by using a transform as described above, or it can be done using a symlink, which essentially is a specialized, built-in transform.

Finally when we want to implement support for standard SNMP mibs while at the same time use a proper hierarchical high level data model for all other north bound interfaces we must use a transform on the SNMP data. To implement this we compile the mib into a YANG model document using the smidump tool. We must then also annotate the YANG model derived from the MIB and set a transformation point at the top. Thus when the SNMP agent tries to read data from the MIB (through the YANG model) our transformation C code gets invoked and we can then, over the maapi interface, read the right data from the high level data model and return that data to the SNMP agent.

10.6. Hooks

A hook is a function that is invoked within the transaction when an object is modified. The hook function has access to the transaction, so it can modify other objects in the transaction as necessary.

A hook is a way for the application to participate in a transaction. A hook is like a callpoint or a validation point only that the application gets to attach (using maapi) to the transaction and can write more data.

For example if we have an optional container containing a set of items, whenever the container is created, our hook gets called and the container can be populated with proper values. This effect is also achieved by letting the container elements have default values. The "default value" solution is compile time, whereas a populating the container through a hook is obviously runtime.

Hooks can also be used to attach some magic to individual elements. Say that we have a leaf:

leaf magic {
   type int32;
}
    

Whenever the magic leaf get set to, say -1, our hook code performs some other arbitrary write operations.

Thus the hook mechanism can be used to achieve a wide variety of effects.

A hook is implemented similar to a callpoint with the exception that only write callbacks need to be implemented. The write callbacks are set_elem(), create(), remove(), set_case(), set_attr(), and move_after(). However a hook only needs to implement the write callbacks that it actually needs for its own use - the others can be left unimplemented, indicated by setting them to NULL in the callback registration.

There are two types of hooks, set hooks and transaction hooks. The main difference between the two is when they are invoked. Set hooks are invoked directly as an object is modified, and transaction hooks are invoked in the two-phase commit phase. The ConfD transaction engine receives all the original write operations from one of the north bound agents. Once all write operations have been received, i.e. when a user for example types "commit" in the CLI, the transaction engine invokes the relevant transaction hooks. Once all transaction hooks are run the validation phase is entered, thus the write operations performed by the transaction hooks are also validated.

When set hooks make changes to the configuration, these changes are just like changes done directly by the user - they will be committed together with other changes, e.g. when the user types "commit" in the CLI. If such a commit operation cannot be completed, due to validation errors, the changes done by set hooks will remain in the change set until explicitly reverted.

Changes done by transaction hooks are different, in that ConfD keeps track of them, and rolls them back in case a commit operation cannot be completed. This makes it possible for the user to fix validation errors and attempt to commit again.

The hook is specified similar to callpoint if we have:

list dyn {
  key name;
  max-elements 64;
  tailf:callpoint foocp {
      tailf:transaction-hook subtree;
  }
  leaf name {
    type string;
  }
  leaf aval {
    type string;
    mandatory true;
  }
  leaf container {
    type empty;
  }
}
    

The statement: tailf:transaction-hook subtree; indicates that we wish to attach a hook to this part of the data model. A set hook uses the statement tailf:set-hook instead. Similar to a validator, the hook code will participate in the transaction by calling maapi_attach() and similar to a data provider, the hook code must register its callbacks through calls to confd_register_trans_cb() and confd_register_data_cb(). Only those of the possible write callbacks that are actually needed by the hook need to be registered as data callbacks. All other callbacks must be set to NULL.

For example our set_elem() callback could look like:

static int my_set_elem(struct confd_trans_ctx *tctx,
                       confd_hkeypath_t *keypath,
                       confd_value_t *newval)
{
    confd_value_t *tag = &(keypath->v[0][0]);
    confd_value_t *key = &(keypath->v[1][0]);

    if (confd_svcmp("donk", key) == 0) {
        maapi_setelem2(maapisock, tctx->thandle,"222", "/foo/bar");
        .....
      

So whenever some north bound agent assigns the value "donk" to /dyn{key}/aval for all values of key our code kicks in and additionally assigns a value to /foo/bar.

We can have three different kinds of hooks.

  1. subtree - this assigns the hook code to all objects found below where the hook is defined. The value "true" is the same as "subtree".

  2. object - This is used when we wish to assign a hook to the manipulation of list entries. The hook reaches down to and including the list where it is defined. If there exists further lists further down in the tree they are not affected by the hook.

  3. node - This is used when we wish to assign a hook an optional container and only that. It affects the container but non of its children.

In some cases a transaction hook may need to update the transaction in a way that really depends on the complete configuration, rather than on the changes done in the current transaction, making it difficult to implement the hook via the create(), set_elem(), etc callbacks. We can then use the tailf:invocation-mode substatement to tailf:transaction-hook, like this:

  tailf:callpoint foocp {
    tailf:transaction-hook subtree {
      tailf:invocation-mode per-transaction;
    }
  }
    

The per-transaction argument tells ConfD that this hook should only have one data callback invocation, regardless of the details of the changes to the objects the hook is assigned to. We can even use the same callpoint name, with the same tailf:invocation-mode statement, at several points in the data model, and still only get one callback invocation. The data callback that gets invoked for a transaction hook specified like this is called write_all() (see the confd_lib_dp(3) manual page). It is thus the only callback that should be registered for such a hook.

Since hook code gets to execute for all the possible write callbacks, the number of use cases for hook code is very large. One common use case is once again associated to the implementation of standard MIBS. Depending on the nature of the chosen standard MIBs, we may need to maintain mapping tables. If for example the keys differ in the SNMP table from the high level data model we may need to maintain additional mapping tables that are maintained by hook code.

10.6.1. Set Hooks and Candidate Configuration

When ConfD has been configured to provide a candidate configuration, set hook code will be invoked when changes are done to the candidate configuration, while transaction hooks will be invoked when the candidate is committed to running.

There are situations when you only want your hook to modify the running configuration:

  • A hook can use maapi to modify config elements that the operator is not allowed to modify directly, according to the active aaa rule set. If such a modification is done on the candidate configuration store, the operator will not be allowed to commit the candidate configuration to the running configuration. In this situation you must thus use a transaction hook to modify the configuration.

10.7. Hidden Data

It is sometimes useful to hide nodes from some of the northbound interfaces. The tailf:export statement can be used to hide an entire namespace. More fine grained control can be attained with the tailf:hidden statement.

The tailf:hidden statement names a hide group, i.e. all containers and leafs that has the tailf:hidden statement, with a specific hide group, are treated the same way as far as being hidden or invisible. The hide group name full is given a special meaning. The full hide group is hidden from all northbound interfaces, not just user interfaces.

A node with the tailf:hidden statement must be optional or have a default value if it can be implicitly created via the creation of a differently hidden node higher up in the hierarchy (e.g. hidden leafs in a non-hidden list entry).

A related situation is when some nodes should be displayed to user only when a certain condition is met. For example, the ethernet subtree should be displayed only when the type of an interface is ethernet. This can be achieved through the tailf:display-when statement.

10.7.1. Fully Hidden Nodes

This is nodes that may be useful for the application code, but should be hidden from all northbound interfaces. An example is the set of physical network interfaces on a device and their types. This is static data, i.e. it can't be changed by configuration, but it can vary between different models of a device that run the same software, and the device-specific data can be provided via init file or through MAAPI.

This type of data could also be realized via a separate namespace where tailf:export is used to limit the visibility, but being able to have some nodes in the data model hidden while others are not allows for greater flexibility - e.g. list entries in the config data can have hidden containers or leafs, which get instantiated automatically along with the visible config nodes.

10.7.2. Hiding nodes from User Interfaces

This is data that is fully visible to programmatic northbound interfaces such as NETCONF, but normally hidden from user interfaces such as CLI and Web UI. Examples are data used for experimental or end-customer-specific features, similar to hidden commands in the CLI but for data nodes.

A user interface may give access to this type of data (and even totally hidden data) if the user executes an unhide command identifying the set of hidden data that should be revealed. After this these data nodes appear the same as unhidden data, i.e. they are included in tab completion, listed by show commands etc.

A hide group can only be unhidden if the group is listed in the confd.conf file. This means that a hide group will be completely hidden to the user interfaces unless it has been explicitly allowed to be unhidden in the confd.conf file. A password can optionally be required to unhide a group.

<hideGroup>
   <name>debug</name>
   <password>secret</password>
</hideGroup>
          

10.7.3. Conditional Display

A typical usage example is a discriminated union. One leaf is the type of something, and depending on the value of this leaf, different containers are visible:

container service {
    leaf type {
        default http;
        type enumeration {
            enum http;
            enum smtp;
        }
    }
    choice service-type {
        container http {
            presence "HTTP enabled";
            tailf:display-when '/service/type = "http"';
            leaf addr {
                mandatory  true;
                type inet:ipv4-address;
            }
            leaf docroot {
                mandatory  true;
                type string;
            }
        }
        container smtp {
            presence "SMTP enabled";
            tailf:display-when '/service/type = "smtp"';
            leaf smtp-relay {
                mandatory  true;
                type boolean;
            }
            leaf use-virtual-mbox {
                type boolean;
            }
        }
    }
}

          

In this example, the "smtp" container should be visible to the user only when the value of service-type is smtp.

This can be accomplished by using the tailf:display-when statement. It contains an XPath expression which specifies when the node should be displayed:

10.8. tailf:symlink

NOTE: The usage of symlink is not recommended. If it is used, use it carefully. It can be made to work in some special cases, but some other use cases, described below, are problematic. In the problematic cases, tailf:link can often be used instead. See Section 10.8.1, “Discussion” for more details.

Sometimes we want to move things in our data models. One major downside of moving structures around in the data model is that all code that reads data from CDB has to be changed. Sometimes this is not feasible because we do not want to change this code. In this case the symlink feature may be a solution.

Another situation where symlinks might be the remedy is when we want to move things in the data model due to aesthetics in the human interfaces, the CLI and the Web UI that are both rendered from the data model.

The syntax for a symlink is straightforward:

container top {
  tailf:symlink foo {
    tailf:path "/baz/bar";
  }
}

container baz {
  container bar {
    leaf target {
      type string;
      default "Zappa";
    }
  }
}

With the above construct we create a link from /top/foo to /baz/bar.

The net result of the link is that it will appear as if all data found in the data model below the link target, e.g. /baz/bar will appear under the link source, e.g. /top/foo. Thus if prior to the symlink the path /baz/bar{13}/server{www}/port was a valid key path, now the path /top/foo/bar{13}/server{www}/port point to the same configuration item. Hence we now have two different ways to configure the same item. The remedy to that is is typically to hide the the target configuration from the north bound agents using the tailf:hidden or tailf:export statements.

The textual format of a symlink is an XPath absolute location path.

The target of a symlink can contain instantiated keys. Using XPath notation we could point to the counters element in a specific server as in :

tailf:symlink mycounter {
  tailf:path "/servers/server[ip='10.0.0.1'][port=80]'
           + "/counter";
}

Slightly more advanced is that the target can refer to keys in the source. If the target lies within a list, all keys must be specified. A key either has a value, or is a reference to a key in the path of the source element, using the function current() as starting point for an XPath location path. Example of a valid target for a symlink is:

/servers/server[ip='10.0.0.1'][port=current()/../myport]/mycounter

This feature must be used when we want to use keys in our own path down the XML tree and use those very same keys to find an other way down the XML tree.

At link time, confdc checks that the target of the symlink can actually exist at run-time. A symlink may point to another symlink. confdc checks at link time, and ConfD at load time, that no cycles exist. Dangling pointers may occur though. If we have a symlink that points into an optional container or into a list entry, the target container can be removed. It is up to the application developers to check that dangling pointers do not occur. If they do, and the symlink is accessed, this is logged in the developer log as an error.

Finally we need to mention that when we are symlinking into another namespace, we must indicate that using the default XML notation for referencing elements in other namespaces. Thus if we want to have a symlink into the http://example.com/ns/servers namespace we typically have to import the module defining that namespace, and use the prefix of that module in the symlink as in:

import servers {
  prefix srv;
}
...

tailf:symlink myserver {
  tailf:path "/srv:servers/srv:server";
}

10.8.1. Discussion

Northbound Client Applications

The YANG modules that the device publishes to the northbound clients define the API between the client and the device. Specifically, the YANG module defines the complete data tree for configuration and operational state.

One problem with tailf:symlink is that a client application (EMS or NMS or similar) cannot just ignore the statement, since it defines a subtree in the datastore. But since this is not a standard statement, third-party clients will ignore it. This means that when they talk to the device, they will not understand the subtree under the symlink.

Note that symlink is used to provide a different entry point to the same underlying data. If both the data model with the symlink and the target data model is being exposed at the same time to the client, this will be confusing to the client. A change in one part of the tree makes another part change automatically.

One solution to this problem is to make sure that the module with the symlink is not exposed to the client. Instead, publish the target module only.

Another solution to this problem is to not publish the target module, but instead use pyang to sanitize the module with the symlink. The sanitizing process replaces the symlink statement with a copy of the target subtree. Then publish the sanitized version of the module.

NOTE: If a module is not published to a client, it should have a tailf:export statement, to exclude the protocol the client is using. For example, suppose we have a symlink in our module acme-system.yang from /system/netconf/nacm to the standard NACM path /nacm. The standard NACM module might be annotated with:

  tailf:export netconf;

and the system module with:

  tailf:export cli;
  tailf:export webui;

Errors

Another problem with symlinks is if the target subtree contains must expression or validation code that refers to nodes outside the target subtree. For example, if a symlink foo points to /x/z in this example:

  container x {
    leaf y {
      type int32;
    }
    leaf z {
      type int32;
      must ". > ../y" {
       error-message "x must be greater than y";
    }
  }

If someone sets the foo to a value that happens to invalidate the must expression, the error message will be misleading since it refers to a node they cannot see, and it is not easily fixed, since they cannot modify the node y.