Added basic rfc8527 support

Added basic rfc8527 support, but without support for:
 * ds:operational
 * with-origin
 * clixon/test/test_rfc8527_*.sh

The current level of rfc8527 support is intended to allow commit
operations on the 'candidate' datastore without rewriting the 'startup'
datastore.
This commit is contained in:
Jonathan Ben-Avraham 2020-11-07 22:11:35 +02:00
parent a1f54d71ac
commit cd45f277ef
9 changed files with 152 additions and 47 deletions

View file

@ -56,6 +56,18 @@ enum restconf_media{
}; };
typedef enum restconf_media restconf_media; typedef enum restconf_media restconf_media;
/* @See https://tools.ietf.org/html/rfc8342, ietf-datastores@2018-02-14.yang */
enum ietf_ds {
IETF_DS_NONE = 0,
IETF_DS_RUNNING,
IETF_DS_CANDIDATE,
IETF_DS_STARTUP,
IETF_DS_INTENDED,
IETF_DS_DYNAMIC,
IETF_DS_OPERATIONAL
};
typedef enum ietf_ds ietf_ds_t;
/* /*
* Prototypes * Prototypes
*/ */

View file

@ -230,6 +230,7 @@ match_list_keys(yang_stmt *y,
* PUT: If it does not, set op to create, otherwise replace * PUT: If it does not, set op to create, otherwise replace
* PATCH: If it does not, fail, otherwise replace/merge * PATCH: If it does not, fail, otherwise replace/merge
* @param[in] plain_patch fail if object does not exists AND merge (not replace) * @param[in] plain_patch fail if object does not exists AND merge (not replace)
* @param[in] ds 0 if "data" resource, 1 if rfc8527 "ds" resource
*/ */
static int static int
api_data_write(clicon_handle h, api_data_write(clicon_handle h,
@ -242,7 +243,8 @@ api_data_write(clicon_handle h,
int pretty, int pretty,
restconf_media media_in, restconf_media media_in,
restconf_media media_out, restconf_media media_out,
int plain_patch) int plain_patch,
ietf_ds_t ds)
{ {
int retval = -1; int retval = -1;
enum operation_type op; enum operation_type op;
@ -596,12 +598,12 @@ api_data_write(clicon_handle h,
NETCONF_BASE_NAMESPACE); /* bind nc to netconf namespace */ NETCONF_BASE_NAMESPACE); /* bind nc to netconf namespace */
cprintf(cbx, "<edit-config"); cprintf(cbx, "<edit-config");
/* RFC8040 Sec 1.4: /* RFC8040 Sec 1.4:
* If the NETCONF server supports :startup, the RESTCONF server MUST * If this is a "data" request and the NETCONF server supports :startup,
* automatically update the non-volatile startup configuration * the RESTCONF server MUST automatically update the non-volatile startup
* datastore, after the "running" datastore has been altered as a * configuration datastore, after the "running" datastore has been altered
* consequence of a RESTCONF edit operation. * as a consequence of a RESTCONF edit operation.
*/ */
if (if_feature(yspec, "ietf-netconf", "startup")) if ((IETF_DS_NONE == ds) && if_feature(yspec, "ietf-netconf", "startup"))
cprintf(cbx, " copystartup=\"true\""); cprintf(cbx, " copystartup=\"true\"");
cprintf(cbx, " autocommit=\"true\""); cprintf(cbx, " autocommit=\"true\"");
cprintf(cbx, "><target><candidate /></target>"); cprintf(cbx, "><target><candidate /></target>");
@ -695,13 +697,14 @@ api_data_put(clicon_handle h,
cvec *qvec, cvec *qvec,
char *data, char *data,
int pretty, int pretty,
restconf_media media_out) restconf_media media_out,
ietf_ds_t ds)
{ {
restconf_media media_in; restconf_media media_in;
media_in = restconf_content_type(h); media_in = restconf_content_type(h);
return api_data_write(h, req, api_path0, pcvec, pi, qvec, data, pretty, return api_data_write(h, req, api_path0, pcvec, pi, qvec, data, pretty,
media_in, media_out, 0); media_in, media_out, 0, ds);
} }
/*! Generic REST PATCH method for plain patch /*! Generic REST PATCH method for plain patch
@ -730,7 +733,8 @@ api_data_patch(clicon_handle h,
cvec *qvec, cvec *qvec,
char *data, char *data,
int pretty, int pretty,
restconf_media media_out) restconf_media media_out,
ietf_ds_t ds)
{ {
restconf_media media_in; restconf_media media_in;
int ret; int ret;
@ -740,7 +744,7 @@ api_data_patch(clicon_handle h,
case YANG_DATA_XML: case YANG_DATA_XML:
case YANG_DATA_JSON: /* plain patch */ case YANG_DATA_JSON: /* plain patch */
ret = api_data_write(h, req, api_path0, pcvec, pi, qvec, data, pretty, ret = api_data_write(h, req, api_path0, pcvec, pi, qvec, data, pretty,
media_in, media_out, 1); media_in, media_out, 1, ds);
break; break;
case YANG_PATCH_XML: case YANG_PATCH_XML:
case YANG_PATCH_JSON: /* RFC 8072 patch */ case YANG_PATCH_JSON: /* RFC 8072 patch */
@ -760,6 +764,7 @@ api_data_patch(clicon_handle h,
* @param[in] pi Offset, where path starts * @param[in] pi Offset, where path starts
* @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] pretty Set to 1 for pretty-printed xml/json output
* @param[in] media_out Output media * @param[in] media_out Output media
* @param[in] ds 0 if "data" resource, 1 if rfc8527 "ds" resource
* See RFC 8040 Sec 4.7 * See RFC 8040 Sec 4.7
* Example: * Example:
* curl -X DELETE http://127.0.0.1/restconf/data/interfaces/interface=eth0 * curl -X DELETE http://127.0.0.1/restconf/data/interfaces/interface=eth0
@ -771,7 +776,8 @@ api_data_delete(clicon_handle h,
char *api_path, char *api_path,
int pi, int pi,
int pretty, int pretty,
restconf_media media_out) restconf_media media_out,
ietf_ds_t ds)
{ {
int retval = -1; int retval = -1;
int i; int i;
@ -834,12 +840,12 @@ api_data_delete(clicon_handle h,
cprintf(cbx, "<edit-config"); cprintf(cbx, "<edit-config");
/* RFC8040 Sec 1.4: /* RFC8040 Sec 1.4:
* If the NETCONF server supports :startup, the RESTCONF server MUST * If this is a "data" request and the NETCONF server supports :startup,
* automatically update the non-volatile startup configuration * the RESTCONF server MUST automatically update the non-volatile startup
* datastore, after the "running" datastore has been altered as a * configuration datastore, after the "running" datastore has been altered
* consequence of a RESTCONF edit operation. * as a consequence of a RESTCONF edit operation.
*/ */
if (if_feature(yspec, "ietf-netconf", "startup")) if ((IETF_DS_NONE == ds) && if_feature(yspec, "ietf-netconf", "startup"))
cprintf(cbx, " copystartup=\"true\""); cprintf(cbx, " copystartup=\"true\"");
cprintf(cbx, " autocommit=\"true\""); cprintf(cbx, " autocommit=\"true\"");
cprintf(cbx, "><target><candidate /></target>"); cprintf(cbx, "><target><candidate /></target>");

View file

@ -46,14 +46,14 @@ int api_data_options(clicon_handle h, void *req);
int api_data_put(clicon_handle h, void *req, char *api_path, int api_data_put(clicon_handle h, void *req, char *api_path,
cvec *pcvec, int pi, cvec *pcvec, int pi,
cvec *qvec, char *data, cvec *qvec, char *data,
int pretty, restconf_media media_out); int pretty, restconf_media media_out, ietf_ds_t ds);
int api_data_patch(clicon_handle h, void *req, char *api_path, int api_data_patch(clicon_handle h, void *req, char *api_path,
cvec *pcvec, int pi, cvec *pcvec, int pi,
cvec *qvec, char *data, int pretty, cvec *qvec, char *data, int pretty,
restconf_media media_out); restconf_media media_out, ietf_ds_t ds);
int api_data_delete(clicon_handle h, void *req, char *api_path, int pi, int api_data_delete(clicon_handle h, void *req, char *api_path, int pi,
int pretty, restconf_media media_out); int pretty, restconf_media media_out, ietf_ds_t ds);
#endif /* _RESTCONF_METHODS_H_ */ #endif /* _RESTCONF_METHODS_H_ */

View file

@ -363,7 +363,8 @@ api_data_head(clicon_handle h,
int pi, int pi,
cvec *qvec, cvec *qvec,
int pretty, int pretty,
restconf_media media_out) restconf_media media_out,
ietf_ds_t ds)
{ {
return api_data_get2(h, req, api_path, pcvec, pi, qvec, pretty, media_out, 1); return api_data_get2(h, req, api_path, pcvec, pi, qvec, pretty, media_out, 1);
} }
@ -402,7 +403,8 @@ api_data_get(clicon_handle h,
int pi, int pi,
cvec *qvec, cvec *qvec,
int pretty, int pretty,
restconf_media media_out) restconf_media media_out,
ietf_ds_t ds)
{ {
return api_data_get2(h, req, api_path, pcvec, pi, qvec, pretty, media_out, 0); return api_data_get2(h, req, api_path, pcvec, pi, qvec, pretty, media_out, 0);
} }

View file

@ -42,9 +42,9 @@
* Prototypes * Prototypes
*/ */
int api_data_head(clicon_handle h, void *req, char *api_path, cvec *pcvec, int pi, int api_data_head(clicon_handle h, void *req, char *api_path, cvec *pcvec, int pi,
cvec *qvec, int pretty, restconf_media media_out); cvec *qvec, int pretty, restconf_media media_out, ietf_ds_t ds);
int api_data_get(clicon_handle h, void *req, char *api_path, cvec *pcvec, int pi, int api_data_get(clicon_handle h, void *req, char *api_path, cvec *pcvec, int pi,
cvec *qvec, int pretty, restconf_media media_out); cvec *qvec, int pretty, restconf_media media_out, ietf_ds_t ds);
int api_operations_get(clicon_handle h, void *req, int api_operations_get(clicon_handle h, void *req,
char *api_path, int pi, cvec *qvec, char *data, char *api_path, int pi, cvec *qvec, char *data,
int pretty, restconf_media media_out); int pretty, restconf_media media_out);

View file

@ -152,7 +152,8 @@ api_data_post(clicon_handle h,
cvec *qvec, cvec *qvec,
char *data, char *data,
int pretty, int pretty,
restconf_media media_out) restconf_media media_out,
ietf_ds_t ds)
{ {
int retval = -1; int retval = -1;
enum operation_type op = OP_CREATE; enum operation_type op = OP_CREATE;
@ -377,12 +378,12 @@ api_data_post(clicon_handle h,
cprintf(cbx, "<edit-config"); cprintf(cbx, "<edit-config");
/* RFC8040 Sec 1.4: /* RFC8040 Sec 1.4:
* If the NETCONF server supports :startup, the RESTCONF server MUST * If this is a "data" request and the NETCONF server supports :startup,
* automatically update the non-volatile startup configuration * the RESTCONF server MUST automatically update the non-volatile startup
* datastore, after the "running" datastore has been altered as a * configuration datastore, after the "running" datastore has been altered
* consequence of a RESTCONF edit operation. * as a consequence of a RESTCONF edit operation.
*/ */
if (if_feature(yspec, "ietf-netconf", "startup")) if ((IETF_DS_NONE == ds) && if_feature(yspec, "ietf-netconf", "startup"))
cprintf(cbx, " copystartup=\"true\""); cprintf(cbx, " copystartup=\"true\"");
cprintf(cbx, " autocommit=\"true\""); cprintf(cbx, " autocommit=\"true\"");
cprintf(cbx, "><target><candidate /></target>"); cprintf(cbx, "><target><candidate /></target>");

View file

@ -44,7 +44,7 @@
int api_data_post(clicon_handle h, void *req, char *api_path, int api_data_post(clicon_handle h, void *req, char *api_path,
int pi, cvec *qvec, char *data, int pi, cvec *qvec, char *data,
int pretty, int pretty,
restconf_media media_out); restconf_media media_out, ietf_ds_t ds);
int api_operations_post(clicon_handle h, void *req, char *api_path, int api_operations_post(clicon_handle h, void *req, char *api_path,
int pi, cvec *qvec, char *data, int pi, cvec *qvec, char *data,

View file

@ -153,7 +153,8 @@ api_root_restconf_exact(clicon_handle h,
goto done; goto done;
if (clixon_xml_parse_string("<restconf xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><data/>" if (clixon_xml_parse_string("<restconf xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><data/>"
"<operations/><yang-library-version>2016-06-21</yang-library-version></restconf>", "<operations/><yang-library-version>" IETF_YANG_LIBRARY_REVISION
"</yang-library-version></restconf>",
YB_MODULE, yspec, &xt, NULL) < 0) YB_MODULE, yspec, &xt, NULL) < 0)
goto done; goto done;
@ -187,6 +188,24 @@ api_root_restconf_exact(clicon_handle h,
return retval; return retval;
} }
/** A stub implementation of the operational state datastore. The full
* implementation is required by https://tools.ietf.org/html/rfc8527#section-3.1
*/
static int
api_operational_state(clicon_handle h,
void *req,
char *request_method,
int pretty,
restconf_media media_out)
{
clicon_debug(1, "%s request method:%s", __FUNCTION__, request_method);
/* We are not implementing this method at this time, 20201105 despite it
* being mandatory https://tools.ietf.org/html/rfc8527#section-3.1 */
return restconf_notimplemented(req);
}
/*! /*!
* See https://tools.ietf.org/html/rfc7895 * See https://tools.ietf.org/html/rfc7895
*/ */
@ -200,7 +219,6 @@ api_yang_library_version(clicon_handle h,
int retval = -1; int retval = -1;
cxobj *xt = NULL; cxobj *xt = NULL;
cbuf *cb = NULL; cbuf *cb = NULL;
char *ietf_yang_library_revision = "2016-06-21"; /* XXX */
clicon_debug(1, "%s", __FUNCTION__); clicon_debug(1, "%s", __FUNCTION__);
if (restconf_reply_header(req, "Content-Type", "%s", restconf_media_int2str(media_out)) < 0) if (restconf_reply_header(req, "Content-Type", "%s", restconf_media_int2str(media_out)) < 0)
@ -209,7 +227,7 @@ api_yang_library_version(clicon_handle h,
goto done; goto done;
if (clixon_xml_parse_va(YB_NONE, NULL, &xt, NULL, if (clixon_xml_parse_va(YB_NONE, NULL, &xt, NULL,
"<yang-library-version>%s</yang-library-version>", "<yang-library-version>%s</yang-library-version>",
ietf_yang_library_revision) < 0) IETF_YANG_LIBRARY_REVISION) < 0)
goto done; goto done;
if (xml_rootchild(xt, 0, &xt) < 0) if (xml_rootchild(xt, 0, &xt) < 0)
goto done; goto done;
@ -250,6 +268,7 @@ api_yang_library_version(clicon_handle h,
* @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] pretty Set to 1 for pretty-printed xml/json output
* @param[in] media_in Input media * @param[in] media_in Input media
* @param[in] media_out Output media * @param[in] media_out Output media
* @param[in] ds 0 if "data" resource, 1 if rfc8527 "ds" resource
*/ */
static int static int
api_data(clicon_handle h, api_data(clicon_handle h,
@ -260,28 +279,58 @@ api_data(clicon_handle h,
cvec *qvec, cvec *qvec,
char *data, char *data,
int pretty, int pretty,
restconf_media media_out) restconf_media media_out,
ietf_ds_t ds)
{ {
int retval = -1; int retval = -1;
int read_only = 0, dynamic = 0;
char *request_method; char *request_method;
clicon_debug(1, "%s", __FUNCTION__); clicon_debug(1, "%s", __FUNCTION__);
request_method = restconf_param_get(h, "REQUEST_METHOD"); request_method = restconf_param_get(h, "REQUEST_METHOD");
clicon_debug(1, "%s method:%s", __FUNCTION__, request_method); clicon_debug(1, "%s method:%s", __FUNCTION__, request_method);
/* https://tools.ietf.org/html/rfc8527#section-3.2 */
/* We assume that dynamic datastores are read only at this time 20201105 */
if (IETF_DS_DYNAMIC == ds)
dynamic = 1;
if ((IETF_DS_INTENDED == ds) || (IETF_DS_RUNNING == ds)
|| (IETF_DS_DYNAMIC == ds) || (IETF_DS_OPERATIONAL == ds)) {
read_only = 1;
}
if (strcmp(request_method, "OPTIONS")==0) if (strcmp(request_method, "OPTIONS")==0)
retval = api_data_options(h, req); retval = api_data_options(h, req);
else if (strcmp(request_method, "HEAD")==0) else if (strcmp(request_method, "HEAD")==0) {
retval = api_data_head(h, req, api_path, pcvec, pi, qvec, pretty, media_out); if (dynamic) {
else if (strcmp(request_method, "GET")==0) retval = restconf_method_notallowed(req, "GET,POST");
retval = api_data_get(h, req, api_path, pcvec, pi, qvec, pretty, media_out); }
else if (strcmp(request_method, "POST")==0) retval = api_data_head(h, req, api_path, pcvec, pi, qvec, pretty, media_out, ds);
retval = api_data_post(h, req, api_path, pi, qvec, data, pretty, media_out); }
else if (strcmp(request_method, "PUT")==0) else if (strcmp(request_method, "GET")==0) {
retval = api_data_put(h, req, api_path, pcvec, pi, qvec, data, pretty, media_out); retval = api_data_get(h, req, api_path, pcvec, pi, qvec, pretty, media_out, ds);
else if (strcmp(request_method, "PATCH")==0) }
retval = api_data_patch(h, req, api_path, pcvec, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "POST")==0) {
else if (strcmp(request_method, "DELETE")==0) retval = api_data_post(h, req, api_path, pi, qvec, data, pretty, media_out, ds);
retval = api_data_delete(h, req, api_path, pi, pretty, media_out); }
else if (strcmp(request_method, "PUT")==0) {
if (read_only) {
retval = restconf_method_notallowed(req, "GET,POST");
}
retval = api_data_put(h, req, api_path, pcvec, pi, qvec, data, pretty, media_out, ds);
}
else if (strcmp(request_method, "PATCH")==0) {
if (read_only) {
retval = restconf_method_notallowed(req, "GET,POST");
}
retval = api_data_patch(h, req, api_path, pcvec, pi, qvec, data, pretty, media_out, ds);
}
else if (strcmp(request_method, "DELETE")==0) {
if (read_only) {
retval = restconf_method_notallowed(req, "GET,POST");
}
retval = api_data_delete(h, req, api_path, pi, pretty, media_out, ds);
}
else else
retval = restconf_notfound(h, req); retval = restconf_notfound(h, req);
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
@ -441,7 +490,41 @@ api_root_restconf(clicon_handle h,
} }
else if (strcmp(api_resource, "data") == 0){ /* restconf, skip /api/data */ else if (strcmp(api_resource, "data") == 0){ /* restconf, skip /api/data */
if (api_data(h, req, path, pcvec, 2, qvec, indata, if (api_data(h, req, path, pcvec, 2, qvec, indata,
pretty, media_out) < 0) pretty, media_out, IETF_DS_NONE) < 0)
goto done;
}
else if (strcmp(api_resource, "ds") == 0) {
/* We should really be getting the supported datastore types from the
* application model, but at this time the datastore model of startup/
* running/cadidate is hardcoded into the clixon implementation. 20201104 */
ietf_ds_t ds = IETF_DS_NONE;
if (4 > pn) { /* Malformed request, no "ietf-datastores:<datastore>" component */
restconf_notfound(h, req);
goto done;
}
/* Assign ds; See https://tools.ietf.org/html/rfc8342#section-7 */
if (0 == strcmp(pvec[3], "ietf-datastores:running"))
ds = IETF_DS_RUNNING;
else if (0 == strcmp(pvec[3], "ietf-datastores:candidate"))
ds = IETF_DS_CANDIDATE;
else if (0 == strcmp(pvec[3], "ietf-datastores:startup"))
ds = IETF_DS_STARTUP;
else if (0 == strcmp(pvec[3], "ietf-datastores:operational")) {
/* See https://tools.ietf.org/html/rfc8527#section-3.1
* https://tools.ietf.org/html/rfc8342#section-5.3 */
if (0 > api_operational_state(h, req, request_method, pretty, media_out)) {
goto done;
}
goto ok;
}
else { /* Malformed request, unsupported datastore type */
restconf_notfound(h, req);
goto done;
}
/* ds is assigned at this point */
if (0 > api_data(h, req, path, pcvec, 3, qvec, indata, pretty, media_out, ds))
goto done; goto done;
} }
else if (strcmp(api_resource, "operations") == 0){ /* rpc */ else if (strcmp(api_resource, "operations") == 0){ /* rpc */

View file

@ -42,6 +42,7 @@
* Constants * Constants
*/ */
#define RESTCONF_API "restconf" #define RESTCONF_API "restconf"
#define IETF_YANG_LIBRARY_REVISION "2019-01-04"
/* RESTCONF enables deployments to specify where the RESTCONF API is /* RESTCONF enables deployments to specify where the RESTCONF API is
located. The client discovers this by getting the "/.well-known/host-meta" located. The client discovers this by getting the "/.well-known/host-meta"