Chapter 23. The Management Agent API

Table of Contents

23.1. What is MAAPI?
23.2. A custom toy CLI

23.1. What is MAAPI?

MAAPI is a C API which provides full access to the ConfD internal transaction engine. It is used in a number of different settings.

  • We use MAAPI if we want to write our own management application. Using the MAAPI interface, it is for example possible to implement a custom built command line interface (CLI) or Web UI. This usage is described below.

  • We use MAAPI to access ConfD data inside a not yet committed transaction when we wish to implement semantic validation in C. This is fully described in Chapter 9, Semantic validation.

  • We use MAAPI to access ConfD data inside a not yet committed transaction when we wish to implement CLI wizards in C. Here we can invoke an external C program which can read and write, both to the executing transaction, but also interact with the CLI user. This is fully described in Chapter 16, The CLI agent.

  • Finally MAAPI is also used during database upgrade to access and write data to a special upgrade transaction. This is fully described in Chapter 5, CDB - The ConfD XML Database.

Thus, MAAPI is an API which contains functions that are the northbound equivalents of all the callbacks described in Chapter 7, The external database API.

A typical sequence of API calls when using MAAPI to write a management application would be

  1. Create a user session, this is the equivalent of an established SSH connection from a NETCONF manager. It is up to the MAAPI application to authenticate users. The TCP connection from the MAAPI application to ConfD is a clear text connection.

  2. Establish a new ConfD transaction

  3. Issue a series of read and write operations towards the ConfD transaction

  4. Commit or abort the transaction

MAAPI also has support for several operations that do not work immediately towards a ConfD transaction. This includes users session management, locking, and candidate manipulation.

23.2. A custom toy CLI

In this section we implement a small toy CLI towards a specific data model. We start with the following YANG module:

Example 23.1. scli.yang YANG module

module scli {
  namespace "http://tail-f.com/test/scli";
  prefix scli;

  import ietf-inet-types {
      prefix inet;
  }

  leaf foo {
    type string;
    default "fooo";
  }
  leaf bar {
    type int32;
    default 66;
  }
  list servers {
    key "name";
    max-elements 64;
    leaf name {
      type string;
    }
    leaf ip {
      type inet:ipv4-address;
      mandatory true;
    }
    leaf port {
      type inet:port-number;
      mandatory true;
    }
  }
}

We compile this file with confdc (1) and load it into ConfD as usual. Data is kept in CDB, since there are no callpoints defined. At this point, we can manipulate the configuration data with a NETCONF manager or the ConfD built-in CLI.

What we wish to do is to write a small custom CLI, that understands the structure of the XML tree using the MAAPI.

All functions in MAAPI are described in the confd_lib_maapi(3) manual page. We will use a subset of those functions in this example.

We start off with some global variables, and a trivial main() function.

#include "confd_lib.h"
#include "confd_maapi.h"

/* include .h file generated by confdc */
#include "scli.h"

static int sock, th;
static char *user = "admin";
static const char *groups[] = {"admin"};
static int debuglevel = CONFD_DEBUG;
static char *context = "maapi";
static enum confd_dbname dbname = CONFD_RUNNING;

int main(int argc, char **argv)
{
    int c;
    while ((c = getopt(argc, argv, "rc")) != -1) {
        switch(c) {
        case 'r':
            dbname = CONFD_RUNNING;
            break;
        case 'c':
            dbname = CONFD_CANDIDATE;
            break;

        }
    }
    confd_init("MYNAME", stderr, debuglevel);
    cnct();
    runcli();
}

The code in main() sets a global variable dbname which will be the database we are opening, i.e. we can choose to run this CLI either towards the running database or towards the candidate database.

The code first calls cnct() which creates a MAAPI socket and connects to ConfD, following that we run the simple CLI.

The code to connect to ConfD looks like this:

static void cnct()
{
    struct in_addr in;
    struct sockaddr_in addr;
    struct confd_ip ip;

    inet_aton("127.0.0.1", &in);
    addr.sin_addr.s_addr = in.s_addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(4565);

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

    if (maapi_connect(sock, (struct sockaddr*)&addr,
                      sizeof (struct sockaddr_in)) < 0)
        confd_fatal("Failed to confd_connect() to confd \n");

    ip.af = AF_INET;
    inet_aton("66.55.44.33", &ip.ip.v4);

    maapi_start_user_session(sock, user, context, groups, 1,
                             &ip, CONFD_PROTO_TCP);

    th = maapi_start_trans(sock, dbname, CONFD_READ_WRITE);
    maapi_set_namespace(sock, th, scli__ns);
}

