* Multi-socket feature (eg IPv4/IPv6 http/https) to restconf evhtp

* Added by-ref parameter to `ys_cv_validate()` returning which sub-yang spec was validated in a union.
This commit is contained in:
Olof hagsand 2020-11-15 12:33:20 +01:00
parent 707685f5ff
commit 6eb18da5e9
13 changed files with 683 additions and 382 deletions

View file

@ -29,6 +29,7 @@
### New features ### New features
* Initial NBMA functionality (thanks: @benavrhm): "ds" resource
* Restconf configuration has a new configure model: `clixon-restconf.yang` enabling restconf daemon configuration from datastore instead of from config file. * Restconf configuration has a new configure model: `clixon-restconf.yang` enabling restconf daemon configuration from datastore instead of from config file.
* Restconf config data, such as addresses, authentication type, etc, is read from the backend datastore instead of the clixon-config file on startup. * Restconf config data, such as addresses, authentication type, etc, is read from the backend datastore instead of the clixon-config file on startup.
* This is enabled by setting `CLIXON_RESTCONF_CONFIG` to true (or start clixon-restconf with `-b`), in which case restconf data can be set in the datastore. * This is enabled by setting `CLIXON_RESTCONF_CONFIG` to true (or start clixon-restconf with `-b`), in which case restconf data can be set in the datastore.
@ -48,6 +49,7 @@ Users may have to change how they access the system
Developers may need to change their code Developers may need to change their code
* Added by-ref parameter to `ys_cv_validate()` returning which sub-yang spec was validated in a union.
* Changed first parameter from `int fd` to `FILE *f` in the following functions: * Changed first parameter from `int fd` to `FILE *f` in the following functions:
* clixon_xml_parse_file(), clixon_json_parse_file(), yang_parse_file() * clixon_xml_parse_file(), clixon_json_parse_file(), yang_parse_file()
* See [Bytewise read() of files is slow #146](https://github.com/clicon/clixon/issues/146) * See [Bytewise read() of files is slow #146](https://github.com/clicon/clixon/issues/146)

View file

@ -75,7 +75,7 @@
/*! Open an INET stream socket and bind it to a file descriptor /*! Open an INET stream socket and bind it to a file descriptor
* *
o * @param[in] h Clicon handle * @param[in] h Clicon handle
* @param[in] dst IPv4 address (see inet_pton(3)) * @param[in] dst IPv4 address (see inet_pton(3))
* @retval s Socket file descriptor (see socket(2)) * @retval s Socket file descriptor (see socket(2))
* @retval -1 Error * @retval -1 Error

View file

@ -60,6 +60,9 @@
#include <sys/wait.h> #include <sys/wait.h>
#include <assert.h> #include <assert.h>
#include <sys/stat.h> /* chmod */ #include <sys/stat.h> /* chmod */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* evhtp */ /* evhtp */
#include <evhtp/evhtp.h> #include <evhtp/evhtp.h>
@ -88,8 +91,9 @@
/* clixon evhtp handle */ /* clixon evhtp handle */
typedef struct { typedef struct {
evhtp_t *eh_htp; evhtp_t **eh_htpvec; /* One per socket */
struct event_base *eh_evbase; int eh_htplen; /* Number of sockets */
struct event_base *eh_evbase; /* Change to list */
evhtp_ssl_cfg_t *eh_ssl_config; evhtp_ssl_cfg_t *eh_ssl_config;
} cx_evhtp_handle; } cx_evhtp_handle;
@ -105,12 +109,16 @@ static void
evhtp_terminate(cx_evhtp_handle *eh) evhtp_terminate(cx_evhtp_handle *eh)
{ {
evhtp_ssl_cfg_t *sc; evhtp_ssl_cfg_t *sc;
int i;
if (eh == NULL) if (eh == NULL)
return; return;
if (eh->eh_htp){ if (eh->eh_htpvec){
evhtp_unbind_socket(eh->eh_htp); for (i=0; i<eh->eh_htplen; i++){
evhtp_free(eh->eh_htp); evhtp_unbind_socket(eh->eh_htpvec[i]);
evhtp_free(eh->eh_htpvec[i]);
}
free(eh->eh_htpvec);
} }
if (eh->eh_evbase) if (eh->eh_evbase)
event_base_free(eh->eh_evbase); event_base_free(eh->eh_evbase);
@ -554,7 +562,7 @@ cx_get_ssl_server_certs(clicon_handle h,
* @retval -1 Error * @retval -1 Error
*/ */
static int static int
cx_get_ssl_client_certs(clicon_handle h, cx_get_ssl_client_ca_certs(clicon_handle h,
const char *server_ca_cert_path, const char *server_ca_cert_path,
evhtp_ssl_cfg_t *ssl_config) evhtp_ssl_cfg_t *ssl_config)
{ {
@ -643,7 +651,7 @@ static int
cx_verify_certs(int pre_verify, cx_verify_certs(int pre_verify,
evhtp_x509_store_ctx_t *store) evhtp_x509_store_ctx_t *store)
{ {
#ifdef NOTYET #if 0 //def NOTYET
char buf[256]; char buf[256];
X509 * err_cert; X509 * err_cert;
int err; int err;
@ -667,6 +675,88 @@ cx_verify_certs(int pre_verify,
return pre_verify; return pre_verify;
} }
/*!
*
* @param[out] addr Address as string, eg "0.0.0.0", "::"
* @param[in] addrtype One of inet:ipv4-address or inet:ipv6-address
* @param[out] ss Server socket (bound for accept)
*/
static int
restconf_socket_init(clicon_handle h,
const char *addr,
const char *addrtype,
uint16_t port,
int *ss)
{
int retval = -1;
int s = -1;
struct sockaddr * sa;
struct sockaddr_in6 sin6 = { 0 };
struct sockaddr_in sin = { 0 };
size_t sin_len;
int on = 1;
if (strcmp(addrtype, "inet:ipv6-address") == 0) {
sin_len = sizeof(struct sockaddr_in6);
sin6.sin6_port = htons(port);
sin6.sin6_family = AF_INET6;
evutil_inet_pton(AF_INET6, addr, &sin6.sin6_addr);
sa = (struct sockaddr *)&sin6;
}
else if (strcmp(addrtype, "inet:ipv4-address") == 0) {
sin_len = sizeof(struct sockaddr_in);
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr.s_addr = inet_addr(addr);
sa = (struct sockaddr *)&sin;
}
else{
clicon_err(OE_XML, EINVAL, "Unexpected addrtype: %s", addrtype);
return -1;
}
/* create inet socket */
if ((s = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) {
clicon_err(OE_UNIX, errno, "socket");
goto done;
}
// evutil_make_socket_closeonexec(s); // XXX
// evutil_make_socket_nonblocking(s); // XXX
if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, sizeof(on)) == -1) {
clicon_err(OE_UNIX, errno, "setsockopt SO_KEEPALIVE");
goto done;
}
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (void *)&on, sizeof(on)) == -1) {
clicon_err(OE_UNIX, errno, "setsockopt SO_REUSEADDR");
goto done;
}
/* only bind ipv6, otherwise it may bind to ipv4 as well which is strange but seems default */
if (sa->sa_family == AF_INET6 &&
setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1) {
clicon_err(OE_UNIX, errno, "setsockopt IPPROTO_IPV6");
goto done;
}
if (bind(s, sa, sin_len) == -1) {
clicon_err(OE_UNIX, errno, "bind port %u", port);
goto done;
}
if (listen(s, SOCKET_LISTEN_BACKLOG) < 0){
clicon_err(OE_UNIX, errno, "listen");
goto done;
}
if (ss)
*ss = s;
retval = 0;
done:
if (retval != 0 && s != -1)
evutil_closesocket(s);
return retval;
// return evhtp_bind_sockaddr(htp, sa, sin_len, SOCKET_LISTEN_BACKLOG);
}
/*! Usage help routine /*! Usage help routine
* @param[in] argv0 command line * @param[in] argv0 command line
* @param[in] h Clicon handle * @param[in] h Clicon handle
@ -701,23 +791,23 @@ usage(clicon_handle h,
exit(0); exit(0);
} }
/*! Main routine for libevhtp restconf /*! Extract socket info from backend config
*/
/*! Phase 2 of start per-socket config
* @param[in] h Clicon handle * @param[in] h Clicon handle
* @param[in] xs socket config * @param[in] xs socket config
* @param[in] nsc Namespace context * @param[in] nsc Namespace context
* @param[out] namespace * @param[out] namespace
* @param[out] address * @param[out] address Address as string, eg "0.0.0.0", "::"
* @param[out] addrtype One of inet:ipv4-address or inet:ipv6-address
* @param[out] port * @param[out] port
* @param[out] ssl * @param[out] ssl
*/ */
static int static int
cx_evhtp_socket(clicon_handle h, cx_evhtp_socket_extract(clicon_handle h,
cxobj *xs, cxobj *xs,
cvec *nsc, cvec *nsc,
char **namespace, char **namespace,
char **address, char **address,
char **addrtype,
uint16_t *port, uint16_t *port,
uint16_t *ssl) uint16_t *ssl)
{ {
@ -726,6 +816,10 @@ cx_evhtp_socket(clicon_handle h,
char *str = NULL; char *str = NULL;
char *reason = NULL; char *reason = NULL;
int ret; int ret;
char *body;
cg_var *cv = NULL;
yang_stmt *y;
yang_stmt *ysub = NULL;
if ((x = xpath_first(xs, nsc, "namespace")) == NULL){ if ((x = xpath_first(xs, nsc, "namespace")) == NULL){
clicon_err(OE_XML, EINVAL, "Mandatory namespace not given"); clicon_err(OE_XML, EINVAL, "Mandatory namespace not given");
@ -736,7 +830,35 @@ cx_evhtp_socket(clicon_handle h,
clicon_err(OE_XML, EINVAL, "Mandatory address not given"); clicon_err(OE_XML, EINVAL, "Mandatory address not given");
goto done; goto done;
} }
*address = xml_body(x); /* address is a union type and needs a special investigation to see which type (ipv4 or ipv6)
* the address is
*/
body = xml_body(x);
y = xml_spec(x);
if ((cv = cv_dup(yang_cv_get(y))) == NULL){
clicon_err(OE_UNIX, errno, "cv_dup");
goto done;
}
if ((ret = cv_parse1(body, cv, &reason)) < 0){
clicon_err(OE_XML, errno, "cv_parse1");
goto done;
}
if (ret == 0){
clicon_err(OE_XML, EFAULT, "%s", reason);
goto done;
}
if ((ret = ys_cv_validate(h, cv, y, &ysub, &reason)) < 0)
goto done;
if (ret == 0){
clicon_err(OE_XML, EFAULT, "Validation os address: %s", reason);
goto done;
}
if (ysub == NULL){
clicon_err(OE_XML, EFAULT, "No address union type");
goto done;
}
*address = body;
*addrtype = yang_argument_get(ysub);
if ((x = xpath_first(xs, nsc, "port")) != NULL && if ((x = xpath_first(xs, nsc, "port")) != NULL &&
(str = xml_body(x)) != NULL){ (str = xml_body(x)) != NULL){
if ((ret = parse_uint16(str, port, &reason)) < 0){ if ((ret = parse_uint16(str, port, &reason)) < 0){
@ -767,7 +889,110 @@ cx_evhtp_socket(clicon_handle h,
return retval; return retval;
} }
/*! Phase 2 of evhtp init, config has been retrieved from backend static int
cx_htp_add(cx_evhtp_handle *eh,
evhtp_t *htp)
{
eh->eh_htplen++;
if ((eh->eh_htpvec = realloc(eh->eh_htpvec, eh->eh_htplen*sizeof(htp))) == NULL){
clicon_err(OE_UNIX, errno, "realloc");
return -1;
}
eh->eh_htpvec[eh->eh_htplen-1] = htp;
return 0;
}
/*! Phase 2 of backend evhtp init, config single socket
* @param[in] h Clicon handle
* @param[in] eh Evhtp handle
* @param[in] ssl_enable Server is SSL enabled
* @param[in] xs XML config of single restconf socket
* @param[in] nsc Namespace context
*/
static int
cx_evhtp_socket(clicon_handle h,
cx_evhtp_handle *eh,
int ssl_enable,
cxobj *xs,
cvec *nsc,
char *server_cert_path,
char *server_key_path,
char *server_ca_cert_path,
int auth_type_client_certificate)
{
int retval = -1;
char *namespace = NULL;
char *address = NULL;
char *addrtype = NULL;
uint16_t ssl = 0;
uint16_t port = 0;
int ss;
evhtp_t *htp = NULL;
/* This is socket create a new evhtp_t instance */
if ((htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
#ifndef EVHTP_DISABLE_EVTHR /* threads */
evhtp_use_threads_wexit(htp, NULL, NULL, 4, NULL);
#endif
/* Callback before the connection is accepted. */
evhtp_set_pre_accept_cb(htp, cx_pre_accept, h);
/* Callback right after a connection is accepted. */
evhtp_set_post_accept_cb(htp, cx_post_accept, h);
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Generic callback called if no other callbacks are matched */
evhtp_set_gencb(htp, cx_gencb, h);
/* Extract socket parameters from single socket config: ns, addr, port, ssl */
if (cx_evhtp_socket_extract(h, xs, nsc, &namespace, &address, &addrtype, &port, &ssl) < 0)
goto done;
/* Sanity checks of socket parameters */
if (ssl){
if (ssl_enable == 0 || server_cert_path==NULL || server_key_path == NULL){
clicon_err(OE_XML, EINVAL, "Enabled SSL server requires server_cert_path and server_key_path");
goto done;
}
// ssl_verify_mode = htp_sslutil_verify2opts(optarg);
if (evhtp_ssl_init(htp, eh->eh_ssl_config) < 0){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
}
/* Open restconf socket and bind */
if (restconf_socket_init(h, address, addrtype, port, &ss) < 0)
goto done;
/* ss is a server socket that the clients connect to. The callback
therefore accepts clients on ss */
/* XXX address in evhtp should be prefixed with eg "ipv4:" */
evutil_make_socket_closeonexec(ss); // XXX
evutil_make_socket_nonblocking(ss); // XXX
if (evhtp_accept_socket(htp, ss, SOCKET_LISTEN_BACKLOG) < 0) {
/* accept_socket() does not close the descriptor
* on error, but this function does.
*/
evutil_closesocket(ss);
goto done;
}
if (cx_htp_add(eh, htp) < 0)
goto done;
retval = 0;
done:
return retval;
}
/*! Phase 2 of backend evhtp init, config has been retrieved from backend
* @param[in] h Clicon handle * @param[in] h Clicon handle
* @param[in] xconfig XML config * @param[in] xconfig XML config
* @param[in] nsc Namespace context * @param[in] nsc Namespace context
@ -780,23 +1005,20 @@ cx_evhtp_init(clicon_handle h,
cxobj *xconfig, cxobj *xconfig,
cvec *nsc, cvec *nsc,
cx_evhtp_handle *eh) cx_evhtp_handle *eh)
{ {
int retval = -1; int retval = -1;
int auth_type_client_certificate = 0;
uint16_t port = 0;
cxobj *xrestconf; cxobj *xrestconf;
cxobj **vec = NULL; cxobj **vec = NULL;
size_t veclen; size_t veclen;
char *auth_type = NULL;
char *server_cert_path = NULL; char *server_cert_path = NULL;
char *server_key_path = NULL; char *server_key_path = NULL;
char *server_ca_cert_path = NULL; char *server_ca_cert_path = NULL;
char *auth_type = NULL;
int auth_type_client_certificate = 0;
//XXX char *client_cert_ca = NULL; //XXX char *client_cert_ca = NULL;
cxobj *x; cxobj *x;
char *namespace = NULL; int i;
char *address = NULL; int ssl_enable = 0;
uint16_t use_ssl_server = 0;
/* Extract socket fields from xconfig */ /* Extract socket fields from xconfig */
if ((xrestconf = xpath_first(xconfig, nsc, "restconf")) == NULL){ if ((xrestconf = xpath_first(xconfig, nsc, "restconf")) == NULL){
@ -804,6 +1026,8 @@ cx_evhtp_init(clicon_handle h,
goto done; goto done;
} }
/* get common fields */ /* get common fields */
if ((x = xpath_first(xrestconf, nsc, "ssl-enable")) != NULL)
ssl_enable = (strcmp(xml_body(x),"true")==0);
if ((x = xpath_first(xrestconf, nsc, "auth-type")) != NULL) /* XXX: leaf-list? */ if ((x = xpath_first(xrestconf, nsc, "auth-type")) != NULL) /* XXX: leaf-list? */
auth_type = xml_body(x); auth_type = xml_body(x);
if (auth_type && strcmp(auth_type, "client-certificate") == 0) if (auth_type && strcmp(auth_type, "client-certificate") == 0)
@ -814,42 +1038,9 @@ cx_evhtp_init(clicon_handle h,
server_key_path = xml_body(x); server_key_path = xml_body(x);
if ((x = xpath_first(xrestconf, nsc, "server-ca-cert-path")) != NULL) if ((x = xpath_first(xrestconf, nsc, "server-ca-cert-path")) != NULL)
server_ca_cert_path = xml_body(x); server_ca_cert_path = xml_body(x);
/* get the list of socket config-data */
if (xpath_vec(xrestconf, nsc, "socket", &vec, &veclen) < 0) /* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */
goto done; if (ssl_enable){
/* Accept only a single socket XXX */
if (veclen != 1){
clicon_err(OE_XML, EINVAL, "Only single socket supported"); /* XXX warning: accept more? */
goto done;
}
if (cx_evhtp_socket(h, vec[0], nsc, &namespace, &address, &port, &use_ssl_server) < 0)
goto done;
if (use_ssl_server &&
(server_cert_path==NULL || server_key_path == NULL)){
clicon_err(OE_XML, EINVAL, "Enabled SSL server requires server_cert_path and server_key_path");
goto done;
}
if (auth_type_client_certificate){
if (!use_ssl_server){
clicon_err(OE_XML, EINVAL, "Client certificate authentication type requires SSL");
goto done;
}
if (server_ca_cert_path == NULL){
clicon_err(OE_XML, EINVAL, "Client certificate authentication type requires server-ca-cert-path");
goto done;
}
}
/* Init evhtp */
if ((eh->eh_evbase = event_base_new()) == NULL){
clicon_err(OE_UNIX, errno, "event_base_new");
goto done;
}
/* create a new evhtp_t instance */
if ((eh->eh_htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
if (use_ssl_server){
/* Init evhtp ssl config struct */ /* Init evhtp ssl config struct */
if ((eh->eh_ssl_config = malloc(sizeof(evhtp_ssl_cfg_t))) == NULL){ if ((eh->eh_ssl_config = malloc(sizeof(evhtp_ssl_cfg_t))) == NULL){
clicon_err(OE_UNIX, errno, "malloc"); clicon_err(OE_UNIX, errno, "malloc");
@ -858,12 +1049,12 @@ cx_evhtp_init(clicon_handle h,
memset(eh->eh_ssl_config, 0, sizeof(evhtp_ssl_cfg_t)); memset(eh->eh_ssl_config, 0, sizeof(evhtp_ssl_cfg_t));
eh->eh_ssl_config->ssl_opts = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1; eh->eh_ssl_config->ssl_opts = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1;
if (cx_get_ssl_server_certs(h, server_cert_path, /* Read server ssl files cert and key */
server_key_path, if (cx_get_ssl_server_certs(h, server_cert_path, server_key_path, eh->eh_ssl_config) < 0)
eh->eh_ssl_config) < 0)
goto done; goto done;
/* If client auth get client CA cert */
if (auth_type_client_certificate) if (auth_type_client_certificate)
if (cx_get_ssl_client_certs(h, server_ca_cert_path, eh->eh_ssl_config) < 0) if (cx_get_ssl_client_ca_certs(h, server_ca_cert_path, eh->eh_ssl_config) < 0)
goto done; goto done;
eh->eh_ssl_config->x509_verify_cb = cx_verify_certs; /* Is extra verification necessary? */ eh->eh_ssl_config->x509_verify_cb = cx_verify_certs; /* Is extra verification necessary? */
if (auth_type_client_certificate){ if (auth_type_client_certificate){
@ -873,51 +1064,14 @@ cx_evhtp_init(clicon_handle h,
} }
// ssl_verify_mode = htp_sslutil_verify2opts(optarg); // ssl_verify_mode = htp_sslutil_verify2opts(optarg);
} }
assert(SSL_VERIFY_NONE == 0);
/* Init evhtp */ /* get the list of socket config-data */
if ((eh->eh_evbase = event_base_new()) == NULL){ if (xpath_vec(xrestconf, nsc, "socket", &vec, &veclen) < 0)
clicon_err(OE_UNIX, errno, "event_base_new");
goto done; goto done;
} for (i=0; i<veclen; i++){
/* create a new evhtp_t instance */ if (cx_evhtp_socket(h, eh, ssl_enable, vec[i], nsc,
if ((eh->eh_htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){ server_cert_path, server_key_path, server_ca_cert_path,
clicon_err(OE_UNIX, errno, "evhtp_new"); auth_type_client_certificate) < 0)
goto done;
}
/* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */
if (use_ssl_server){
if (evhtp_ssl_init(eh->eh_htp, eh->eh_ssl_config) < 0){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
}
#ifndef EVHTP_DISABLE_EVTHR /* threads */
evhtp_use_threads_wexit(eh->eh_htp, NULL, NULL, 4, NULL);
#endif
/* Callback before the connection is accepted. */
evhtp_set_pre_accept_cb(eh->eh_htp, cx_pre_accept, h);
/* Callback right after a connection is accepted. */
evhtp_set_post_accept_cb(eh->eh_htp, cx_post_accept, h);
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(eh->eh_htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(eh->eh_htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Generic callback called if no other callbacks are matched */
evhtp_set_gencb(eh->eh_htp, cx_gencb, h);
if (evhtp_bind_socket(eh->eh_htp, /* evhtp handle */
address, /* string address, eg ipv4:<ipv4addr> */
port, /* port */
SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */
) < 0){
clicon_err(OE_UNIX, errno, "evhtp_bind_socket");
goto done; goto done;
} }
retval = 0; retval = 0;
@ -930,6 +1084,7 @@ cx_evhtp_init(clicon_handle h,
/*! Read config from backend */ /*! Read config from backend */
int int
restconf_config_backend(clicon_handle h, restconf_config_backend(clicon_handle h,
cx_evhtp_handle *eh,
int argc, int argc,
char **argv, char **argv,
int drop_privileges) int drop_privileges)
@ -945,8 +1100,8 @@ restconf_config_backend(clicon_handle h,
size_t cligen_bufthreshold; size_t cligen_bufthreshold;
cvec *nsc = NULL; cvec *nsc = NULL;
cxobj *xconfig = NULL; cxobj *xconfig = NULL;
cxobj *xerr = NULL;
uint32_t id = 0; /* Session id, to poll backend up */ uint32_t id = 0; /* Session id, to poll backend up */
cx_evhtp_handle *eh = NULL;
/* Set default namespace according to CLICON_NAMESPACE_NETCONF_DEFAULT */ /* Set default namespace according to CLICON_NAMESPACE_NETCONF_DEFAULT */
xml_nsctx_namespace_netconf_default(h); xml_nsctx_namespace_netconf_default(h);
@ -1052,13 +1207,16 @@ restconf_config_backend(clicon_handle h,
goto done; goto done;
if (clicon_rpc_get_config(h, NULL, "running", "/restconf", nsc, &xconfig) < 0) if (clicon_rpc_get_config(h, NULL, "running", "/restconf", nsc, &xconfig) < 0)
goto done; goto done;
/* Initialize evhtp handle - fill it in cx_evhtp_init */ if ((xerr = xpath_first(xconfig, NULL, "/rpc-error")) != NULL){
if ((eh = malloc(sizeof *eh)) == NULL){ clixon_netconf_error(xerr, "Get backend restconf config", NULL);
clicon_err(OE_UNIX, errno, "malloc");
goto done; goto done;
} }
memset(eh, 0, sizeof *eh); /* Init evhtp, common stuff */
_EVHTP_HANDLE = eh; /* global */ if ((eh->eh_evbase = event_base_new()) == NULL){
clicon_err(OE_UNIX, errno, "event_base_new");
goto done;
}
if (cx_evhtp_init(h, xconfig, nsc, eh) < 0) if (cx_evhtp_init(h, xconfig, nsc, eh) < 0)
goto done; goto done;
/* Drop privileges after evhtp and server key/cert read */ /* Drop privileges after evhtp and server key/cert read */
@ -1069,7 +1227,6 @@ restconf_config_backend(clicon_handle h,
} }
/* libevent main loop */ /* libevent main loop */
event_base_loop(eh->eh_evbase, 0); /* XXX: replace with clixon_event_loop() */ event_base_loop(eh->eh_evbase, 0); /* XXX: replace with clixon_event_loop() */
retval = 0; retval = 0;
done: done:
if (xconfig) if (xconfig)
@ -1083,6 +1240,7 @@ restconf_config_backend(clicon_handle h,
/*! Read config locally */ /*! Read config locally */
int int
restconf_config_local(clicon_handle h, restconf_config_local(clicon_handle h,
cx_evhtp_handle *eh,
int argc, int argc,
char **argv, char **argv,
uint16_t port, uint16_t port,
@ -1101,7 +1259,7 @@ restconf_config_local(clicon_handle h,
size_t cligen_bufthreshold; size_t cligen_bufthreshold;
char *restconf_ipv4_addr = NULL; char *restconf_ipv4_addr = NULL;
char *restconf_ipv6_addr = NULL; char *restconf_ipv6_addr = NULL;
cx_evhtp_handle *eh = NULL; evhtp_t *htp;
/* port = defaultport unless explicitly set -P */ /* port = defaultport unless explicitly set -P */
if (port == 0){ if (port == 0){
@ -1111,13 +1269,6 @@ restconf_config_local(clicon_handle h,
/* Set default namespace according to CLICON_NAMESPACE_NETCONF_DEFAULT */ /* Set default namespace according to CLICON_NAMESPACE_NETCONF_DEFAULT */
xml_nsctx_namespace_netconf_default(h); xml_nsctx_namespace_netconf_default(h);
/* Initialize evhtp handle */
if ((eh = malloc(sizeof *eh)) == NULL){
clicon_err(OE_UNIX, errno, "malloc");
goto done;
}
memset(eh, 0, sizeof *eh);
_EVHTP_HANDLE = eh; /* global */
/* Check server ssl certs */ /* Check server ssl certs */
if (use_ssl){ if (use_ssl){
/* Init evhtp ssl config struct */ /* Init evhtp ssl config struct */
@ -1148,39 +1299,6 @@ restconf_config_local(clicon_handle h,
clicon_err(OE_UNIX, errno, "event_base_new"); clicon_err(OE_UNIX, errno, "event_base_new");
goto done; goto done;
} }
/* create a new evhtp_t instance */
if ((eh->eh_htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
/* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */
if (use_ssl){
if (evhtp_ssl_init(eh->eh_htp, eh->eh_ssl_config) < 0){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
}
#ifndef EVHTP_DISABLE_EVTHR
evhtp_use_threads_wexit(eh->eh_htp, NULL, NULL, 4, NULL);
#endif
/* Callback before the connection is accepted. */
evhtp_set_pre_accept_cb(eh->eh_htp, cx_pre_accept, h);
/* Callback right after a connection is accepted. */
evhtp_set_post_accept_cb(eh->eh_htp, cx_post_accept, h);
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(eh->eh_htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(eh->eh_htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Generic callback called if no other callbacks are matched */
evhtp_set_gencb(eh->eh_htp, cx_gencb, h);
/* bind to a socket, optionally with specific protocol support formatting /* bind to a socket, optionally with specific protocol support formatting
*/ */
@ -1193,12 +1311,47 @@ restconf_config_local(clicon_handle h,
} }
if (restconf_ipv4_addr != NULL && strlen(restconf_ipv4_addr)){ if (restconf_ipv4_addr != NULL && strlen(restconf_ipv4_addr)){
cbuf *cb; cbuf *cb;
/* create a new evhtp_t instance */
if ((htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
/* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */
if (use_ssl){
if (evhtp_ssl_init(htp, eh->eh_ssl_config) < 0){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
}
#ifndef EVHTP_DISABLE_EVTHR
evhtp_use_threads_wexit(htp, NULL, NULL, 4, NULL);
#endif
/* Callback before the connection is accepted. */
evhtp_set_pre_accept_cb(htp, cx_pre_accept, h);
/* Callback right after a connection is accepted. */
evhtp_set_post_accept_cb(htp, cx_post_accept, h);
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Generic callback called if no other callbacks are matched */
evhtp_set_gencb(htp, cx_gencb, h);
if ((cb = cbuf_new()) == NULL){ if ((cb = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new"); clicon_err(OE_UNIX, errno, "cbuf_new");
goto done; goto done;
} }
cprintf(cb, "ipv4:%s", restconf_ipv4_addr); cprintf(cb, "ipv4:%s", restconf_ipv4_addr);
if (evhtp_bind_socket(eh->eh_htp, /* evhtp handle */ if (evhtp_bind_socket(htp, /* evhtp handle */
cbuf_get(cb), /* string address, eg ipv4:<ipv4addr> */ cbuf_get(cb), /* string address, eg ipv4:<ipv4addr> */
port, /* port */ port, /* port */
SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */ SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */
@ -1208,16 +1361,51 @@ restconf_config_local(clicon_handle h,
} }
if (cb) if (cb)
cbuf_free(cb); cbuf_free(cb);
if (cx_htp_add(eh, htp) < 0)
goto done;
} }
/* Eeh can only bind one */ /* Eeh can only bind one */
if (0 && restconf_ipv6_addr != NULL && strlen(restconf_ipv6_addr)){ if (restconf_ipv6_addr != NULL && strlen(restconf_ipv6_addr)){
cbuf *cb; cbuf *cb;
/* create a new evhtp_t instance */
if ((htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
/* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */
if (use_ssl){
if (evhtp_ssl_init(htp, eh->eh_ssl_config) < 0){
clicon_err(OE_UNIX, errno, "evhtp_new");
goto done;
}
}
#ifndef EVHTP_DISABLE_EVTHR
evhtp_use_threads_wexit(htp, NULL, NULL, 4, NULL);
#endif
/* Callback before the connection is accepted. */
evhtp_set_pre_accept_cb(htp, cx_pre_accept, h);
/* Callback right after a connection is accepted. */
evhtp_set_post_accept_cb(htp, cx_post_accept, h);
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Callback to be executed for all /restconf api calls */
if (evhtp_set_cb(htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){
clicon_err(OE_EVENTS, errno, "evhtp_set_cb");
goto done;
}
/* Generic callback called if no other callbacks are matched */
evhtp_set_gencb(htp, cx_gencb, h);
if ((cb = cbuf_new()) == NULL){ if ((cb = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new"); clicon_err(OE_UNIX, errno, "cbuf_new");
goto done; goto done;
} }
cprintf(cb, "ipv6:%s", restconf_ipv6_addr); cprintf(cb, "ipv6:%s", restconf_ipv6_addr);
if (evhtp_bind_socket(eh->eh_htp, /* evhtp handle */ if (evhtp_bind_socket(htp, /* evhtp handle */
cbuf_get(cb), /* string address, eg ipv6:<ipv6addr> */ cbuf_get(cb), /* string address, eg ipv6:<ipv6addr> */
port, /* port */ port, /* port */
SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */ SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */
@ -1227,6 +1415,8 @@ restconf_config_local(clicon_handle h,
} }
if (cb) if (cb)
cbuf_free(cb); cbuf_free(cb);
if (cx_htp_add(eh, htp) < 0)
goto done;
} }
if (drop_privileges){ if (drop_privileges){
@ -1491,10 +1681,16 @@ main(int argc,
/* port = defaultport unless explicitly set -P */ /* port = defaultport unless explicitly set -P */
if (port == 0) if (port == 0)
port = defaultport; port = defaultport;
if ((eh = malloc(sizeof *eh)) == NULL){
clicon_err(OE_UNIX, errno, "malloc");
goto done;
}
memset(eh, 0, sizeof *eh);
_EVHTP_HANDLE = eh; /* global */
if (clicon_option_bool(h, "CLICON_RESTCONF_CONFIG") == 0){ if (clicon_option_bool(h, "CLICON_RESTCONF_CONFIG") == 0){
/* Read config locally */ /* Read config locally */
if (restconf_config_local(h, argc, argv, if (restconf_config_local(h, eh, argc, argv,
port, port,
ssl_verify_clients, ssl_verify_clients,
use_ssl, use_ssl,
@ -1504,12 +1700,10 @@ main(int argc,
} }
else { else {
/* Read config from backend */ /* Read config from backend */
if (restconf_config_backend(h, argc, argv, drop_privileges) < 0) if (restconf_config_backend(h, eh, argc, argv, drop_privileges) < 0)
goto done; goto done;
} }
event_base_loop(eh->eh_evbase, 0);
retval = 0; retval = 0;
done: done:
clicon_debug(1, "restconf_main_evhtp done"); clicon_debug(1, "restconf_main_evhtp done");

View file

@ -31,6 +31,10 @@
# #
# ***** END LICENSE BLOCK ***** # ***** END LICENSE BLOCK *****
# #
# Build, compile and test a clixon-system container
# This is used in CI
# NOTE: restconf config is controlled at install time with ./configure --with-restconf=... option
VPATH = @srcdir@ VPATH = @srcdir@
srcdir = @srcdir@ srcdir = @srcdir@
top_srcdir = @top_srcdir@ top_srcdir = @top_srcdir@

View file

@ -61,7 +61,7 @@ int yang2cv_type(char *ytype, enum cv_type *cv_type);
char *cv2yang_type(enum cv_type cv_type); char *cv2yang_type(enum cv_type cv_type);
yang_stmt *yang_find_identity(yang_stmt *ys, char *identity); yang_stmt *yang_find_identity(yang_stmt *ys, char *identity);
yang_stmt *yang_find_identity_nsc(yang_stmt *yspec, char *identity, cvec *nsc); yang_stmt *yang_find_identity_nsc(yang_stmt *yspec, char *identity, cvec *nsc);
int ys_cv_validate(clicon_handle h, cg_var *cv, yang_stmt *ys, char **reason); int ys_cv_validate(clicon_handle h, cg_var *cv, yang_stmt *ys, yang_stmt **ysub, char **reason);
int clicon_type2cv(char *type, char *rtype, yang_stmt *ys, enum cv_type *cvtype); int clicon_type2cv(char *type, char *rtype, yang_stmt *ys, enum cv_type *cvtype);
int yang_type_get(yang_stmt *ys, char **otype, yang_stmt **restype, int yang_type_get(yang_stmt *ys, char **otype, yang_stmt **restype,
int *options, cvec **cvv, int *options, cvec **cvv,

View file

@ -1024,7 +1024,7 @@ xml_yang_validate_add(clicon_handle h,
goto fail; goto fail;
} }
} }
if ((ret = ys_cv_validate(h, cv, yt, &reason)) < 0) if ((ret = ys_cv_validate(h, cv, yt, NULL, &reason)) < 0)
goto done; goto done;
if (ret == 0){ if (ret == 0){
if (netconf_bad_element_xml(xret, "application", yang_argument_get(yt), reason) < 0) if (netconf_bad_element_xml(xret, "application", yang_argument_get(yt), reason) < 0)

View file

@ -221,7 +221,7 @@ compile_pattern2regexp(clicon_handle h,
/*! Resolve types: populate type caches /*! Resolve types: populate type caches
* @param[in] ys This is a type statement * @param[in] ys This is a type statement
* @param[in] arg Not used * @param[in] arg Not used
* Typically only called once when loading te yang type system. * Typically only called once when loading the yang type system.
* @note unions not cached * @note unions not cached
*/ */
int int
@ -741,7 +741,7 @@ cv_validate1(clicon_handle h,
/* Forward */ /* Forward */
static int ys_cv_validate_union(clicon_handle h,yang_stmt *ys, char **reason, static int ys_cv_validate_union(clicon_handle h,yang_stmt *ys, char **reason,
yang_stmt *yrestype, char *type, char *val); yang_stmt *yrestype, char *type, char *val, yang_stmt **ysubp);
/*! /*!
* @param[out] reason If given and return val is 0, contains a malloced string * @param[out] reason If given and return val is 0, contains a malloced string
@ -767,6 +767,7 @@ ys_cv_validate_union_one(clicon_handle h,
char *restype; char *restype;
enum cv_type cvtype; enum cv_type cvtype;
cg_var *cvt=NULL; cg_var *cvt=NULL;
yang_stmt *ysubt = NULL;
if ((regexps = cvec_new(0)) == NULL){ if ((regexps = cvec_new(0)) == NULL){
clicon_err(OE_UNIX, errno, "cvec_new"); clicon_err(OE_UNIX, errno, "cvec_new");
@ -781,7 +782,7 @@ ys_cv_validate_union_one(clicon_handle h,
goto done; goto done;
restype = yrt?yang_argument_get(yrt):NULL; restype = yrt?yang_argument_get(yrt):NULL;
if (restype && strcmp(restype, "union") == 0){ /* recursive union */ if (restype && strcmp(restype, "union") == 0){ /* recursive union */
if ((retval = ys_cv_validate_union(h, ys, reason, yrt, type, val)) < 0) if ((retval = ys_cv_validate_union(h, ys, reason, yrt, type, val, &ysubt)) < 0)
goto done; goto done;
} }
else { else {
@ -828,7 +829,11 @@ ys_cv_validate_union_one(clicon_handle h,
} }
/*! Validate union /*! Validate union
* @param[in] h Clixon handle
* @param[in] ys Yang statement (union)
* @param[out] reason If given, and return value is 0, contains malloced string * @param[out] reason If given, and return value is 0, contains malloced string
* @param[in] val Value to match
* @param[out] ysubp Sub-type of ys that matches val
* @retval -1 Error (fatal), with errno set to indicate error * @retval -1 Error (fatal), with errno set to indicate error
* @retval 0 Validation not OK, malloced reason is returned. Free reason with free() * @retval 0 Validation not OK, malloced reason is returned. Free reason with free()
* @retval 1 Validation OK * @retval 1 Validation OK
@ -839,7 +844,8 @@ ys_cv_validate_union(clicon_handle h,
char **reason, char **reason,
yang_stmt *yrestype, yang_stmt *yrestype,
char *type, /* orig type */ char *type, /* orig type */
char *val) char *val,
yang_stmt **ysubp)
{ {
int retval = 1; /* valid */ int retval = 1; /* valid */
yang_stmt *yt = NULL; yang_stmt *yt = NULL;
@ -859,9 +865,14 @@ ys_cv_validate_union(clicon_handle h,
reason1 = *reason; reason1 = *reason;
*reason = NULL; *reason = NULL;
} }
if (retval == 1) /* Enough that one type validates value */ /* Enough that one type validates value, return that value
*/
if (retval == 1) {
if (ysubp)
*ysubp = yt;
break; break;
} }
}
done: done:
if (retval == 0 && reason1){ if (retval == 0 && reason1){
*reason = reason1; *reason = reason1;
@ -877,6 +888,7 @@ ys_cv_validate_union(clicon_handle h,
* @param[in] h Clicon handle * @param[in] h Clicon handle
* @param[in] cv A cligen variable to validate. This is a correctly parsed cv. * @param[in] cv A cligen variable to validate. This is a correctly parsed cv.
* @param[in] ys A yang statement, must be leaf or leaf-list. * @param[in] ys A yang statement, must be leaf or leaf-list.
* @param[out] ysub Sub-type that matches val (in case of union, otherwise ys)
* @param[out] reason If given, and if return value is 0, contains malloced * @param[out] reason If given, and if return value is 0, contains malloced
* string describing reason why validation failed. * string describing reason why validation failed.
* @retval -1 Error (fatal), with errno set to indicate error * @retval -1 Error (fatal), with errno set to indicate error
@ -889,6 +901,7 @@ int
ys_cv_validate(clicon_handle h, ys_cv_validate(clicon_handle h,
cg_var *cv, cg_var *cv,
yang_stmt *ys, yang_stmt *ys,
yang_stmt **ysub,
char **reason) char **reason)
{ {
int retval = -1; int retval = -1;
@ -948,7 +961,7 @@ ys_cv_validate(clicon_handle h,
*/ */
if ((val = cv_string_get(cv)) == NULL) if ((val = cv_string_get(cv)) == NULL)
val = ""; val = "";
if ((retval2 = ys_cv_validate_union(h, ys, reason, yrestype, origtype, val)) < 0) if ((retval2 = ys_cv_validate_union(h, ys, reason, yrestype, origtype, val, ysub)) < 0)
goto done; goto done;
retval = retval2; /* invalid (0) with latest reason or valid 1 */ retval = retval2; /* invalid (0) with latest reason or valid 1 */
} }
@ -969,6 +982,8 @@ ys_cv_validate(clicon_handle h,
if ((retval = cv_validate1(h, cv, cvtype, options, cvv, if ((retval = cv_validate1(h, cv, cvtype, options, cvv,
regexps, yrestype, restype, reason)) < 0) regexps, yrestype, restype, reason)) < 0)
goto done; goto done;
if (ysub)
*ysub = ys;
} }
done: done:
if (origtype) if (origtype)

67
test/certs.sh Normal file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Create server certs
# Assume: the following variables set:
# $dir, $certdir, $srvkey, $srvcert, $cakey, $cacert
# and that $certdir exists
# 1. CA
cat<<EOF > $dir/ca.cnf
[ ca ]
default_ca = CA_default
[ CA_default ]
serial = ca-serial
crl = ca-crl.pem
database = ca-database.txt
name_opt = CA_default
cert_opt = CA_default
default_crl_days = 9999
default_md = md5
[ req ]
default_bits = 2048
days = 1
distinguished_name = req_distinguished_name
attributes = req_attributes
prompt = no
output_password = password
[ req_distinguished_name ]
C = SE
L = Stockholm
O = Clixon
OU = clixon
CN = ca
emailAddress = olof@hagsand.se
[ req_attributes ]
challengePassword = test
EOF
# Generate CA cert
openssl req -x509 -days 1 -config $dir/ca.cnf -keyout $cakey -out $cacert
cat<<EOF > $dir/srv.cnf
[req]
prompt = no
distinguished_name = dn
req_extensions = ext
[dn]
CN = www.clicon.org # localhost
emailAddress = olof@hagsand.se
O = Clixon
L = Stockholm
C = SE
[ext]
subjectAltName = DNS:clicon.org
EOF
# Generate server key
openssl genrsa -out $srvkey 2048
# Generate CSR (signing request)
openssl req -new -config $dir/srv.cnf -key $srvkey -out $certdir/srv_csr.pem
# Sign server cert by CA
openssl x509 -req -extfile $dir/srv.cnf -days 1 -passin "pass:password" -in $certdir/srv_csr.pem -CA $cacert -CAkey $cakey -CAcreateserial -out $srvcert

View file

@ -42,6 +42,7 @@ if [ -f ./config.sh ]; then
fi fi
# Sanity nginx running on systemd platforms # Sanity nginx running on systemd platforms
# ./lib.sh: line 45: systemctl: command not found
if systemctl > /dev/null; then if systemctl > /dev/null; then
nginxactive=$(systemctl show nginx |grep ActiveState=active) nginxactive=$(systemctl show nginx |grep ActiveState=active)
if [ "${WITH_RESTCONF}" = "fcgi" ]; then if [ "${WITH_RESTCONF}" = "fcgi" ]; then
@ -273,8 +274,8 @@ wait_backend(){
# @see wait_restconf # @see wait_restconf
start_restconf(){ start_restconf(){
# Start in background # Start in background
if [ $RCPROTO = https ]; then if [ $RCPROTO = https -a "${WITH_RESTCONF}" = "evhtp" ]; then
EXTRA="-s" # server certs EXTRA="-s" # server certs ONLY evhtp
else else
EXTRA= EXTRA=
fi fi

View file

@ -1,7 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Restconf basic functionality # Restconf basic functionality also uri encoding using eth/0/0
# also uri encoding using eth/0/0 # Note there are many variants: (1)fcgi/evhtp, (2) http/https, (3) IPv4/IPv6, (4)local or backend-config
# Assume http server setup, such as nginx described in apps/restconf/README.md # (1) fcgi/evhtp
# This is compile-time --with-restconf=fcgi or evhtp, so either or
# - fcgi: Assume http server setup, such as nginx described in apps/restconf/README.md
# - evhtp: test both local config and get config from backend
# (2) http/https
# - fcgi: relies on nginx has https setup
# - evhtp: generate self-signed server certs
# (3) IPv4/IPv6 (only loopback 127.0.0.1 / ::1)
# - The tests runs through both
# (4) local/backend config. Evhtp only
# - The tests runs through both (if compiled with evhtp)
# See also test_restconf2.sh
# Magic line must be first in script (see README.md) # Magic line must be first in script (see README.md)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
@ -29,23 +40,61 @@ cat <<EOF > $cfg
<CLICON_BACKEND_PIDFILE>/usr/local/var/$APPNAME/$APPNAME.pidfile</CLICON_BACKEND_PIDFILE> <CLICON_BACKEND_PIDFILE>/usr/local/var/$APPNAME/$APPNAME.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR> <CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
<CLICON_MODULE_LIBRARY_RFC7895>true</CLICON_MODULE_LIBRARY_RFC7895> <CLICON_MODULE_LIBRARY_RFC7895>true</CLICON_MODULE_LIBRARY_RFC7895>
EOF
if [ "${WITH_RESTCONF}" = "evhtp" ]; then
# Create server certs
certdir=$dir/certs
srvkey=$certdir/srv_key.pem
srvcert=$certdir/srv_cert.pem
cakey=$certdir/ca_key.pem # needed?
cacert=$certdir/ca_cert.pem
test -d $certdir || mkdir $certdir
. ./certs.sh
cat <<EOF >> $cfg
<CLICON_SSL_SERVER_CERT>$srvcert</CLICON_SSL_SERVER_CERT>
<CLICON_SSL_SERVER_KEY>$srvkey</CLICON_SSL_SERVER_KEY>
<CLICON_SSL_CA_CERT>$srvcert</CLICON_SSL_CA_CERT>
EOF
fi
cat <<EOF >> $cfg
</clixon-config> </clixon-config>
EOF EOF
# This is a fixed 'state' implemented in routing_backend. It is assumed to be always there # This is a fixed 'state' implemented in routing_backend. It is assumed to be always there
state='{"clixon-example:state":{"op":\["41","42","43"\]}' state='{"clixon-example:state":{"op":\["41","42","43"\]}'
if [ ${RCPROTO} = "https" ]; then # For backend config, create 4 sockets, all combinations IPv4/IPv6 + http/https
ssl=true RESTCONFCONFIG=$(cat <<EOF
port=443 <restconf xmlns="https://clicon.org/restconf">
else <ssl-enable>true</ssl-enable>
ssl=false <auth-type>password</auth-type>
port=80 <server-cert-path>$srvcert</server-cert-path>
fi <server-key-path>$srvkey</server-key-path>
<server-ca-cert-path>$cakey</server-ca-cert-path>
<socket><namespace>default</namespace><address>0.0.0.0</address><port>80</port><ssl>false</ssl></socket>
<socket><namespace>default</namespace><address>::</address><port>80</port><ssl>false</ssl></socket>
<socket><namespace>default</namespace><address>0.0.0.0</address><port>443</port><ssl>true</ssl></socket>
<socket><namespace>default</namespace><address>::</address><port>443</port><ssl>true</ssl></socket>
</restconf>
EOF
)
# Restconf test routine with arguments:
# 1. proto:http/https
# 2: addr: 127.0.0.1/::1 # IPv4 or IPv6
# 3: config: local / backend config (evhtp only)
testrun() testrun()
{ {
USEBACKEND=$1 proto=$1 # http/https
addr=$2 # 127.0.0.1/::1
config=$3 # local/backend
RCPROTO=$proto # for start/wait of restconf
echo "proto:$proto"
echo "addr:$addr"
echo "config:$config"
new "test params: -f $cfg -- -s" new "test params: -f $cfg -- -s"
if [ $BE -ne 0 ]; then if [ $BE -ne 0 ]; then
@ -63,10 +112,10 @@ testrun()
new "wait backend" new "wait backend"
wait_backend wait_backend
if $USEBACKEND; then if [ $config = backend ] ; then # Create a backend config
# restconf backend config
new "netconf edit config" new "netconf edit config"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc $DEFAULTNS><edit-config><target><candidate/></target><config><restconf xmlns=\"https://clicon.org/restconf\"><socket><namespace>default</namespace><address>0.0.0.0</address><port>$port</port><ssl>$ssl</ssl></socket><auth-type>password</auth-type></restconf></config></edit-config></rpc>]]>]]>" expecteof "$clixon_netconf -qf $cfg" 0 "<rpc $DEFAULTNS><edit-config><target><candidate/></target><config>$RESTCONFCONFIG</config></edit-config></rpc>]]>]]>" "^<rpc-reply $DEFAULTNS><ok/></rpc-reply>]]>]]>$"
"^<rpc-reply $DEFAULTNS><ok/></rpc-reply>]]>]]>$"
new "netconf commit" new "netconf commit"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc $DEFAULTNS><commit/></rpc>]]>]]>" "^<rpc-reply $DEFAULTNS><ok/></rpc-reply>]]>]]>$" expecteof "$clixon_netconf -qf $cfg" 0 "<rpc $DEFAULTNS><commit/></rpc>]]>]]>" "^<rpc-reply $DEFAULTNS><ok/></rpc-reply>]]>]]>$"
@ -76,7 +125,7 @@ testrun()
new "kill old restconf daemon" new "kill old restconf daemon"
stop_restconf_pre stop_restconf_pre
if $USEBACKEND; then if [ $config = backend ] ; then # Add -b option
new "start restconf daemon -b" new "start restconf daemon -b"
start_restconf -f $cfg -b start_restconf -f $cfg -b
else else
@ -88,21 +137,21 @@ testrun()
wait_restconf wait_restconf
new "restconf root discovery. RFC 8040 3.1 (xml+xrd)" new "restconf root discovery. RFC 8040 3.1 (xml+xrd)"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/.well-known/host-meta)" 0 'HTTP/1.1 200 OK' "<XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'>" "<Link rel='restconf' href='/restconf'/>" "</XRD>" expectpart "$(curl $CURLOPTS -X GET $proto://$addr/.well-known/host-meta)" 0 'HTTP/1.1 200 OK' "<XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'>" "<Link rel='restconf' href='/restconf'/>" "</XRD>"
new "restconf get restconf resource. RFC 8040 3.3 (json)" new "restconf get restconf resource. RFC 8040 3.3 (json)"
expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf)" 0 'HTTP/1.1 200 OK' '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2019-01-04"}}' expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $proto://$addr/restconf)" 0 'HTTP/1.1 200 OK' '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2019-01-04"}}'
new "restconf get restconf resource. RFC 8040 3.3 (xml)" new "restconf get restconf resource. RFC 8040 3.3 (xml)"
# Get XML instead of JSON? # Get XML instead of JSON?
expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf)" 0 'HTTP/1.1 200 OK' '<restconf xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><data/><operations/><yang-library-version>2019-01-04</yang-library-version></restconf>' expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $proto://$addr/restconf)" 0 'HTTP/1.1 200 OK' '<restconf xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><data/><operations/><yang-library-version>2019-01-04</yang-library-version></restconf>'
# Should be alphabetically ordered # Should be alphabetically ordered
new "restconf get restconf/operations. RFC8040 3.3.2 (json)" new "restconf get restconf/operations. RFC8040 3.3.2 (json)"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/operations)" 0 'HTTP/1.1 200 OK' '{"operations":{"clixon-example:client-rpc":\[null\],"clixon-example:empty":\[null\],"clixon-example:optional":\[null\],"clixon-example:example":\[null\],"clixon-lib:debug":\[null\],"clixon-lib:ping":\[null\],"clixon-lib:stats":\[null\],"clixon-lib:restart-plugin":\[null\],"ietf-netconf:get-config":\[null\],"ietf-netconf:edit-config":\[null\],"ietf-netconf:copy-config":\[null\],"ietf-netconf:delete-config":\[null\],"ietf-netconf:lock":\[null\],"ietf-netconf:unlock":\[null\],"ietf-netconf:get":\[null\],"ietf-netconf:close-session":\[null\],"ietf-netconf:kill-session":\[null\],"ietf-netconf:commit":\[null\],"ietf-netconf:discard-changes":\[null\],"ietf-netconf:validate":\[null\]' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/operations)" 0 'HTTP/1.1 200 OK' '{"operations":{"clixon-example:client-rpc":\[null\],"clixon-example:empty":\[null\],"clixon-example:optional":\[null\],"clixon-example:example":\[null\],"clixon-lib:debug":\[null\],"clixon-lib:ping":\[null\],"clixon-lib:stats":\[null\],"clixon-lib:restart-plugin":\[null\],"ietf-netconf:get-config":\[null\],"ietf-netconf:edit-config":\[null\],"ietf-netconf:copy-config":\[null\],"ietf-netconf:delete-config":\[null\],"ietf-netconf:lock":\[null\],"ietf-netconf:unlock":\[null\],"ietf-netconf:get":\[null\],"ietf-netconf:close-session":\[null\],"ietf-netconf:kill-session":\[null\],"ietf-netconf:commit":\[null\],"ietf-netconf:discard-changes":\[null\],"ietf-netconf:validate":\[null\]'
new "restconf get restconf/operations. RFC8040 3.3.2 (xml)" new "restconf get restconf/operations. RFC8040 3.3.2 (xml)"
ret=$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $RCPROTO://localhost/restconf/operations) ret=$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $proto://$addr/restconf/operations)
expect='<operations><client-rpc xmlns="urn:example:clixon"/><empty xmlns="urn:example:clixon"/><optional xmlns="urn:example:clixon"/><example xmlns="urn:example:clixon"/><debug xmlns="http://clicon.org/lib"/><ping xmlns="http://clicon.org/lib"/><stats xmlns="http://clicon.org/lib"/><restart-plugin xmlns="http://clicon.org/lib"/><get-config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><edit-config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><copy-config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><delete-config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><lock xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><unlock xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><get xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><close-session xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><kill-session xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><commit xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><discard-changes xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><validate xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/>' expect='<operations><client-rpc xmlns="urn:example:clixon"/><empty xmlns="urn:example:clixon"/><optional xmlns="urn:example:clixon"/><example xmlns="urn:example:clixon"/><debug xmlns="http://clicon.org/lib"/><ping xmlns="http://clicon.org/lib"/><stats xmlns="http://clicon.org/lib"/><restart-plugin xmlns="http://clicon.org/lib"/><get-config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><edit-config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><copy-config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><delete-config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><lock xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><unlock xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><get xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><close-session xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><kill-session xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><commit xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><discard-changes xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/><validate xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"/>'
match=`echo $ret | grep --null -Eo "$expect"` match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then if [ -z "$match" ]; then
@ -110,10 +159,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi fi
new "restconf get restconf/yang-library-version. RFC8040 3.3.3" new "restconf get restconf/yang-library-version. RFC8040 3.3.3"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/yang-library-version)" 0 'HTTP/1.1 200 OK' '{"yang-library-version":"2019-01-04"}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/yang-library-version)" 0 'HTTP/1.1 200 OK' '{"yang-library-version":"2019-01-04"}'
new "restconf get restconf/yang-library-version. RFC8040 3.3.3 (xml)" new "restconf get restconf/yang-library-version. RFC8040 3.3.3 (xml)"
ret=$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $RCPROTO://localhost/restconf/yang-library-version) ret=$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $proto://$addr/restconf/yang-library-version)
expect="<yang-library-version>2019-01-04</yang-library-version>" expect="<yang-library-version>2019-01-04</yang-library-version>"
match=`echo $ret | grep --null -Eo "$expect"` match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then if [ -z "$match" ]; then
@ -121,48 +170,48 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi fi
new "restconf schema resource, RFC 8040 sec 3.7 according to RFC 7895 (explicit resource)" new "restconf schema resource, RFC 8040 sec 3.7 according to RFC 7895 (explicit resource)"
expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-yang-library:modules-state/module=ietf-interfaces,2018-02-20)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:module":\[{"name":"ietf-interfaces","revision":"2018-02-20","namespace":"urn:ietf:params:xml:ns:yang:ietf-interfaces","conformance-type":"implement"}\]}' expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-yang-library:modules-state/module=ietf-interfaces,2018-02-20)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:module":\[{"name":"ietf-interfaces","revision":"2018-02-20","namespace":"urn:ietf:params:xml:ns:yang:ietf-interfaces","conformance-type":"implement"}\]}'
new "restconf schema resource, mod-state top-level" new "restconf schema resource, mod-state top-level"
expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-yang-library:modules-state)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:modules-state":{"module-set-id":"0","module":\[{"name":"clixon-example","revision":"2020-03-11","namespace":"urn:example:clixon","conformance-type":"implement"},{"name":"clixon-lib","revision":"2020-04-23","' expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-yang-library:modules-state)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:modules-state":{"module-set-id":"0","module":\[{"name":"clixon-example","revision":"2020-03-11","namespace":"urn:example:clixon","conformance-type":"implement"},{"name":"clixon-lib","revision":"2020-04-23","'
new "restconf options. RFC 8040 4.1" new "restconf options. RFC 8040 4.1"
expectpart "$(curl $CURLOPTS -X OPTIONS $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" expectpart "$(curl $CURLOPTS -X OPTIONS $proto://$addr/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE"
# -I means HEAD # -I means HEAD
new "restconf HEAD. RFC 8040 4.2" new "restconf HEAD. RFC 8040 4.2"
expectpart "$(curl $CURLOPTS -I -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+json" expectpart "$(curl $CURLOPTS -I -H "Accept: application/yang-data+json" $proto://$addr/restconf/data)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+json"
new "restconf empty rpc JSON" new "restconf empty rpc JSON"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":null} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":null} $proto://$addr/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content"
new "restconf empty rpc XML" new "restconf empty rpc XML"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+xml" -d '<input xmlns="urn:example:clixon"></input>' $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+xml" -d '<input xmlns="urn:example:clixon"></input>' $proto://$addr/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content"
new "restconf empty rpc, default media type should fail" new "restconf empty rpc, default media type should fail"
expectpart "$(curl $CURLOPTS -X POST -d {\"clixon-example:input\":null} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' expectpart "$(curl $CURLOPTS -X POST -d {\"clixon-example:input\":null} $proto://$addr/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type'
new "restconf empty rpc, default media type should fail (JSON)" new "restconf empty rpc, default media type should fail (JSON)"
expectpart "$(curl $CURLOPTS -X POST -H "Accept: application/yang-data+json" -d {\"clixon-example:input\":null} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' expectpart "$(curl $CURLOPTS -X POST -H "Accept: application/yang-data+json" -d {\"clixon-example:input\":null} $proto://$addr/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type'
new "restconf empty rpc with extra args (should fail)" new "restconf empty rpc with extra args (should fail)"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":{\"extra\":null}} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error","error-message":"Unrecognized parameter: extra in rpc: empty"}}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":{\"extra\":null}} $proto://$addr/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error","error-message":"Unrecognized parameter: extra in rpc: empty"}}}'
# Irritiating to get debugs on the terminal # Irritiating to get debugs on the terminal
#new "restconf debug rpc" #new "restconf debug rpc"
#expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-lib:input\":{\"level\":0}} $RCPROTO://localhost/restconf/operations/clixon-lib:debug)" 0 "HTTP/1.1 204 No Content" #expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-lib:input\":{\"level\":0}} $proto://$addr/restconf/operations/clixon-lib:debug)" 0 "HTTP/1.1 204 No Content"
new "restconf get empty config + state json" new "restconf get empty config + state json"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" '{"clixon-example:state":{"op":\["41","42","43"\]}}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" '{"clixon-example:state":{"op":\["41","42","43"\]}}'
new "restconf get empty config + state json with wrong module name" new "restconf get empty config + state json with wrong module name"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/badmodule:state)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"badmodule"},"error-severity":"error","error-message":"No such yang module prefix"}}}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/badmodule:state)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"badmodule"},"error-severity":"error","error-message":"No such yang module prefix"}}}'
#'HTTP/1.1 404 Not Found' #'HTTP/1.1 404 Not Found'
#'{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"No such yang module: badmodule"}}}' #'{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"No such yang module: badmodule"}}}'
new "restconf get empty config + state xml" new "restconf get empty config + state xml"
ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state) ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $proto://$addr/restconf/data/clixon-example:state)
expect='<state xmlns="urn:example:clixon"><op>41</op><op>42</op><op>43</op></state>' expect='<state xmlns="urn:example:clixon"><op>41</op><op>42</op><op>43</op></state>'
match=`echo $ret | grep --null -Eo "$expect"` match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then if [ -z "$match" ]; then
@ -170,11 +219,11 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi fi
new "restconf get data type json" new "restconf get data type json"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"}'
new "restconf get state operation" new "restconf get state operation"
# Cant get shell macros to work, inline matching from lib.sh # Cant get shell macros to work, inline matching from lib.sh
ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42) ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $proto://$addr/restconf/data/clixon-example:state/op=42)
expect='<op xmlns="urn:example:clixon">42</op>' expect='<op xmlns="urn:example:clixon">42</op>'
match=`echo $ret | grep --null -Eo "$expect"` match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then if [ -z "$match" ]; then
@ -182,11 +231,11 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi fi
new "restconf get state operation type json" new "restconf get state operation type json"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"}'
new "restconf get state operation type xml" new "restconf get state operation type xml"
# Cant get shell macros to work, inline matching from lib.sh # Cant get shell macros to work, inline matching from lib.sh
ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42) ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $proto://$addr/restconf/data/clixon-example:state/op=42)
expect='<op xmlns="urn:example:clixon">42</op>' expect='<op xmlns="urn:example:clixon">42</op>'
match=`echo $ret | grep --null -Eo "$expect"` match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then if [ -z "$match" ]; then
@ -194,88 +243,88 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi fi
new "restconf GET datastore" new "restconf GET datastore"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" '{"clixon-example:state":{"op":\["41","42","43"\]}}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" '{"clixon-example:state":{"op":\["41","42","43"\]}}'
# Exact match # Exact match
new "restconf Add subtree eth/0/0 to datastore using POST" new "restconf Add subtree eth/0/0 to datastore using POST"
expectpart "$(curl $CURLOPTS -X POST -H "Accept: application/yang-data+json" -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $RCPROTO://localhost/restconf/data)" 0 'HTTP/1.1 201 Created' "Location: $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces" expectpart "$(curl $CURLOPTS -X POST -H "Accept: application/yang-data+json" -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $proto://$addr/restconf/data)" 0 'HTTP/1.1 201 Created' "Location: $proto://$addr/restconf/data/ietf-interfaces:interfaces"
new "restconf Re-add subtree eth/0/0 which should give error" new "restconf Re-add subtree eth/0/0 which should give error"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $RCPROTO://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $proto://$addr/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}'
new "restconf Check interfaces eth/0/0 added" new "restconf Check interfaces eth/0/0 added"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}'
new "restconf delete interfaces" new "restconf delete interfaces"
expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 204 No Content" expectpart "$(curl $CURLOPTS -X DELETE $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 204 No Content"
new "restconf Check empty config" new "restconf Check empty config"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" "$state" expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" "$state"
new "restconf Add interfaces subtree eth/0/0 using POST" new "restconf Add interfaces subtree eth/0/0 using POST"
expectpart "$(curl $CURLOPTS -X POST $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}')" 0 "HTTP/1.1 201 Created" expectpart "$(curl $CURLOPTS -X POST $proto://$addr/restconf/data/ietf-interfaces:interfaces -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}')" 0 "HTTP/1.1 201 Created"
new "restconf Check eth/0/0 added config" new "restconf Check eth/0/0 added config"
expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}'
new "restconf Check eth/0/0 GET augmented state level 1" new "restconf Check eth/0/0 GET augmented state level 1"
expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}' expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}'
new "restconf Check eth/0/0 GET augmented state level 2" new "restconf Check eth/0/0 GET augmented state level 2"
expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0/clixon-example:my-status)" 0 'HTTP/1.1 200 OK' '{"clixon-example:my-status":{"int":42,"str":"foo"}}' expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0/clixon-example:my-status)" 0 'HTTP/1.1 200 OK' '{"clixon-example:my-status":{"int":42,"str":"foo"}}'
new "restconf Check eth/0/0 added state XXXXXXX" new "restconf Check eth/0/0 added state XXXXXXX"
expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 'HTTP/1.1 200 OK' '{"clixon-example:state":{"op":\["41","42","43"\]}}' expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/clixon-example:state)" 0 'HTTP/1.1 200 OK' '{"clixon-example:state":{"op":\["41","42","43"\]}}'
new "restconf Re-post eth/0/0 which should generate error" new "restconf Re-post eth/0/0 which should generate error"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} '
new "Add leaf description using POST" new "Add leaf description using POST"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:description":"The-first-interface"}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created" expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:description":"The-first-interface"}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created"
new "Add nothing using POST (expect fail)" new "Add nothing using POST (expect fail)"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body MUST contain exactly one instance of the expected data resource"}}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body MUST contain exactly one instance of the expected data resource"}}}'
new "restconf Check description added" new "restconf Check description added"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}'
new "restconf delete eth/0/0" new "restconf delete eth/0/0"
expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 204 No Content" expectpart "$(curl $CURLOPTS -X DELETE $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 204 No Content"
new "Check deleted eth/0/0" new "Check deleted eth/0/0"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "$state" expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data)" 0 "HTTP/1.1 200 OK" "$state"
new "restconf Re-Delete eth/0/0 using none should generate error" new "restconf Re-Delete eth/0/0 using none should generate error"
expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 409 Conflict" '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-missing","error-severity":"error","error-message":"Data does not exist; cannot delete resource"}}}' expectpart "$(curl $CURLOPTS -X DELETE $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 409 Conflict" '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-missing","error-severity":"error","error-message":"Data does not exist; cannot delete resource"}}}'
new "restconf Add subtree eth/0/0 using PUT" new "restconf Add subtree eth/0/0 using PUT"
expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created" expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created"
new "restconf get subtree" new "restconf get subtree"
expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}'
new "restconf rpc using POST json" new "restconf rpc using POST json"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":42}}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 "HTTP/1.1 200 OK" '{"clixon-example:output":{"x":"42","y":"42"}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":42}}' $proto://$addr/restconf/operations/clixon-example:example)" 0 "HTTP/1.1 200 OK" '{"clixon-example:output":{"x":"42","y":"42"}}'
if ! $YANG_UNKNOWN_ANYDATA ; then if ! $YANG_UNKNOWN_ANYDATA ; then
new "restconf rpc using POST json wrong" new "restconf rpc using POST json wrong"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error","error-message":"Failed to find YANG spec of XML node: wrongelement with parent: example in namespace: urn:example:clixon"}}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' $proto://$addr/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error","error-message":"Failed to find YANG spec of XML node: wrongelement with parent: example in namespace: urn:example:clixon"}}}'
fi fi
new "restconf rpc non-existing rpc without namespace" new "restconf rpc non-existing rpc without namespace"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $proto://$addr/restconf/operations/kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}'
new "restconf rpc non-existing rpc" new "restconf rpc non-existing rpc"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/clixon-example:kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $proto://$addr/restconf/operations/clixon-example:kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}'
new "restconf rpc missing name" new "restconf rpc missing name"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations)" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $proto://$addr/restconf/operations)" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}}'
new "restconf rpc missing input" new "restconf rpc missing input"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"restconf RPC does not have input statement"}}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $proto://$addr/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"restconf RPC does not have input statement"}}}'
new "restconf rpc using POST xml" new "restconf rpc using POST xml"
ret=$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":42}}' $RCPROTO://localhost/restconf/operations/clixon-example:example) ret=$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":42}}' $proto://$addr/restconf/operations/clixon-example:example)
expect='<output xmlns="urn:example:clixon"><x>42</x><y>42</y></output>' expect='<output xmlns="urn:example:clixon"><x>42</x><y>42</y></output>'
match=`echo $ret | grep --null -Eo "$expect"` match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then if [ -z "$match" ]; then
@ -283,10 +332,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi fi
new "restconf rpc using wrong prefix" new "restconf rpc using wrong prefix"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' $RCPROTO://localhost/restconf/operations/wrong:example)" 0 "HTTP/1.1 412 Precondition Failed" '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"yang module not found"}}}' expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' $proto://$addr/restconf/operations/wrong:example)" 0 "HTTP/1.1 412 Precondition Failed" '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"yang module not found"}}}'
new "restconf local client rpc using POST xml" new "restconf local client rpc using POST xml"
ret=$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":"example"}}' $RCPROTO://localhost/restconf/operations/clixon-example:client-rpc) ret=$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":"example"}}' $proto://$addr/restconf/operations/clixon-example:client-rpc)
expect='<output xmlns="urn:example:clixon"><x>example</x></output>' expect='<output xmlns="urn:example:clixon"><x>example</x></output>'
match=`echo $ret | grep --null -Eo "$expect"` match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then if [ -z "$match" ]; then
@ -294,10 +343,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi fi
new "restconf Add subtree without key (expected error)" new "restconf Add subtree without key (expected error)"
expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =interface, expected' expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =interface, expected'
new "restconf Add subtree with too many keys (expected error)" new "restconf Add subtree with too many keys (expected error)"
expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 "HTTP/1.1 400 Bad Request" '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}}' expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 "HTTP/1.1 400 Bad Request" '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}}'
if [ $RC -ne 0 ]; then if [ $RC -ne 0 ]; then
new "Kill restconf daemon" new "Kill restconf daemon"
@ -316,10 +365,25 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi fi
} }
new "Use local restconf config" # Go thru all combinations of IPv4/IPv6, http/https, local/backend config
testrun false protos="http"
if [ "${WITH_RESTCONF}" = "evhtp" ]; then
new "Get restconf config from backend" # http only relevant for evhtp (for fcgi: need nginx config)
testrun true protos="$protos https"
fi
for proto in $protos; do
for addr in 127.0.0.1 "\[::1\]"; do
configs="local"
if [ "${WITH_RESTCONF}" = "evhtp" ]; then
# backend config retrieval only implemented for evhtp
configs="$configs backend"
fi
echo "configs:$configs"
for config in $configs; do
new "restconf test: proto:$proto addr:$addr config:$config"
testrun $proto $addr $config
done
done
done
rm -rf $dir rm -rf $dir

