diff --git a/CHANGELOG.md b/CHANGELOG.md index eb894f43..3c08b5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ ### Major changes: +* Added Clixon Restconf library + * Builds and installs a new restconf library: libclixon_restconf.so and clixon_restconf.h + * The restconf library can be included by a restconf plugin. + * Example code in example/Makefile.in and example/restconf_lib.c +* Authorization + * Example extended with authorization + * Test added with http basic authorization (test/test_auth.sh) + * Documentation in FAQ.md * Restconf error handling for get, put and post. Several cornercases remain. Available both as xml and json (set accept header), pretty-printed and not (set clixon config option). * Proper RFC 6241 Netconf error handling * New functions added in clixon_netconf_lib.[ch] @@ -12,6 +20,7 @@ ### Minor changes: +* README.md extended with new yang, netconf, restconf, datastore, and auth sections. * The key-value datastore is no longer supported. Use the default text datastore. * Add username to rpc calls to prepare for authorization for backend: clicon_rpc_config_get(h, db, xpath, xt) --> clicon_rpc_config_get(h, db, xpath, username, xt) diff --git a/README.md b/README.md index 141e78b6..301ceee8 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,43 @@ Clixon is an automatic configuration manager where you generate interactive CLI, NETCONF, RESTCONF and embedded databases with transaction support from a YANG specification. -Table of contents -================= - * [Documentation](#documentation) - * [Installation](#installation) - * [Dependencies](#dependencies) - * [Licenses](#licenses) +Topics +====== * [Background](#background) + * [Frequently asked questions](doc/FAQ.md) + * [Installation](#installation) + * [Licenses](#licenses) + * [Support](#support) + * [Dependencies](#dependencies) + * [Extending](#extending) + * [Yang](#yang) + * [Netconf](#netconf) + * [Restconf](#restconf) + * [Datastore](datastore/README.md) + * [Authentication and Authorization](#auth) + * [Example](example/README.md) + * [Changelog](CHANGELOG.md) recent changes. * [Clixon SDK](#SDK) + * [Clicon and Clixon project page](http://www.clicon.org) + * [Tests](test/README.md) + * [Reference manual](http://www.clicon.org/doxygen/index.html) (Note: the link may not be up-to-date. It is better to build your own: `cd doc; make doc`) + +Background +========== -Documentation -============= -- [Frequently asked questions](doc/FAQ.md) -- [CHANGELOG](CHANGELOG.md) recent changes. -- [XML datastore](datastore/README.md) -- [Netconf support](apps/netconf/README.md) -- [Restconf support](apps/restconf/README.md) -- [Reference manual](http://www.clicon.org/doxygen/index.html) (Note: the link may not be up-to-date. It is better to build your own: `cd doc; make doc`) -- [Routing example](example/README.md) -- [Clicon and Clixon project page](http://www.clicon.org) -- [Tests](test/README.md) +Clixon was implemented to provide an open-source generic configuration +tool. The existing [CLIgen](http://www.cligen.se) tool was extended to +a framework. Most of the user projects are for embedded network and +measuring devices, but can be deployed for more general use. + +Users of clixon currently include: + * [Netgate](https://www.netgate.com) + * [CloudMon360](http://cloudmon360.com) + * [Grideye](http://hagsand.se/grideye) + * [Netclean](https://www.netclean.com/solutions/whitebox) (only CLIgen) + * [Prosilient's PTAnalyzer] (only CLIgen) + +See also [Clicon project page](http://clicon.org). Installation ============ @@ -38,6 +55,13 @@ A typical installation is as follows: One [example application](example/README.md) is provided, a IETF IP YANG datamodel with generated CLI and configuration interface. +Licenses +======== +Clixon is open-source and dual licensed. Either Apache License, Version 2.0 or GNU +General Public License Version 2; you choose. + +See [LICENSE.md](LICENSE.md) for the license. + Dependencies ============ Clixon depends on the following software packages, which need to exist on the target machine. @@ -50,53 +74,105 @@ to build and install CLIgen: - Yacc/bison - Lex/Flex - Fcgi (if restconf is enabled) -- [Qdbm](http://fallabs.com/qdbm/) key-value store (if keyvalue datastore is enabled) There is no yum/apt/ostree package for Clixon (please help?) -Licenses +Support +======= +Clixon interaction is best done posting issues, pull requests, or joining the [slack channel](https://join.slack.com/t/clixondev/shared_invite/enQtMzI3OTM4MzA3Nzk3LTA3NWM4OWYwYWMxZDhiYTNhNjRkNjQ1NWI1Zjk5M2JjMDk4MTUzMTljYTZiYmNhODkwMDI2ZTkyNWU3ZWMyN2U). + +Extending +========= +Clixon provides a core system and can be used as-is using available +Yang specifications. However, an application very quickly needs to +specialize funxtions. Clixon is extended by (most commonly) writing +plugins for cli and backend. Extensions for netconf and restconf +are also available. + +Plugins are written in C and easiest is to look at +[example](example/README.md) or consulting the [FAQ](doc/FAQ.md). + +Yang +==== + +YANG and XML is at the heart of Clixon. Yang modules are used as a +specification for handling XML configuration data. The YANG spec is +used to generate an interactive CLI, netconf and restconf clients. It +also manages an XML datastore. + +Clixon mainly follows [YANG 1.0 RFC 6020](https://www.rfc-editor.org/rfc/rfc6020.txt) with some exceptions: +- conformance: feature, if-feature, deviation +- identity, base, identityref +- list features: min/max-elements, unique + +The aim is also to cover new featires in YANG 1.1 [YANG RFC 7950](https://www.rfc-editor.org/rfc/rfc7950.txt) + +Clixon has its own XML library designed for performance. + +Netconf +======= +Clixon implements the following NETCONF proposals or standards: +- [NETCONF Configuration Protocol](http://www.rfc-base.org/txt/rfc-4741.txt) +- [Using the NETCONF Configuration Protocol over Secure SHell (SSH)](http://www.rfc-base.org/txt/rfc-4742.txt) +- [NETCONF Event Notifications](http://www.rfc-base.org/txt/rfc-5277.txt) + +Some updates are being made to RFC 6241 and RFC 6242. + +Clixon does not support the following features: + +- :url capability +- copy-config source config +- edit-config testopts +- edit-config erropts +- edit-config config-text + +Restconf ======== -Clixon is dual licensed. Either Apache License, Version 2.0 or GNU -General Public License Version 2; you choose. +Clixon restconf is a daemon based on FASTCGI. Instructions are available to +run with NGINX. +The implementatation is based on [RFC 8040: RESTCONF Protocol](https://tools.ietf.org/html/rfc8040). +The following features are supported: +- OPTIONS, HEAD, GET, POST, PUT, DELETE +The following are not implemented +- PATCH +- query parameters (section 4.9) +- notifications (sec 6) +- schema resource -See [LICENSE.md](LICENSE.md) for the license. +See [more detailed restconf instructions](apps/restconf/README.md). -Background -========== -We implemented Clixon since we needed a generic configuration tool in -several projects, including -[KTH](http://www.csc.kth.se/~olofh/10G_OSR). Most of these projects -were for embedded network and measuring-probe devices. We started with -something called Clicon which was based on a key-value specification -and data-store. But as time passed new standards evolved and we -started adapting it to XML, Yang and netconf. Finally we made Clixon, -where the legacy key specification has been replaced completely by -YANG and using XML as configuration data. This means that legacy -Clicon applications do not run on Clixon. +Datastore +========= +The Clixon datastore is a stand-alone XML based datastore. The idea is +to be able to use different datastores backends with the same +API. + +Update: There used to be a key-value plugin based on qdbm but isnow obsoleted. Only a text datastore is implemented. + +The datastore is primarily designed to be used by Clixon but can be used +separately. + +See [more detailed restconf instructions](datastore/README.md). + + +Auth +==== + +Authentication is not in-scope for Clixon, however, there is ongoing work +to implement [NACM](https://tools.ietf.org/html/rfc8341). + +There are hooks (plugin callbacks) to identify which user is accessing a +client. That identity can then be used for authorization. + +In short, authentication needs to be coupled to clixon clients: + * CLI - Login has already been made via SSH + * Netconf - SSH netconf subsystem + * Restconf needs credentials. See [FAQ](doc/FAQ.md#How-do-I-write-an-authentication-callback). The [Example](example/README.md) has an example how to do this with HTTP basic auth. It is possible for do this for more advanced mechanisms such as Oauth2 or [https://github.com/CESNET/Netopeer2/tree/master/server/configuration] SDK === -clixon sdk +clixon sdk The figure shows the SDK runtime of Clixon. -YANG and XML is at the heart of Clixon. Yang modules are used as a -specification for handling XML configuration data. The spec is also -used to generate an interactive CLI client as well as provide -[Netconf](apps/netconf/README.md) and -[Restconf](apps/restconf/README.md) clients. - -The [YANG RFC 6020](https://www.rfc-editor.org/rfc/rfc6020.txt) is implemented with the following exceptions: -- conformance: feature, if-feature, deviation -- identity, base, identityref -- list features: min/max-elements, unique, ordered-by - -There are also new features in YANG 1.1 [YANG RFC -7950](https://www.rfc-editor.org/rfc/rfc7950.txt), most of which are -not implemented. - - - - - diff --git a/apps/netconf/Makefile.in b/apps/netconf/Makefile.in index 523a51a2..8adf51a2 100644 --- a/apps/netconf/Makefile.in +++ b/apps/netconf/Makefile.in @@ -44,6 +44,7 @@ bindir = @bindir@ libdir = @libdir@ mandir = @mandir@ libexecdir = @libexecdir@ +wwwdir = /www-data localstatedir = @localstatedir@ sysconfdir = @sysconfdir@ includedir = @includedir@ diff --git a/apps/netconf/netconf_main.c b/apps/netconf/netconf_main.c index ef9af729..87167585 100644 --- a/apps/netconf/netconf_main.c +++ b/apps/netconf/netconf_main.c @@ -201,7 +201,6 @@ netconf_input_cb(int s, retval = 0; goto done; } - for (i=0; i +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include +#include + +static const char Base64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char Pad64 = '='; + +/* skips all whitespace anywhere. + converts characters, four at a time, starting at (or after) + src from base - 64 numbers into three 8 bit bytes in the target area. + it returns the number of data bytes stored at the target, or -1 on error. + @note what is copyright of this? + */ +int +b64_decode(const char *src, + char *target, + size_t targsize) +{ + int tarindex, state, ch; + char *pos; + + state = 0; + tarindex = 0; + + while ((ch = *src++) != '\0') { + if (isspace(ch)) /* Skip whitespace anywhere. */ + continue; + + if (ch == Pad64) + break; + + pos = strchr(Base64, ch); + if (pos == 0) /* A non-base64 character. */ + return (-1); + + switch (state) { + case 0: + if (target) { + if ((size_t)tarindex >= targsize) + return (-1); + target[tarindex] = (pos - Base64) << 2; + } + state = 1; + break; + case 1: + if (target) { + if ((size_t)tarindex + 1 >= targsize) + return (-1); + target[tarindex] |= (pos - Base64) >> 4; + target[tarindex+1] = ((pos - Base64) & 0x0f) + << 4 ; + } + tarindex++; + state = 2; + break; + case 2: + if (target) { + if ((size_t)tarindex + 1 >= targsize) + return (-1); + target[tarindex] |= (pos - Base64) >> 2; + target[tarindex+1] = ((pos - Base64) & 0x03) + << 6; + } + tarindex++; + state = 3; + break; + case 3: + if (target) { + if ((size_t)tarindex >= targsize) + return (-1); + target[tarindex] |= (pos - Base64); + } + tarindex++; + state = 0; + break; + default: + return -1; + } + } + + /* + * We are done decoding Base-64 chars. Let's see if we ended + * on a byte boundary, and/or with erroneous trailing characters. + */ + + if (ch == Pad64) { /* We got a pad char. */ + ch = *src++; /* Skip it, get next. */ + switch (state) { + case 0: /* Invalid = in first position */ + case 1: /* Invalid = in second position */ + return (-1); + + case 2: /* Valid, means one byte of info */ + /* Skip any number of spaces. */ + for ((void)NULL; ch != '\0'; ch = *src++) + if (!isspace(ch)) + break; + /* Make sure there is another trailing = sign. */ + if (ch != Pad64) + return (-1); + ch = *src++; /* Skip the = */ + /* Fall through to "single trailing =" case. */ + /* FALLTHROUGH */ + + case 3: /* Valid, means two bytes of info */ + /* + * We know this char is an =. Is there anything but + * whitespace after it? + */ + for ((void)NULL; ch != '\0'; ch = *src++) + if (!isspace(ch)) + return (-1); + + /* + * Now make sure for cases 2 and 3 that the "extra" + * bits that slopped past the last full byte were + * zeros. If we don't check them, they become a + * subliminal channel. + */ + if (target && target[tarindex] != 0) + return (-1); + } + } else { + /* + * We ended by seeing the end of the string. Make sure we + * have no partial bytes lying around. + */ + if (state != 0) + return (-1); + } + + return (tarindex); +} + + +/*! Process a rest request that requires (cookie) "authentication" + * Note, this is loaded as dlsym fixed symbol in plugin + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[out] username Malloced username, or NULL. + * @retval -1 Fatal error + * @retval 0 OK + * For grideye, return "u" entry name if it has a valid "user" entry. + */ +int +plugin_credentials(clicon_handle h, + FCGX_Request *r, + char **username) +{ + int retval = -1; + cxobj *xt = NULL; + cxobj *x; + char *xbody; + char *auth; + char *user = NULL; + char *passwd; + char *passwd2; + size_t authlen; + cbuf *cb = NULL; + int ret; + + clicon_debug(1, "%s", __FUNCTION__); + *username = NULL; /* unauthorized */ + /* Check if basic_auth set, if not return OK */ + if (clicon_rpc_get_config(h, "running", "/", NULL, &xt) < 0) + goto done; + if ((x = xpath_first(xt, "basic_auth")) == NULL) + goto none; + if ((xbody = xml_body(x)) == NULL) + goto none; + if (strcmp(xbody, "true")) + goto none; + /* At this point in the code we must use HTTP basic authentication */ + if ((auth = FCGX_GetParam("HTTP_AUTHORIZATION", r->envp)) == NULL) + goto done; + if (strlen(auth) < strlen("Basic ")) + goto fail; + if (strncmp("Basic ", auth, strlen("Basic "))) + goto fail; + auth += strlen("Basic "); + authlen = strlen(auth)*2; + if ((user = malloc(authlen)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + memset(user, 0, authlen); + if ((ret = b64_decode(auth, user, authlen)) < 0) + goto done; + /* auth string is on the format user:passwd */ + if ((passwd = index(user,':')) == NULL) + goto fail; + *passwd = '\0'; + passwd++; + clicon_debug(1, "%s user:%s passwd:%s", __FUNCTION__, user, passwd); + if ((cb = cbuf_new()) == NULL) + goto done; + cprintf(cb, "auth[user=%s]", user); + if ((x = xpath_first(xt, cbuf_get(cb))) == NULL) + goto fail; + + passwd2 = xml_find_body(x, "password"); + if (strcmp(passwd, passwd2)) + goto fail; + if ((*username = strdup(user)) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + fail: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (user) + free(user); + if (cb) + cbuf_free(cb); + if (xt) + xml_free(xt); + return retval; + none: /* basic_auth is not enabled, harcode authenticated user "none" */ + if ((*username = strdup("none")) == NULL){ + clicon_err(OE_XML, errno, "strdup"); + goto done; + } + goto fail; +} + + +/*! Restconf plugin initialization + */ +int +plugin_init(clicon_handle h) +{ + int retval = -1; + + clicon_debug(1, "%s restconf", __FUNCTION__); + retval = 0; + // done: + return retval; +} diff --git a/lib/clixon/clixon_plugin.h b/lib/clixon/clixon_plugin.h index f102de3d..48169df9 100644 --- a/lib/clixon/clixon_plugin.h +++ b/lib/clixon/clixon_plugin.h @@ -47,6 +47,10 @@ typedef void *plghndl_t; /* Find plugin by name callback. XXX Should be clicon internal */ typedef void *(find_plugin_t)(clicon_handle, char *); + + + + /* * Prototypes */ @@ -60,7 +64,7 @@ typedef void *(find_plugin_t)(clicon_handle, char *); * @see plginit_t */ #define PLUGIN_INIT "plugin_init" -typedef int (plginit_t)(clicon_handle); /* Plugin Init */ +typedef void * (plginit_t)(clicon_handle); /* Clixon plugin Init */ /* Called when backend started with cmd-line arguments from daemon call. * @see plgstart_t @@ -86,6 +90,18 @@ typedef int (plgexit_t)(clicon_handle); /* Plugin exit */ */ typedef int (plgcredentials_t)(clicon_handle, void *, char **username); + +/* grideye agent plugin init struct for the api + * Note: Implicit init function, see PLUGIN_INIT_FN_V2 + */ +struct clixon_plugin_api{ + plgcredentials_t *cp_auth; +}; + +/* + * Prototypes + */ + /* Find a function in global namespace or a plugin. XXX clicon internal */ void *clicon_find_func(clicon_handle h, char *plugin, char *func); diff --git a/test/lib.sh b/test/lib.sh index b6964e2e..971b1088 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -28,13 +28,17 @@ rm -rf $dir/* # error and exit, arg is optional extra errmsg err(){ - echo "Error in Test$testnr [$testname]:" + echo -e "\e[31m\nError in Test$testnr [$testname]:" if [ $# -gt 0 ]; then echo "Expected: $1" fi if [ $# -gt 1 ]; then echo "Received: $2" fi + echo -e "\e[0m:" + echo "$ret"| od -t c > $dir/clixon-ret + echo "$expect"| od -t c > $dir/clixon-expect + diff $dir/clixon-ret $dir/clixon-expect exit $testnr } @@ -43,7 +47,11 @@ new(){ testnr=`expr $testnr + 1` testname=$1 >&2 echo "Test$testnr [$1]" -# sleep 1 +} +new2(){ + testnr=`expr $testnr + 1` + testname=$1 + >&2 echo -n "Test$testnr [$1]" } # clixon tester. First arg is command and second is expected outcome @@ -82,34 +90,15 @@ expectfn(){ fi } -# Similar to expectfn, but checks for equality and not only match -expecteq2(){ +expecteq(){ ret=$1 expect=$2 - - # Match if both are empty string if [ -z "$ret" -a -z "$expect" ]; then return fi - if [ "$ret" != "$expect" ]; then - err "$expect" "$ret" - fi -} - -# Similar to expectfn, but checks for equality and not only match -expecteq(){ - cmd=$1 - expect=$2 - ret=$($cmd) - - if [ $? -ne 0 ]; then - err "wrong args" - fi - # Match if both are empty string - if [ -z "$ret" -a -z "$expect" ]; then - return - fi - if [ "$ret" != "$expect" ]; then + if [[ "$ret" = "$expect" ]]; then + echo + else err "$expect" "$ret" fi } diff --git a/test/test_auth.sh b/test/test_auth.sh new file mode 100755 index 00000000..92325864 --- /dev/null +++ b/test/test_auth.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Authentication and authorization + +# include err() and new() functions and creates $dir +. ./lib.sh + +cfg=$dir/conf_yang.xml +fyang=$dir/test.yang +fyangerr=$dir/err.yang + +cat < $cfg + + $cfg + /usr/local/share/routing/yang + example + /usr/local/lib/routing/clispec + /usr/local/lib/routing/restconf + /usr/local/lib/routing/cli + routing + /usr/local/var/routing/routing.sock + /usr/local/var/routing/routing.pidfile + 1 + /usr/local/var/routing + /usr/local/lib/xmldb/text.so + false + +EOF + +cat < $fyang +module example{ + prefix ex; + leaf basic_auth{ + description "Basic user / password authentication as in HTTP basic auth"; + type boolean; + default false; + } + list auth { + description "user / password entries. Valid if basic_auth=true"; + key user; + leaf user{ + description "User name"; + type string; + } + leaf password{ + description "Password"; + type string; + } + } +} +EOF + +# kill old backend (if any) +new "kill old backend" +sudo clixon_backend -zf $cfg -y $fyang +if [ $? -ne 0 ]; then + err +fi + +new "start backend -s init -f $cfg -y $fyang" +# start new backend +sudo clixon_backend -s init -f $cfg -y $fyang +if [ $? -ne 0 ]; then + err +fi + +new "kill old restconf daemon" +sudo pkill -u www-data clixon_restconf + +new "start restconf daemon" +sudo start-stop-daemon -S -q -o -b -x /www-data/clixon_restconf -d /www-data -c www-data -- -f $cfg # -D + +sleep 1 + +new2 "auth get" +expecteq "$(curl -sS -X GET http://localhost/restconf/data)" '{"data": null} + ' + +new "auth set authentication config" +expecteof "$clixon_netconf -qf $cfg -y $fyang" "truefoobar]]>]]>" "^]]>]]>$" + +expecteof "$clixon_netconf -qf $cfg -y $fyang" "]]>]]>" "^]]>]]>$" + +new2 "auth get (access denied)" +expecteq "$(curl -sS -X GET http://localhost/restconf/data)" "access-denied +The requested URL /restconf/data was unauthorized." + +new2 "auth get (access)" +expecteq "$(curl -u foo:bar -sS -X GET http://localhost/restconf/data)" '{"data": {"basic_auth": true,"auth": [{"user": "foo","password": "bar"}]}} + ' + +new "Kill restconf daemon" +sudo pkill -u www-data clixon_restconf + +pid=`pgrep clixon_backend` +if [ -z "$pid" ]; then + err "backend already dead" +fi +# kill backend +sudo clixon_backend -zf $cfg +if [ $? -ne 0 ]; then + err "kill backend" +fi + +rm -rf $dir diff --git a/test/test_restconf.sh b/test/test_restconf.sh index 7562a8f4..818388fb 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -78,22 +78,22 @@ sleep 1 new "restconf tests" -new "restconf root discovery. RFC 8040 3.1 (xml+xrd)" -expecteq2 "$(curl -s -X GET http://localhost/.well-known/host-meta)" " +new2 "restconf root discovery. RFC 8040 3.1 (xml+xrd)" +expecteq "$(curl -s -X GET http://localhost/.well-known/host-meta)" " " -new "restconf get restconf resource. RFC 8040 3.3 (json)" -expecteq2 "$(curl -sG http://localhost/restconf)" '{"restconf": {"data": null,"operations": null,"yang-library-version": "2016-06-21"}} +new2 "restconf get restconf resource. RFC 8040 3.3 (json)" +expecteq "$(curl -sG http://localhost/restconf)" '{"restconf": {"data": null,"operations": null,"yang-library-version": "2016-06-21"}} ' -new "restconf get restconf resource. RFC 8040 3.3 (xml)" +new2 "restconf get restconf resource. RFC 8040 3.3 (xml)" # Get XML instead of JSON? -expecteq2 "$(curl -s -H 'Accept: application/yang-data+xml' -G http://localhost/restconf)" '2016-06-21 +expecteq "$(curl -s -H 'Accept: application/yang-data+xml' -G http://localhost/restconf)" '2016-06-21 ' -new "restconf get restconf/operations. RFC8040 3.3.2 (json)" -expecteq2 "$(curl -sG http://localhost/restconf/operations)" '{"operations": {"ex:empty": null,"ex:input": null,"ex:output": null,"rt:fib-route": null,"rt:route-count": null}} +new2 "restconf get restconf/operations. RFC8040 3.3.2 (json)" +expecteq "$(curl -sG http://localhost/restconf/operations)" '{"operations": {"ex:empty": null,"ex:input": null,"ex:output": null,"rt:fib-route": null,"rt:route-count": null}} ' new "restconf get restconf/operations. RFC8040 3.3.2 (xml)" @@ -104,8 +104,8 @@ if [ -z "$match" ]; then err "$expect" "$ret" fi -new "restconf get restconf/yang-library-version. RFC8040 3.3.3" -expecteq2 "$(curl -sG http://localhost/restconf/yang-library-version)" '{"yang-library-version": "2016-06-21"}' +new2 "restconf get restconf/yang-library-version. RFC8040 3.3.3" +expecteq "$(curl -sG http://localhost/restconf/yang-library-version)" '{"yang-library-version": "2016-06-21"}' new "restconf get restconf/yang-library-version. RFC8040 3.3.3 (xml)" ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/yang-library-version) @@ -122,12 +122,12 @@ new "restconf head. RFC 8040 4.2" expectfn "curl -s -I http://localhost/restconf/data" "HTTP/1.1 200 OK" #Content-Type: application/yang-data+json" -new "restconf empty rpc" -expecteq2 "$(curl -s -X POST -d {\"input\":{\"name\":\"\"}} http://localhost/restconf/operations/ex:empty)" '{"output": null} +new2 "restconf empty rpc" +expecteq "$(curl -s -X POST -d {\"input\":{\"name\":\"\"}} http://localhost/restconf/operations/ex:empty)" '{"output": null} ' -new "restconf get empty config + state json" -expecteq2 "$(curl -sSG http://localhost/restconf/data)" '{"data": {"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} +new2 "restconf get empty config + state json" +expecteq "$(curl -sSG http://localhost/restconf/data)" '{"data": {"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} ' new "restconf get empty config + state xml" @@ -138,8 +138,8 @@ if [ -z "$match" ]; then err "$expect" "$ret" fi -new "restconf get data/interfaces-state/interface=eth0 json" -expecteq2 "$(curl -s -G http://localhost/restconf/data/interfaces-state/interface=eth0)" '{"interface": [{"name": "eth0","type": "eth","if-index": 42}]} +new2 "restconf get data/interfaces-state/interface=eth0 json" +expecteq "$(curl -s -G http://localhost/restconf/data/interfaces-state/interface=eth0)" '{"interface": [{"name": "eth0","type": "eth","if-index": 42}]} ' new "restconf get state operation eth0 xml" @@ -151,8 +151,8 @@ if [ -z "$match" ]; then err "$expect" "$ret" fi -new "restconf get state operation eth0 type json" -expecteq2 "$(curl -s -G http://localhost/restconf/data/interfaces-state/interface=eth0/type)" '{"type": "eth"} +new2 "restconf get state operation eth0 type json" +expecteq "$(curl -s -G http://localhost/restconf/data/interfaces-state/interface=eth0/type)" '{"type": "eth"} ' new "restconf get state operation eth0 type xml" @@ -164,8 +164,8 @@ if [ -z "$match" ]; then err "$expect" "$ret" fi -new "restconf GET datastore" -expecteq2 "$(curl -s -X GET http://localhost/restconf/data)" '{"data": {"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} +new2 "restconf GET datastore" +expecteq "$(curl -s -X GET http://localhost/restconf/data)" '{"data": {"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} ' # Exact match @@ -176,14 +176,14 @@ new "restconf Re-add subtree which should give error" expectfn 'curl -s -X POST -d {"interfaces":{"interface":{"name":"eth/0/0","type":"eth","enabled":true}}} http://localhost/restconf/data' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' # XXX Cant get this to work -#expecteq2 "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"eth\",\"enabled\":true}}} http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' +#expecteq "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"eth\",\"enabled\":true}}} http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' new "restconf Check interfaces eth/0/0 added" expectfn "curl -s -G http://localhost/restconf/data" '{"interfaces": {"interface": \[{"name": "eth/0/0","type": "eth","enabled": true}\]},"interfaces-state": {"interface": \[{"name": "eth0","type": "eth","if-index": 42}\]}} ' -new "restconf delete interfaces" -expecteq2 $(curl -s -X DELETE http://localhost/restconf/data/interfaces) "" +new2 "restconf delete interfaces" +expecteq $(curl -s -X DELETE http://localhost/restconf/data/interfaces) "" new "restconf Check empty config" expectfn "curl -sG http://localhost/restconf/data" "$state" @@ -191,43 +191,43 @@ expectfn "curl -sG http://localhost/restconf/data" "$state" new "restconf Add interfaces subtree eth/0/0 using POST" expectfn 'curl -s -X POST -d {"interface":{"name":"eth/0/0","type":"eth","enabled":true}} http://localhost/restconf/data/interfaces' "" # XXX cant get this to work -#expecteq2 "$(curl -s -X POST -d '{"interface":{"name":"eth/0/0","type\":"eth","enabled":true}}' http://localhost/restconf/data/interfaces)" "" +#expecteq "$(curl -s -X POST -d '{"interface":{"name":"eth/0/0","type\":"eth","enabled":true}}' http://localhost/restconf/data/interfaces)" "" -new "restconf Check eth/0/0 added" -expecteq 'curl -s -G http://localhost/restconf/data' '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} +new2 "restconf Check eth/0/0 added" +expecteq "$(curl -s -G http://localhost/restconf/data)" '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} ' -new "restconf Re-post eth/0/0 which should generate error" -expecteq 'curl -s -X POST -d {"interface":{"name":"eth/0/0","type":"eth","enabled":true}} http://localhost/restconf/data/interfaces' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' +new2 "restconf Re-post eth/0/0 which should generate error" +expecteq "$(curl -s -X POST -d '{"interface":{"name":"eth/0/0","type":"eth","enabled":true}}' http://localhost/restconf/data/interfaces)" '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' -new "Add leaf description using POST" -expecteq 'curl -s -X POST -d {"description":"The-first-interface"} http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "" +new2 "Add leaf description using POST" +expecteq "$(curl -s -X POST -d '{"description":"The-first-interface"}' http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0)" "" new "Add nothing using POST" expectfn 'curl -s -X POST http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "data is in some way badly formed" -new "restconf Check description added" -expecteq "curl -s -G http://localhost/restconf/data" '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","description": "The-first-interface","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} +new2 "restconf Check description added" +expecteq "$(curl -s -G http://localhost/restconf/data)" '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","description": "The-first-interface","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} ' new "restconf delete eth/0/0" -expecteq 'curl -s -X DELETE http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "" +expecteq "$(curl -s -X DELETE http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0)" "" new "Check deleted eth/0/0" expectfn 'curl -s -G http://localhost/restconf/data' $state -new "restconf Re-Delete eth/0/0 using none should generate error" -expecteq 'curl -s -X DELETE http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-missing","error-type": "application","error-severity": "error","error-message": "Data does not exist; cannot delete resource"}}} ' +new2 "restconf Re-Delete eth/0/0 using none should generate error" +expecteq "$(curl -s -X DELETE http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0)" '{"ietf-restconf:errors" : {"error": {"error-tag": "data-missing","error-type": "application","error-severity": "error","error-message": "Data does not exist; cannot delete resource"}}} ' -new "restconf Add subtree eth/0/0 using PUT" -expecteq 'curl -s -X PUT -d {"interface":{"name":"eth/0/0","type":"eth","enabled":true}} http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "" +new2 "restconf Add subtree eth/0/0 using PUT" +expecteq "$(curl -s -X PUT -d '{"interface":{"name":"eth/0/0","type":"eth","enabled":true}}' http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0)" "" -new "restconf get subtree" -expecteq 'curl -s -G http://localhost/restconf/data' '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} +new2 "restconf get subtree" +expecteq "$(curl -s -G http://localhost/restconf/data)" '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} ' -new "restconf rpc using POST json" -expecteq 'curl -s -X POST -d {"input":{"routing-instance-name":"ipv4"}} http://localhost/restconf/operations/rt:fib-route' '{"output": {"route": {"address-family": "ipv4","next-hop": {"next-hop-list": "2.3.4.5"}}}} +new2 "restconf rpc using POST json" +expecteq "$(curl -s -X POST -d '{"input":{"routing-instance-name":"ipv4"}}' http://localhost/restconf/operations/rt:fib-route)" '{"output": {"route": {"address-family": "ipv4","next-hop": {"next-hop-list": "2.3.4.5"}}}} ' new "restconf rpc using POST xml" diff --git a/test/test_restconf2.sh b/test/test_restconf2.sh index 37bb8708..7f8bdb7a 100755 --- a/test/test_restconf2.sh +++ b/test/test_restconf2.sh @@ -83,11 +83,11 @@ expectfn 'curl -s -X POST -d {"interface":{"name":"TEST"}} http://localhost/rest new "restconf POST interface" expectfn 'curl -s -X POST -d {"interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/cont1' "" -new "restconf POST again" -expecteq 'curl -s -X POST -d {"interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/cont1' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' +new2 "restconf POST again" +expecteq "$(curl -s -X POST -d '{"interface":{"name":"TEST","type":"eth0"}}' http://localhost/restconf/data/cont1)" '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' -new "restconf POST from top" -expecteq 'curl -s -X POST -d {"cont1":{"interface":{"name":"TEST","type":"eth0"}}} http://localhost/restconf/data' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' +new2 "restconf POST from top" +expecteq "$(curl -s -X POST -d '{"cont1":{"interface":{"name":"TEST","type":"eth0"}}}' http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' new "restconf DELETE" expectfn 'curl -s -X DELETE http://localhost/restconf/data/cont1' ""