The code first creates a normal socket, and then connects to ConfD using maapi_connect(). Following that the code calls maapi_start_user_session() which creates a user session on the socket.

We must usually have an established user session before we can issue any of the MAAPI calls. There are some calls that can be performed on the socket without an already established user session.

A user session must be authenticated by the agent before the agent connects to ConfD. This authentication must be performed by the agent code, and once the agent issues the call to maapi_start_user_session(), the authentication must already be done. ConfD also has an authentication system built-in to it. The behavior of the ConfD built-in authentication system is controlled through the AAA section in confd.conf. Thus, when we're implementing a proprietary agent, either we do the authentication ourselves, or we can ask ConfD to authenticate the user through the API function maapi_authenticate(). Thus we could have written:

char *groups[10];
if (maapi_authenticate(sock, user, pass, &groups[0], 10) == 1) {
    int i = 0;
    while (groups[i])
      i++;
    maapi_start_user_session(sock, user, context, groups, i,
                             &srcip, CONFD_PROTO_TCP);

This way we only need to obtain the clear text password in the agent and we can let ConfD perform the actual authentication as well as the group assignment. The group assignment is important, since the authorization model in ConfD hinges entirely on group membership.

Once we have established a user session, we continue to create a transaction towards either the running database or the candidate. This is done through the call to maapi_start_trans(). The call returns a transaction handle, a th which is subsequently used when we read and write data. Remember that we are creating a transaction, thus nothing is written to actual storage until we commit the transaction. This applies also when we access the running database.

Finally we make our first call towards the transaction, and that is to indicate the name of the namespace we are planning to work against. This is a hashed integer value which can be found in a header file generated from scli.fxs. Read more about how to generate a header (.h) file from a .fxs file in the confdc (1) compiler man page.

Once we have connected, established a user session and also created our first transaction, we enter the CLI input loop called runcli().

#define DELIM " \n\r"

static void runcli()
{
    char ibuf[BUFSIZ];
    struct maapi_cursor mc;
    int ival;
    char *tok;

    printf("#cli "); fflush(stdout);

    while(fgets(ibuf, BUFSIZ, stdin) != NULL) {
        if ((tok = strtok(ibuf, DELIM)) == NULL) {
            printf("#cli "); fflush(stdout);
            continue;
        }

        if (strcmp(tok, "show") == 0) {
            if (maapi_get_str_elem(sock, th, ibuf, BUFSIZ, "/foo") ==CONFD_OK)
                printf ("foo: %s\n", ibuf);
            else if (confd_errno == CONFD_ERR_NOEXISTS)
                printf ("foo: \n");
            else
                confd_fatal("What \n");

            if (maapi_get_int32_elem(sock, th, &ival, "/bar") == CONFD_OK)
                printf ("bar: %d\n", ival);
            else if (confd_errno == CONFD_ERR_NOEXISTS)
                printf ("bar: ");
            else
                confd_fatal("What \n");

            maapi_init_cursor(sock, th, &mc, "/servers/server");
            maapi_get_next(&mc);
            while (mc.n != 0) {
                struct in_addr ip;
                uint16_t port;
                char tmpbuf[BUFSIZ];

                maapi_get_ipv4_elem(sock, th, &ip,
                                    "/servers/server{%x}/ip",
                                    &mc.keys[0]);
                maapi_get_u_int16_elem(sock, th, &port,
                                       "/servers/server{%x}/port",
                                       &mc.keys[0]);
                confd_pp_value(tmpbuf,BUFSIZ,&mc.keys[0]);
                printf ("server name=%s ip=%s port=%d\n",
                        tmpbuf, inet_ntoa(ip), port);
                maapi_get_next(&mc);
            }
            maapi_destroy_cursor(&mc);
        }
        else if (strcmp(tok, "abort") == 0) {
            maapi_abort_trans(sock, th);
            maapi_finish_trans(sock, th);
            th = maapi_start_trans(sock, dbname, CONFD_READ_WRITE);
            maapi_set_namespace(sock, th, scli__ns);
        }
        else if (strcmp(tok, "commit") == 0) {
            maapi_apply_trans(sock, th, 0);
            maapi_finish_trans(sock, th);
            th = maapi_start_trans(sock, dbname, CONFD_READ_WRITE);
            maapi_set_namespace(sock, th, scli__ns);
        }
        else if (strcmp(tok, "create") == 0) {
            char *name; char *ipstr; char *portstr;
            if (((name = strtok(NULL, DELIM)) == NULL) ||
                ((ipstr = strtok(NULL, DELIM)) == NULL) ||
                ((portstr = strtok(NULL, DELIM)) == NULL)) {
                printf ("input error \n"); goto err;
            }
            if (maapi_create(sock, th, "/servers/server{%s}", name) !=
                CONFD_OK) {
                printf ("error: %d %s \n ", confd_errno,confd_lasterr());
                goto err;
            }
            if (maapi_set_elem2(sock,th,ipstr,"/servers/server{%s}/ip",name)
                != CONFD_OK) {
                printf ("error: %d %s \n ", confd_errno, confd_lasterr());
                goto err;
            }
            if (maapi_set_elem2(sock,th,portstr,"/servers/server{%s}/port",
                                name)
                != CONFD_OK) {
                printf ("error: %d %s \n ", confd_errno, confd_lasterr());
                goto err;
            }
        }
        else if (strcmp(tok, "delete-config") == 0) {
            if (maapi_delete_config(sock, dbname) != CONFD_OK)
                printf ("error: %d, %s\n", confd_errno,
                        confd_lasterr());
        }

        else if (strcmp(tok, "delete") == 0) {
            char *name;
            if ((name= strtok(NULL, DELIM)) == NULL) {
                printf ("input error"); goto err;
            }
            if (maapi_delete(sock, th, "/servers/server{%s}", name)
                != CONFD_OK) {
                printf ("error: %s \n ", confd_lasterr());
                goto err;
            }
        }
        else if (strcmp(tok, "candidate-reset") == 0) {
            if (maapi_candidate_reset(sock) != CONFD_OK)
                printf ("error: %d, %s\n", confd_errno,
                        confd_lasterr());
        }

        else if (strcmp(tok, "validate-trans") == 0) {
            if (maapi_validate_trans(sock, th,1,1) == CONFD_OK) {
                printf ("ok \n");
            }
            else {
                printf ("nok: %s\n", confd_lasterr());
            }
        }
        else if (strcmp(tok, "candidate-confirmed-commit") == 0) {
            char *istr = strtok(NULL, DELIM);
            if (!istr) {printf("input error\n"); goto err;}
            if (maapi_candidate_confirmed_commit(sock, atoi(istr))!=CONFD_OK) {
                printf("error: %s\n", confd_lasterr());
            }
            else {
                printf("ok\n");
            }
        }
        else if (strcmp(tok, "candidate-commit") == 0) {
            if (maapi_candidate_commit(sock) != CONFD_OK) {
                printf("error: %s\n", confd_lasterr());
            }
            else {
                printf("ok\n");
            }
        }
        else {
            printf("commands \n");
            printf("   show    - show current conf\n");
            printf("   help    - show this text\n");
            printf("   abort   - abort current trans \n");
            printf("   commit  - commit current trans\n");
            printf("   create name ip port  - create new server\n");
            printf("   delete name          - delete server\n");
            printf("   candidate-reset      - copy running into cand");
            printf("   validate             - trans validate");
            printf("   delete-config        - delete config");
            printf("   candidate-commit     - copy cand to running | confirm");
            printf("   candidate-confirmed-commit secs \n");

            printf(" \n");
        }
          err:
        printf("#cli "); fflush(stdout);
    }
}

The code above reads lines from stdin, parses the line and invokes different MAAPI calls. For example, if we wish to use the show command to show the contents of the opened database, we first read the two leaf elements, called /foo and /bar. We must check the return values from those read calls. If we look at the data model, the values are defined as:

  leaf foo {
    type string;
    default "fooo";
  }
  leaf bar {
    type int32;
    default 66;
  }

The above elements both have default values. However if we open an empty candidate, they do not exist there.

Following that, in order to show the database, we must traverse all /servers/server instances and display them on the screen. We do this by using a maapi_cursor. A cursor must first be initialized, and then we can use the cursor to find out the key value(s) of the next element until there are no more elements.

Another interesting call is the call to create a new server. We create a new server instance through the call to maapi_create(sock, th, "/servers/server{%s}", name). Once we have created a new "server" instance we must also set the values for the two leafs inside the server instance, the ip and the port elements. If we fail to do this, commit will fail.

There are two variants on the MAAPI function which sets a a value. One where the value is a regular string, and one where the value is of type confd_value_t. Usually when we implement a proprietary agent, somehow a user will enter values as strings. In our code above, we use the string variant.

This program can be found in the examples section of a ConfD release, in the misc/maapi_cli directory.

If we want to use MAAPI to implement a general purpose agent which works on all data models, we can use the confd_cs_node tree that is generated when we call maapi_load_schemas()/confd_load_schemas(), see the section called “USING SCHEMA INFORMATION” in the confd_types(3) manual page. This is a representation of the complete data model in the form of a tree of linked C structures.