* 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
* 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 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.
@ -48,6 +49,7 @@ Users may have to change how they access the system
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:
* 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)

View file

@ -75,7 +75,7 @@
/*! 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))
* @retval s Socket file descriptor (see socket(2))
* @retval -1 Error

View file

@ -60,6 +60,9 @@
#include <sys/wait.h>
#include <assert.h>
#include <sys/stat.h> /* chmod */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* evhtp */
#include <evhtp/evhtp.h>
@ -88,8 +91,9 @@
/* clixon evhtp handle */
typedef struct {
evhtp_t *eh_htp;
struct event_base *eh_evbase;
evhtp_t **eh_htpvec; /* One per socket */
int eh_htplen; /* Number of sockets */
struct event_base *eh_evbase; /* Change to list */
evhtp_ssl_cfg_t *eh_ssl_config;
} cx_evhtp_handle;
@ -105,12 +109,16 @@ static void
evhtp_terminate(cx_evhtp_handle *eh)
{
evhtp_ssl_cfg_t *sc;
int i;
if (eh == NULL)
return;
if (eh->eh_htp){
evhtp_unbind_socket(eh->eh_htp);
evhtp_free(eh->eh_htp);
if (eh->eh_htpvec){
for (i=0; i<eh->eh_htplen; i++){
evhtp_unbind_socket(eh->eh_htpvec[i]);
evhtp_free(eh->eh_htpvec[i]);
}
free(eh->eh_htpvec);
}
if (eh->eh_evbase)
event_base_free(eh->eh_evbase);
@ -554,9 +562,9 @@ cx_get_ssl_server_certs(clicon_handle h,
* @retval -1 Error
*/
static int
cx_get_ssl_client_certs(clicon_handle h,
const char *server_ca_cert_path,
evhtp_ssl_cfg_t *ssl_config)
cx_get_ssl_client_ca_certs(clicon_handle h,
const char *server_ca_cert_path,
evhtp_ssl_cfg_t *ssl_config)
{
int retval = -1;
struct stat f_stat;
@ -643,7 +651,7 @@ static int
cx_verify_certs(int pre_verify,
evhtp_x509_store_ctx_t *store)
{
#ifdef NOTYET
#if 0 //def NOTYET
char buf[256];
X509 * err_cert;
int err;
@ -667,6 +675,88 @@ cx_verify_certs(int 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
* @param[in] argv0 command line
* @param[in] h Clicon handle
@ -701,31 +791,35 @@ usage(clicon_handle h,
exit(0);
}
/*! Main routine for libevhtp restconf
*/
/*! Phase 2 of start per-socket config
/*! Extract socket info from backend config
* @param[in] h Clicon handle
* @param[in] xs socket config
* @param[in] nsc Namespace context
* @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] ssl
*/
static int
cx_evhtp_socket(clicon_handle h,
cxobj *xs,
cvec *nsc,
char **namespace,
char **address,
uint16_t *port,
uint16_t *ssl)
cx_evhtp_socket_extract(clicon_handle h,
cxobj *xs,
cvec *nsc,
char **namespace,
char **address,
char **addrtype,
uint16_t *port,
uint16_t *ssl)
{
int retval = -1;
cxobj *x;
char *str = NULL;
char *reason = NULL;
int ret;
int retval = -1;
cxobj *x;
char *str = NULL;
char *reason = NULL;
int ret;
char *body;
cg_var *cv = NULL;
yang_stmt *y;
yang_stmt *ysub = NULL;
if ((x = xpath_first(xs, nsc, "namespace")) == NULL){
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");
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 &&
(str = xml_body(x)) != NULL){
if ((ret = parse_uint16(str, port, &reason)) < 0){
@ -767,7 +889,110 @@ cx_evhtp_socket(clicon_handle h,
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] xconfig XML config
* @param[in] nsc Namespace context
@ -780,30 +1005,29 @@ cx_evhtp_init(clicon_handle h,
cxobj *xconfig,
cvec *nsc,
cx_evhtp_handle *eh)
{
int retval = -1;
int auth_type_client_certificate = 0;
uint16_t port = 0;
cxobj *xrestconf;
cxobj **vec = NULL;
size_t veclen;
char *auth_type = NULL;
char *server_cert_path = NULL;
char *server_key_path = NULL;
char *server_ca_cert_path = NULL;
int retval = -1;
cxobj *xrestconf;
cxobj **vec = NULL;
size_t veclen;
char *server_cert_path = NULL;
char *server_key_path = NULL;
char *server_ca_cert_path = NULL;
char *auth_type = NULL;
int auth_type_client_certificate = 0;
//XXX char *client_cert_ca = NULL;
cxobj *x;
char *namespace = NULL;
char *address = NULL;
uint16_t use_ssl_server = 0;
cxobj *x;
int i;
int ssl_enable = 0;
/* Extract socket fields from xconfig */
if ((xrestconf = xpath_first(xconfig, nsc, "restconf")) == NULL){
clicon_err(OE_CFG, ENOENT, "restconf top symbol not found");
goto done;
}
/* 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? */
auth_type = xml_body(x);
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);
if ((x = xpath_first(xrestconf, nsc, "server-ca-cert-path")) != NULL)
server_ca_cert_path = xml_body(x);
/* get the list of socket config-data */
if (xpath_vec(xrestconf, nsc, "socket", &vec, &veclen) < 0)
goto done;
/* 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){
/* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */
if (ssl_enable){
/* Init evhtp ssl config struct */
if ((eh->eh_ssl_config = malloc(sizeof(evhtp_ssl_cfg_t))) == NULL){
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));
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,
server_key_path,
eh->eh_ssl_config) < 0)
/* Read server ssl files cert and key */
if (cx_get_ssl_server_certs(h, server_cert_path, server_key_path, eh->eh_ssl_config) < 0)
goto done;
/* If client auth get client CA cert */
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;
eh->eh_ssl_config->x509_verify_cb = cx_verify_certs; /* Is extra verification necessary? */
if (auth_type_client_certificate){
@ -873,52 +1064,15 @@ cx_evhtp_init(clicon_handle h,
}
// ssl_verify_mode = htp_sslutil_verify2opts(optarg);
}
assert(SSL_VERIFY_NONE == 0);
/* 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;
}
/* 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");
/* get the list of socket config-data */
if (xpath_vec(xrestconf, nsc, "socket", &vec, &veclen) < 0)
goto done;
for (i=0; i<veclen; i++){
if (cx_evhtp_socket(h, eh, ssl_enable, vec[i], nsc,
server_cert_path, server_key_path, server_ca_cert_path,
auth_type_client_certificate) < 0)
goto done;
}
retval = 0;
done:
@ -929,10 +1083,11 @@ cx_evhtp_init(clicon_handle h,
/*! Read config from backend */
int
restconf_config_backend(clicon_handle h,
int argc,
char **argv,
int drop_privileges)
restconf_config_backend(clicon_handle h,
cx_evhtp_handle *eh,
int argc,
char **argv,
int drop_privileges)
{
int retval = -1;
char *argv0 = argv[0];
@ -945,8 +1100,8 @@ restconf_config_backend(clicon_handle h,
size_t cligen_bufthreshold;
cvec *nsc = NULL;
cxobj *xconfig = NULL;
cxobj *xerr = NULL;
uint32_t id = 0; /* Session id, to poll backend up */
cx_evhtp_handle *eh = NULL;
/* Set default namespace according to CLICON_NAMESPACE_NETCONF_DEFAULT */
xml_nsctx_namespace_netconf_default(h);
@ -1052,13 +1207,16 @@ restconf_config_backend(clicon_handle h,
goto done;
if (clicon_rpc_get_config(h, NULL, "running", "/restconf", nsc, &xconfig) < 0)
goto done;
/* Initialize evhtp handle - fill it in cx_evhtp_init */
if ((eh = malloc(sizeof *eh)) == NULL){
clicon_err(OE_UNIX, errno, "malloc");
if ((xerr = xpath_first(xconfig, NULL, "/rpc-error")) != NULL){
clixon_netconf_error(xerr, "Get backend restconf config", NULL);
goto done;
}
memset(eh, 0, sizeof *eh);
_EVHTP_HANDLE = eh; /* global */
/* Init evhtp, common stuff */
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)
goto done;
/* Drop privileges after evhtp and server key/cert read */
@ -1069,7 +1227,6 @@ restconf_config_backend(clicon_handle h,
}
/* libevent main loop */
event_base_loop(eh->eh_evbase, 0); /* XXX: replace with clixon_event_loop() */
retval = 0;
done:
if (xconfig)
@ -1083,6 +1240,7 @@ restconf_config_backend(clicon_handle h,
/*! Read config locally */
int
restconf_config_local(clicon_handle h,
cx_evhtp_handle *eh,
int argc,
char **argv,
uint16_t port,
@ -1101,7 +1259,7 @@ restconf_config_local(clicon_handle h,
size_t cligen_bufthreshold;
char *restconf_ipv4_addr = NULL;
char *restconf_ipv6_addr = NULL;
cx_evhtp_handle *eh = NULL;
evhtp_t *htp;
/* port = defaultport unless explicitly set -P */
if (port == 0){
@ -1111,13 +1269,6 @@ restconf_config_local(clicon_handle h,
/* Set default namespace according to CLICON_NAMESPACE_NETCONF_DEFAULT */
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 */
if (use_ssl){
/* Init evhtp ssl config struct */
@ -1148,39 +1299,6 @@ restconf_config_local(clicon_handle h,
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;
}
/* 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
*/
@ -1193,12 +1311,47 @@ restconf_config_local(clicon_handle h,
}
if (restconf_ipv4_addr != NULL && strlen(restconf_ipv4_addr)){
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){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
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> */
port, /* port */
SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */
@ -1208,16 +1361,51 @@ restconf_config_local(clicon_handle h,
}
if (cb)
cbuf_free(cb);
if (cx_htp_add(eh, htp) < 0)
goto done;
}
/* 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;
/* 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){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
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> */
port, /* port */
SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */
@ -1227,6 +1415,8 @@ restconf_config_local(clicon_handle h,
}
if (cb)
cbuf_free(cb);
if (cx_htp_add(eh, htp) < 0)
goto done;
}
if (drop_privileges){
@ -1329,7 +1519,7 @@ restconf_config_local(clicon_handle h,
clicon_debug(1, "restconf_main_evhtp done");
return retval;
}
int
main(int argc,
char **argv)
@ -1491,10 +1681,16 @@ main(int argc,
/* port = defaultport unless explicitly set -P */
if (port == 0)
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){
/* Read config locally */
if (restconf_config_local(h, argc, argv,
if (restconf_config_local(h, eh, argc, argv,
port,
ssl_verify_clients,
use_ssl,
@ -1504,12 +1700,10 @@ main(int argc,
}
else {
/* 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;
}
event_base_loop(eh->eh_evbase, 0);
retval = 0;
done:
clicon_debug(1, "restconf_main_evhtp done");

View file

@ -31,6 +31,10 @@
#
# ***** 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@
srcdir = @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);
yang_stmt *yang_find_identity(yang_stmt *ys, char *identity);
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 yang_type_get(yang_stmt *ys, char **otype, yang_stmt **restype,
int *options, cvec **cvv,

View file

@ -1024,7 +1024,7 @@ xml_yang_validate_add(clicon_handle h,
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;
if (ret == 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
* @param[in] ys This is a type statement
* @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
*/
int
@ -741,7 +741,7 @@ cv_validate1(clicon_handle h,
/* Forward */
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
@ -767,6 +767,7 @@ ys_cv_validate_union_one(clicon_handle h,
char *restype;
enum cv_type cvtype;
cg_var *cvt=NULL;
yang_stmt *ysubt = NULL;
if ((regexps = cvec_new(0)) == NULL){
clicon_err(OE_UNIX, errno, "cvec_new");
@ -781,7 +782,7 @@ ys_cv_validate_union_one(clicon_handle h,
goto done;
restype = yrt?yang_argument_get(yrt):NULL;
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;
}
else {
@ -828,18 +829,23 @@ ys_cv_validate_union_one(clicon_handle h,
}
/*! 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[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 0 Validation not OK, malloced reason is returned. Free reason with free()
* @retval 1 Validation OK
*/
static int
ys_cv_validate_union(clicon_handle h,
yang_stmt *ys,
char **reason,
yang_stmt *yrestype,
char *type, /* orig type */
char *val)
yang_stmt *ys,
char **reason,
yang_stmt *yrestype,
char *type, /* orig type */
char *val,
yang_stmt **ysubp)
{
int retval = 1; /* valid */
yang_stmt *yt = NULL;
@ -859,8 +865,13 @@ ys_cv_validate_union(clicon_handle h,
reason1 = *reason;
*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;
}
}
done:
if (retval == 0 && reason1){
@ -877,6 +888,7 @@ ys_cv_validate_union(clicon_handle h,
* @param[in] h Clicon handle
* @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[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
* string describing reason why validation failed.
* @retval -1 Error (fatal), with errno set to indicate error
@ -889,6 +901,7 @@ int
ys_cv_validate(clicon_handle h,
cg_var *cv,
yang_stmt *ys,
yang_stmt **ysub,
char **reason)
{
int retval = -1;
@ -948,7 +961,7 @@ ys_cv_validate(clicon_handle h,
*/
if ((val = cv_string_get(cv)) == NULL)
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;
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,
regexps, yrestype, restype, reason)) < 0)
goto done;
if (ysub)
*ysub = ys;
}
done:
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
# Sanity nginx running on systemd platforms
# ./lib.sh: line 45: systemctl: command not found
if systemctl > /dev/null; then
nginxactive=$(systemctl show nginx |grep ActiveState=active)
if [ "${WITH_RESTCONF}" = "fcgi" ]; then
@ -273,8 +274,8 @@ wait_backend(){
# @see wait_restconf
start_restconf(){
# Start in background
if [ $RCPROTO = https ]; then
EXTRA="-s" # server certs
if [ $RCPROTO = https -a "${WITH_RESTCONF}" = "evhtp" ]; then
EXTRA="-s" # server certs ONLY evhtp
else
EXTRA=
fi

View file

@ -1,7 +1,18 @@
#!/usr/bin/env bash
# Restconf basic functionality
# also uri encoding using eth/0/0
# Assume http server setup, such as nginx described in apps/restconf/README.md
# Restconf basic functionality 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
# (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)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
@ -29,24 +40,62 @@ cat <<EOF > $cfg
<CLICON_BACKEND_PIDFILE>/usr/local/var/$APPNAME/$APPNAME.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
<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>
EOF
# This is a fixed 'state' implemented in routing_backend. It is assumed to be always there
state='{"clixon-example:state":{"op":\["41","42","43"\]}'
if [ ${RCPROTO} = "https" ]; then
ssl=true
port=443
else
ssl=false
port=80
fi
# For backend config, create 4 sockets, all combinations IPv4/IPv6 + http/https
RESTCONFCONFIG=$(cat <<EOF
<restconf xmlns="https://clicon.org/restconf">
<ssl-enable>true</ssl-enable>
<auth-type>password</auth-type>
<server-cert-path>$srvcert</server-cert-path>
<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()
{
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"
if [ $BE -ne 0 ]; then
new "kill old backend"
@ -63,10 +112,10 @@ testrun()
new "wait backend"
wait_backend
if $USEBACKEND; then
if [ $config = backend ] ; then # Create a backend config
# restconf backend 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>]]>]]>"
"^<rpc-reply $DEFAULTNS><ok/></rpc-reply>]]>]]>$"
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>]]>]]>$"
new "netconf commit"
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"
stop_restconf_pre
if $USEBACKEND; then
if [ $config = backend ] ; then # Add -b option
new "start restconf daemon -b"
start_restconf -f $cfg -b
else
@ -86,23 +135,23 @@ testrun()
fi
new "wait restconf"
wait_restconf
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)"
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)"
# 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
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)"
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"/>'
match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then
@ -110,10 +159,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi
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)"
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>"
match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then
@ -121,48 +170,48 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi
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"
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"
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
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"
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"
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"
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)"
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)"
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
#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"
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"
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'
#'{"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"
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>'
match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then
@ -170,11 +219,11 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi
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"
# 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>'
match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then
@ -182,11 +231,11 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi
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"
# 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>'
match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then
@ -194,88 +243,88 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi
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
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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)"
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"
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"
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"
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"
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"
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"
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"
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
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
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"
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"
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"
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"
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>'
match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then
@ -283,10 +332,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi
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"
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>'
match=`echo $ret | grep --null -Eo "$expect"`
if [ -z "$match" ]; then
@ -294,10 +343,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi
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)"
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
new "Kill restconf daemon"
@ -316,10 +365,25 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR
fi
}
new "Use local restconf config"
testrun false
new "Get restconf config from backend"
testrun true
# Go thru all combinations of IPv4/IPv6, http/https, local/backend config
protos="http"
if [ "${WITH_RESTCONF}" = "evhtp" ]; then
# http only relevant for evhtp (for fcgi: need nginx config)
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

View file

@ -1,6 +1,8 @@
#!/usr/bin/env bash
# Restconf basic functionality
# 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)
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
# Local for test here
certdir=$dir/certs
srvkey=$certdir/srv_key.pem
srvcert=$certdir/srv_cert.pem
cakey=$certdir/ca_key.pem # needed?
cacert=$certdir/ca_cert.pem
users="andy guest" # generate certs for some users in nacm.sh
@ -117,72 +119,13 @@ EOF
)
if $genkeys; then
# Create certs
# 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
# Server certs
. ./certs.sh
[ 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
# create client certs
for name in $users; do
cat<<EOF > $dir/$name.cnf
# create client certs
for name in $users; do
cat<<EOF > $dir/$name.cnf
[req]
prompt = no
distinguished_name = dn
@ -193,15 +136,15 @@ O = Clixon
L = Stockholm
C = SE
EOF
# Create client key
openssl genrsa -out "$certdir/$name.key" 2048
# Create client key
openssl genrsa -out "$certdir/$name.key" 2048
# Generate CSR (signing request)
openssl req -new -config $dir/$name.cnf -key $certdir/$name.key -out $certdir/$name.csr
# Generate CSR (signing request)
openssl req -new -config $dir/$name.cnf -key $certdir/$name.key -out $certdir/$name.csr
# 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
done
# 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
done # client key
fi # genkeys
@ -226,16 +169,18 @@ testrun()
cat <<EOF > $dir/startup_db
<config>
<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>
<namespace>default</namespace>
<address>0.0.0.0</address>
<port>$port</port>
<ssl>$ssl</ssl>
</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>
$RULES
</config>
@ -259,6 +204,9 @@ EOF
start_backend -s startup -f $cfg
fi
new "wait for backend"
wait_backend
if [ $RC -ne 0 ]; then
new "kill old restconf daemon"
stop_restconf_pre
@ -271,9 +219,6 @@ EOF
fi
fi
new "wait for backend"
wait_backend
new "wait for restconf"
wait_restconf --key $certdir/andy.key --cert $certdir/andy.crt

View file

@ -57,10 +57,6 @@ module clixon-restconf {
description
"PAM password auth";
}
enum none {
description
"No authentication, no security.";
}
}
description
"Enumeration of HTTP authorization types.";
@ -91,31 +87,22 @@ module clixon-restconf {
presence "Enables RESTCONF";
description
"HTTP daemon configuration.";
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";
}
leaf ssl-enable {
description
"Enable ssl server functionality.
Setting to false means the following are invalid:
- auth-type=client-certificate
- socket entries with ssl=true
Also, the following are not releveant: server-cert-path, server-key-path,
server-ca-cert-path";
type boolean;
default false;
}
leaf auth-type {
leaf-list auth-type {
type http-auth-type;
description
"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 {
type string;
@ -141,6 +128,26 @@ module clixon-restconf {
default "/etc/ssl/certs/clixon-ca_crt.pem";
/* 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 {
input {