View file

@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Restconf basic functionality # Restconf basic functionality
# Assume http server setup, such as nginx described in apps/restconf/README.md # Assume http server setup, such as nginx described in apps/restconf/README.md
# Systematic tests of restconf operations GET/POST/PUT/DELETE
# See also test_restconf.sh
# Magic line must be first in script (see README.md) # Magic line must be first in script (see README.md)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi

View file

@ -23,10 +23,12 @@ fyang=$dir/example.yang
cfg=$dir/conf.xml cfg=$dir/conf.xml
# Local for test here
certdir=$dir/certs certdir=$dir/certs
srvkey=$certdir/srv_key.pem srvkey=$certdir/srv_key.pem
srvcert=$certdir/srv_cert.pem srvcert=$certdir/srv_cert.pem
cakey=$certdir/ca_key.pem # needed? cakey=$certdir/ca_key.pem # needed?
cacert=$certdir/ca_cert.pem cacert=$certdir/ca_cert.pem
users="andy guest" # generate certs for some users in nacm.sh users="andy guest" # generate certs for some users in nacm.sh
@ -117,71 +119,12 @@ EOF
) )
if $genkeys; then if $genkeys; then
# Create certs
# 1. CA
cat<<EOF > $dir/ca.cnf
[ ca ]
default_ca = CA_default
[ CA_default ] # Server certs
serial = ca-serial . ./certs.sh
crl = ca-crl.pem
database = ca-database.txt
name_opt = CA_default
cert_opt = CA_default
default_crl_days = 9999
default_md = md5
[ req ] # create client certs
default_bits = 2048 for name in $users; do
days = 1
distinguished_name = req_distinguished_name
attributes = req_attributes
prompt = no
output_password = password
[ req_distinguished_name ]
C = SE
L = Stockholm
O = Clixon
OU = clixon
CN = ca
emailAddress = olof@hagsand.se
[ req_attributes ]
challengePassword = test
EOF
# Generate CA cert
openssl req -x509 -days 1 -config $dir/ca.cnf -keyout $cakey -out $cacert
cat<<EOF > $dir/srv.cnf
[req]
prompt = no
distinguished_name = dn
req_extensions = ext
[dn]
CN = www.clicon.org # localhost
emailAddress = olof@hagsand.se
O = Clixon
L = Stockholm
C = SE
[ext]
subjectAltName = DNS:clicon.org
EOF
# Generate server key
openssl genrsa -out $srvkey 2048
# Generate CSR (signing request)
openssl req -new -config $dir/srv.cnf -key $srvkey -out $certdir/srv_csr.pem
# Sign server cert by CA
openssl x509 -req -extfile $dir/srv.cnf -days 1 -passin "pass:password" -in $certdir/srv_csr.pem -CA $cacert -CAkey $cakey -CAcreateserial -out $srvcert
# create client certs
for name in $users; do
cat<<EOF > $dir/$name.cnf cat<<EOF > $dir/$name.cnf
[req] [req]
prompt = no prompt = no
@ -201,7 +144,7 @@ EOF
# Sign by CA # Sign by CA
openssl x509 -req -extfile $dir/$name.cnf -days 1 -passin "pass:password" -in $certdir/$name.csr -CA $cacert -CAkey $cakey -CAcreateserial -out $certdir/$name.crt openssl x509 -req -extfile $dir/$name.cnf -days 1 -passin "pass:password" -in $certdir/$name.csr -CA $cacert -CAkey $cakey -CAcreateserial -out $certdir/$name.crt
done done # client key
fi # genkeys fi # genkeys
@ -226,16 +169,18 @@ testrun()
cat <<EOF > $dir/startup_db cat <<EOF > $dir/startup_db
<config> <config>
<restconf xmlns="https://clicon.org/restconf"> <restconf xmlns="https://clicon.org/restconf">
<auth-type>$authtype</auth-type>
<ssl-enable>true</ssl-enable>
<server-cert-path>$srvcert</server-cert-path>
<server-key-path>$srvkey</server-key-path>
<server-ca-cert-path>$cacert</server-ca-cert-path>
<socket> <socket>
<namespace>default</namespace> <namespace>default</namespace>
<address>0.0.0.0</address> <address>0.0.0.0</address>
<port>$port</port> <port>$port</port>
<ssl>$ssl</ssl> <ssl>$ssl</ssl>
</socket> </socket>
<auth-type>$authtype</auth-type>
<server-cert-path>$srvcert</server-cert-path>
<server-key-path>$srvkey</server-key-path>
<server-ca-cert-path>$cacert</server-ca-cert-path>
</restconf> </restconf>
$RULES $RULES
</config> </config>
@ -259,6 +204,9 @@ EOF
start_backend -s startup -f $cfg start_backend -s startup -f $cfg
fi fi
new "wait for backend"
wait_backend
if [ $RC -ne 0 ]; then if [ $RC -ne 0 ]; then
new "kill old restconf daemon" new "kill old restconf daemon"
stop_restconf_pre stop_restconf_pre
@ -271,9 +219,6 @@ EOF
fi fi
fi fi
new "wait for backend"
wait_backend
new "wait for restconf" new "wait for restconf"
wait_restconf --key $certdir/andy.key --cert $certdir/andy.crt wait_restconf --key $certdir/andy.key --cert $certdir/andy.crt

View file

@ -57,10 +57,6 @@ module clixon-restconf {
description description
"PAM password auth"; "PAM password auth";
} }
enum none {
description
"No authentication, no security.";
}
} }
description description
"Enumeration of HTTP authorization types."; "Enumeration of HTTP authorization types.";
@ -91,31 +87,22 @@ module clixon-restconf {
presence "Enables RESTCONF"; presence "Enables RESTCONF";
description description
"HTTP daemon configuration."; "HTTP daemon configuration.";
list socket { leaf ssl-enable {
key "namespace address port"; description
leaf namespace { "Enable ssl server functionality.
type string; Setting to false means the following are invalid:
description "indicates a namespace for instance. On platforms where namespaces are not suppported, always 'default'"; - auth-type=client-certificate
} - socket entries with ssl=true
leaf address { Also, the following are not releveant: server-cert-path, server-key-path,
type inet:ip-address; server-ca-cert-path";
description "IP address to bind to";
}
leaf port {
type inet:port-number;
description "IP port to bind to";
}
leaf ssl {
type boolean; type boolean;
default true; default false;
description "Enable for HTTPS otherwise HTTP protocol";
} }
} leaf-list auth-type {
leaf auth-type {
type http-auth-type; type http-auth-type;
description description
"The authentication type. "The authentication type.
Note client-certificate applies only if socket has ssl enabled"; Note client-certificate applies only if ssl-enable is true and socket has ssl";
} }
leaf server-cert-path { leaf server-cert-path {
type string; type string;
@ -141,6 +128,26 @@ module clixon-restconf {
default "/etc/ssl/certs/clixon-ca_crt.pem"; default "/etc/ssl/certs/clixon-ca_crt.pem";
/* CLICON_SSL_CA_CERT */ /* CLICON_SSL_CA_CERT */
} }
list socket {
key "namespace address port";
leaf namespace {
type string;
description "indicates a namespace for instance. On platforms where namespaces are not suppported, always 'default'";
}
leaf address {
type inet:ip-address;
description "IP address to bind to";
}
leaf port {
type inet:port-number;
description "IP port to bind to";
}
leaf ssl {
type boolean;
default true;
description "Enable for HTTPS otherwise HTTP protocol";
}
}
} }
rpc restconf-control { rpc restconf-control {
input { input {