* Identity/identityref mapped between XML and JSON
This commit is contained in:
parent
ccc95b2826
commit
70ebfa4d80
22 changed files with 779 additions and 133 deletions
|
|
@ -7,12 +7,13 @@
|
|||
* RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns:
|
||||
* `201 Created` for created resources
|
||||
* `204 No Content` for replaced resources.
|
||||
* identity/identityref mapped between XML and JSON
|
||||
* XML uses prefixes, JSON uses module-names (previously prefixes were used in both cases)
|
||||
* See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56)
|
||||
* Implementation detail: due to difference between RESTCONF and NETCONF semantics, a PUT first to make en internal netconf edit-config create operation; if that fails, a replace operation is tried.
|
||||
* HTTP `Location:` fields added in RESTCONF POST replies
|
||||
* HTTP `Cache-Control: no-cache` fields added in HTTP responses (RFC Section 5.5)
|
||||
* Restconf monitoring capabilities (RFC Section 9.1)
|
||||
*
|
||||
* Yang extensions support
|
||||
* New plugin callback: ca_extension
|
||||
* The main example explains how to implement a Yang extension in a backend plugin.
|
||||
|
|
@ -21,6 +22,8 @@
|
|||
* RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns:
|
||||
* `201 Created` for created resources
|
||||
* `204 No Content` for replaced resources.
|
||||
* RESTCONF identities has been changed to use module names instead of prefixes.
|
||||
* Eg, `curl -X POST -d '{"type":"ex:eth"}` --> `curl -X POST -d '{"type":"ietf-interfaces:eth"`}
|
||||
* JSON changes
|
||||
* Non-pretty-print output removed all extra spaces.
|
||||
* Example: `{"nacm-example:x": 42}` --> {"nacm-example:x":42}`
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ APPSRC = restconf_main.c
|
|||
APPSRC += restconf_methods.c
|
||||
APPSRC += restconf_methods_post.c
|
||||
APPSRC += restconf_methods_get.c
|
||||
APPSRC += restconf_methods_patch.c
|
||||
APPSRC += restconf_stream.c
|
||||
APPOBJ = $(APPSRC:.c=.o)
|
||||
|
||||
|
|
|
|||
|
|
@ -78,8 +78,9 @@
|
|||
/* restconf */
|
||||
#include "restconf_lib.h"
|
||||
#include "restconf_methods.h"
|
||||
#include "restconf_methods_post.h"
|
||||
#include "restconf_methods_get.h"
|
||||
#include "restconf_methods_post.h"
|
||||
#include "restconf_methods_patch.h"
|
||||
#include "restconf_stream.h"
|
||||
|
||||
/* Command line options to be passed to getopt(3) */
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ api_data_put(clicon_handle h,
|
|||
char *namespace0;
|
||||
char *dname;
|
||||
|
||||
clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"",
|
||||
clicon_debug(1, "%s api_path:\"%s\" data:\"%s\"",
|
||||
__FUNCTION__, api_path0, data);
|
||||
if ((yspec = clicon_dbspec_yang(h)) == NULL){
|
||||
clicon_err(OE_FATAL, 0, "No DB_SPEC");
|
||||
|
|
@ -336,6 +336,12 @@ api_data_put(clicon_handle h,
|
|||
}
|
||||
}
|
||||
else{
|
||||
/* Data here cannot cannot be Yang populated since it is loosely
|
||||
* hanging without top symbols.
|
||||
* And if it is not yang populated, it cant be translated properly
|
||||
* from JSON to XML.
|
||||
* Therefore, yang population is done later after addsub below
|
||||
*/
|
||||
if ((ret = json_parse_str(data, yspec, &xdata0, &xerr)) < 0){
|
||||
if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0)
|
||||
goto done;
|
||||
|
|
@ -487,6 +493,21 @@ api_data_put(clicon_handle h,
|
|||
xml_purge(xbot);
|
||||
if (xml_addsub(xparent, xdata) < 0)
|
||||
goto done;
|
||||
/* xbot is already populated, resolve yang for added xdata too */
|
||||
if (xml_apply0(xdata, CX_ELMNT, xml_spec_populate, yspec) < 0)
|
||||
goto done;
|
||||
if (!parse_xml){
|
||||
/* json2xml decode could not be done above in json_parse,
|
||||
need to be done here instead */
|
||||
if ((ret = json2xml_decode(xdata, &xerr)) < 0)
|
||||
goto done;
|
||||
if (ret == 0){
|
||||
if (api_return_err(h, r, xerr, pretty, use_xml, 0) < 0)
|
||||
goto done;
|
||||
goto ok;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we already have that default namespace, remove it in child */
|
||||
if ((xa = xml_find_type(xdata, NULL, "xmlns", CX_ATTR)) != NULL){
|
||||
if (xml2ns(xparent, NULL, &namespace0) < 0)
|
||||
|
|
@ -495,6 +516,7 @@ api_data_put(clicon_handle h,
|
|||
if (strcmp(namespace0, xml_value(xa))==0)
|
||||
xml_purge(xa);
|
||||
}
|
||||
|
||||
}
|
||||
/* Create text buffer for transfer to backend */
|
||||
if ((cbx = cbuf_new()) == NULL)
|
||||
|
|
@ -613,29 +635,6 @@ api_data_put(clicon_handle h,
|
|||
return retval;
|
||||
} /* api_data_put */
|
||||
|
||||
/*! Generic REST PATCH method
|
||||
* @param[in] h CLIXON handle
|
||||
* @param[in] r Fastcgi request handle
|
||||
* @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040)
|
||||
* @param[in] pcvec Vector of path ie DOCUMENT_URI element
|
||||
* @param[in] pi Offset, where to start pcvec
|
||||
* @param[in] qvec Vector of query string (QUERY_STRING)
|
||||
* @param[in] data Stream input data
|
||||
* Netconf: <edit-config> (nc:operation="merge")
|
||||
* See RFC8040 Sec 4.6
|
||||
*/
|
||||
int
|
||||
api_data_patch(clicon_handle h,
|
||||
FCGX_Request *r,
|
||||
char *api_path,
|
||||
cvec *pcvec,
|
||||
int pi,
|
||||
cvec *qvec,
|
||||
char *data)
|
||||
{
|
||||
notimplemented(r);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*! Generic REST DELETE method translated to edit-config
|
||||
* @param[in] h CLIXON handle
|
||||
|
|
|
|||
|
|
@ -46,9 +46,6 @@ int api_data_put(clicon_handle h, FCGX_Request *r, char *api_path,
|
|||
cvec *pcvec, int pi,
|
||||
cvec *qvec, char *data,
|
||||
int pretty, int use_xml, int parse_xml);
|
||||
int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path,
|
||||
cvec *pcvec, int pi,
|
||||
cvec *qvec, char *data);
|
||||
int api_data_delete(clicon_handle h, FCGX_Request *r, char *api_path, int pi,
|
||||
int pretty, int use_xml);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@
|
|||
#include <fcgiapp.h> /* Need to be after clixon_xml-h due to attribute format */
|
||||
|
||||
#include "restconf_lib.h"
|
||||
#include "restconf_methods.h"
|
||||
#include "restconf_methods_get.h"
|
||||
|
||||
/*! Generic GET (both HEAD and GET)
|
||||
|
|
@ -187,6 +186,17 @@ api_data_get2(clicon_handle h,
|
|||
goto done;
|
||||
}
|
||||
else{
|
||||
#if 0
|
||||
if (debug){
|
||||
cbuf *ccc=cbuf_new();
|
||||
if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0)
|
||||
goto done;
|
||||
clicon_debug(1, "%s xret: %s",
|
||||
__FUNCTION__, cbuf_get(ccc));
|
||||
cbuf_free(ccc);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (xml2json_cbuf(cbx, xret, pretty) < 0)
|
||||
goto done;
|
||||
}
|
||||
|
|
|
|||
151
apps/restconf/restconf_methods_patch.c
Normal file
151
apps/restconf/restconf_methods_patch.c
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright (C) 2009-2019 Olof Hagsand
|
||||
|
||||
This file is part of CLIXON.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Alternatively, the contents of this file may be used under the terms of
|
||||
the GNU General Public License Version 3 or later (the "GPL"),
|
||||
in which case the provisions of the GPL are applicable instead
|
||||
of those above. If you wish to allow use of your version of this file only
|
||||
under the terms of the GPL, and not to allow others to
|
||||
use your version of this file under the terms of Apache License version 2,
|
||||
indicate your decision by deleting the provisions above and replace them with
|
||||
the notice and other provisions required by the GPL. If you do not delete
|
||||
the provisions above, a recipient may use your version of this file under
|
||||
the terms of any one of the Apache License version 2 or the GPL.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
* See rfc8040
|
||||
|
||||
* sudo apt-get install libfcgi-dev
|
||||
* gcc -o fastcgi fastcgi.c -lfcgi
|
||||
|
||||
* sudo su -c "/www-data/clixon_restconf -D 1 f /usr/local/etc/example.xml " -s /bin/sh www-data
|
||||
|
||||
* This is the interface:
|
||||
* api/data/profile=<name>/metric=<name> PUT data:enable=<flag>
|
||||
* api/test
|
||||
+----------------------------+--------------------------------------+
|
||||
| 100 Continue | POST accepted, 201 should follow |
|
||||
| 200 OK | Success with response message-body |
|
||||
| 201 Created | POST to create a resource success |
|
||||
| 204 No Content | Success without response message- |
|
||||
| | body |
|
||||
| 304 Not Modified | Conditional operation not done |
|
||||
| 400 Bad Request | Invalid request message |
|
||||
| 401 Unauthorized | Client cannot be authenticated |
|
||||
| 403 Forbidden | Access to resource denied |
|
||||
| 404 Not Found | Resource target or resource node not |
|
||||
| | found |
|
||||
| 405 Method Not Allowed | Method not allowed for target |
|
||||
| | resource |
|
||||
| 409 Conflict | Resource or lock in use |
|
||||
| 412 Precondition Failed | Conditional method is false |
|
||||
| 413 Request Entity Too | too-big error |
|
||||
| Large | |
|
||||
| 414 Request-URI Too Large | too-big error |
|
||||
| 415 Unsupported Media Type | non RESTCONF media type |
|
||||
| 500 Internal Server Error | operation-failed |
|
||||
| 501 Not Implemented | unknown-operation |
|
||||
| 503 Service Unavailable | Recoverable server error |
|
||||
+----------------------------+--------------------------------------+
|
||||
Mapping netconf error-tag -> status code
|
||||
+-------------------------+-------------+
|
||||
| <error‑tag> | status code |
|
||||
+-------------------------+-------------+
|
||||
| in-use | 409 |
|
||||
| invalid-value | 400 |
|
||||
| too-big | 413 |
|
||||
| missing-attribute | 400 |
|
||||
| bad-attribute | 400 |
|
||||
| unknown-attribute | 400 |
|
||||
| bad-element | 400 |
|
||||
| unknown-element | 400 |
|
||||
| unknown-namespace | 400 |
|
||||
| access-denied | 403 |
|
||||
| lock-denied | 409 |
|
||||
| resource-denied | 409 |
|
||||
| rollback-failed | 500 |
|
||||
| data-exists | 409 |
|
||||
| data-missing | 409 |
|
||||
| operation-not-supported | 501 |
|
||||
| operation-failed | 500 |
|
||||
| partial-operation | 500 |
|
||||
| malformed-message | 400 |
|
||||
+-------------------------+-------------+
|
||||
|
||||
* "api-path" is "URI-encoded path expression" definition in RFC8040 3.5.3
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "clixon_config.h" /* generated by config & autoconf */
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <syslog.h>
|
||||
#include <fcntl.h>
|
||||
#include <assert.h>
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
#include <limits.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* cligen */
|
||||
#include <cligen/cligen.h>
|
||||
|
||||
/* clicon */
|
||||
#include <clixon/clixon.h>
|
||||
|
||||
#include <fcgiapp.h> /* Need to be after clixon_xml-h due to attribute format */
|
||||
|
||||
#include "restconf_lib.h"
|
||||
#include "restconf_methods_patch.h"
|
||||
|
||||
|
||||
/*! Generic REST PATCH method
|
||||
* @param[in] h CLIXON handle
|
||||
* @param[in] r Fastcgi request handle
|
||||
* @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040)
|
||||
* @param[in] pcvec Vector of path ie DOCUMENT_URI element
|
||||
* @param[in] pi Offset, where to start pcvec
|
||||
* @param[in] qvec Vector of query string (QUERY_STRING)
|
||||
* @param[in] data Stream input data
|
||||
* Netconf: <edit-config> (nc:operation="merge")
|
||||
* See RFC8040 Sec 4.6
|
||||
*/
|
||||
int
|
||||
api_data_patch(clicon_handle h,
|
||||
FCGX_Request *r,
|
||||
char *api_path,
|
||||
cvec *pcvec,
|
||||
int pi,
|
||||
cvec *qvec,
|
||||
char *data)
|
||||
{
|
||||
notimplemented(r);
|
||||
return 0;
|
||||
}
|
||||
|
||||
48
apps/restconf/restconf_methods_patch.h
Normal file
48
apps/restconf/restconf_methods_patch.h
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright (C) 2009-2019 Olof Hagsand
|
||||
|
||||
This file is part of CLIXON.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Alternatively, the contents of this file may be used under the terms of
|
||||
the GNU General Public License Version 3 or later (the "GPL"),
|
||||
in which case the provisions of the GPL are applicable instead
|
||||
of those above. If you wish to allow use of your version of this file only
|
||||
under the terms of the GPL, and not to allow others to
|
||||
use your version of this file under the terms of Apache License version 2,
|
||||
indicate your decision by deleting the provisions above and replace them with
|
||||
the notice and other provisions required by the GPL. If you do not delete
|
||||
the provisions above, a recipient may use your version of this file under
|
||||
the terms of any one of the Apache License version 2 or the GPL.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
|
||||
* Restconf method implementation
|
||||
*/
|
||||
|
||||
|
||||
#ifndef _RESTCONF_METHODS_PATCH_H_
|
||||
#define _RESTCONF_METHODS_PATCH_H_
|
||||
|
||||
/*
|
||||
* Prototypes
|
||||
*/
|
||||
int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path,
|
||||
cvec *pcvec, int pi,
|
||||
cvec *qvec, char *data);
|
||||
|
||||
#endif /* _RESTCONF_METHODS_PATCH_H_ */
|
||||
|
|
@ -62,7 +62,6 @@
|
|||
#include <fcgiapp.h> /* Need to be after clixon_xml.h due to attribute format */
|
||||
|
||||
#include "restconf_lib.h"
|
||||
#include "restconf_methods.h"
|
||||
#include "restconf_methods_post.h"
|
||||
|
||||
/*! Generic REST POST method
|
||||
|
|
@ -129,9 +128,10 @@ api_data_post(clicon_handle h,
|
|||
cxobj *xerr = NULL; /* malloced must be freed */
|
||||
cxobj *xe; /* dont free */
|
||||
char *username;
|
||||
int nullspec = 0;
|
||||
int ret;
|
||||
|
||||
clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"",
|
||||
clicon_debug(1, "%s api_path:\"%s\" data:\"%s\"",
|
||||
__FUNCTION__,
|
||||
api_path, data);
|
||||
if ((yspec = clicon_dbspec_yang(h)) == NULL){
|
||||
|
|
@ -178,6 +178,15 @@ api_data_post(clicon_handle h,
|
|||
}
|
||||
}
|
||||
else {
|
||||
/* Data here cannot cannot (always) be Yang populated since it is
|
||||
* loosely hanging without top symbols.
|
||||
* And if it is not yang populated, it cant be translated properly
|
||||
* from JSON to XML.
|
||||
* Therefore, yang population is done later after addsub below
|
||||
* Further complication is that if data is root resource, then it will
|
||||
* work, so I need to check below that it didnt.
|
||||
* THIS could be simplified.
|
||||
*/
|
||||
if ((ret = json_parse_str(data, yspec, &xdata0, &xerr)) < 0){
|
||||
if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0)
|
||||
goto done;
|
||||
|
|
@ -245,9 +254,24 @@ api_data_post(clicon_handle h,
|
|||
/* Replace xbot with x, ie bottom of api-path with data */
|
||||
if (xml_addsub(xbot, xdata) < 0)
|
||||
goto done;
|
||||
/* xbot is already populated, resolve yang for added xdata too */
|
||||
if (xml_spec_populate(xdata, yspec) < 0)
|
||||
/* xbot is already populated, resolve yang for added xdata too
|
||||
*/
|
||||
nullspec = (xml_spec(xdata) == NULL);
|
||||
if (xml_apply0(xdata, CX_ELMNT, xml_spec_populate, yspec) < 0)
|
||||
goto done;
|
||||
if (!parse_xml && nullspec){
|
||||
/* json2xml decode may not have been done above in json_parse,
|
||||
need to be done here instead
|
||||
UNLESS it is a root resource, then json-parse does right
|
||||
*/
|
||||
if ((ret = json2xml_decode(xdata, &xerr)) < 0)
|
||||
goto done;
|
||||
if (ret == 0){
|
||||
if (api_return_err(h, r, xerr, pretty, use_xml, 0) < 0)
|
||||
goto done;
|
||||
goto ok;
|
||||
}
|
||||
}
|
||||
|
||||
/* Create text buffer for transfer to backend */
|
||||
if ((cbx = cbuf_new()) == NULL){
|
||||
|
|
|
|||
|
|
@ -57,4 +57,4 @@
|
|||
* The easy way to do this is to always generate all prefix/namespace bindings
|
||||
* on the top-level for the modules involved in the netconf operation.
|
||||
*/
|
||||
#undef IDENTITYREF_KLUDGE
|
||||
#define IDENTITYREF_KLUDGE
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
/*
|
||||
* Prototypes
|
||||
*/
|
||||
int json2xml_decode(cxobj *x, cxobj **xerr);
|
||||
int xml2json_cbuf(cbuf *cb, cxobj *x, int pretty);
|
||||
int xml2json_cbuf_vec(cbuf *cb, cxobj **vec, size_t veclen, int pretty);
|
||||
int xml2json(FILE *f, cxobj *x, int pretty);
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ yang_stmt *ys_module(yang_stmt *ys);
|
|||
yang_stmt *ys_real_module(yang_stmt *ys);
|
||||
yang_stmt *ys_spec(yang_stmt *ys);
|
||||
yang_stmt *yang_find_module_by_prefix(yang_stmt *ys, char *prefix);
|
||||
yang_stmt *yang_find_module_by_prefix_yspec(yang_stmt *yspec, char *prefix);
|
||||
yang_stmt *yang_find_module_by_namespace(yang_stmt *yspec, char *namespace);
|
||||
yang_stmt *yang_find_module_by_name(yang_stmt *yspec, char *name);
|
||||
yang_stmt *yang_find(yang_stmt *yn, int keyword, const char *argument);
|
||||
|
|
@ -178,6 +179,7 @@ yang_stmt *yang_find_datanode(yang_stmt *yn, char *argument);
|
|||
yang_stmt *yang_find_schemanode(yang_stmt *yn, char *argument);
|
||||
char *yang_find_myprefix(yang_stmt *ys);
|
||||
char *yang_find_mynamespace(yang_stmt *ys);
|
||||
int yang_find_prefix_by_namespace(yang_stmt *ys, char *namespace, char **prefix);
|
||||
yang_stmt *yang_choice(yang_stmt *y);
|
||||
int yang_order(yang_stmt *y);
|
||||
int yang_print(FILE *f, yang_stmt *yn);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
* http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "clixon_config.h" /* generated by config & autoconf */
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
|
@ -64,6 +68,7 @@
|
|||
#include "clixon_xml.h"
|
||||
#include "clixon_xml_sort.h"
|
||||
#include "clixon_xml_map.h"
|
||||
#include "clixon_xml_nsctx.h" /* namespace context */
|
||||
#include "clixon_netconf_lib.h"
|
||||
#include "clixon_json.h"
|
||||
#include "clixon_json_parse.h"
|
||||
|
|
@ -224,7 +229,8 @@ array_eval(cxobj *xprev,
|
|||
}
|
||||
|
||||
/*! Escape a json string as well as decode xml cdata
|
||||
* And a
|
||||
* @param[out] cb cbuf (encoded)
|
||||
* @param[in] str string (unencoded)
|
||||
*/
|
||||
static int
|
||||
json_str_escape_cdata(cbuf *cb,
|
||||
|
|
@ -272,25 +278,287 @@ json_str_escape_cdata(cbuf *cb,
|
|||
return retval;
|
||||
}
|
||||
|
||||
/*! If set, quoute the json value with double quotes
|
||||
* @þaram[in] xb XML body object
|
||||
@ @retval 0 Value should not be quouted, XML value is int, boolean,..
|
||||
@ @retval 1 Value should be quouted, XML value is string,..
|
||||
/*! Decode types from JSON to XML identityrefs
|
||||
* Assume an xml tree where prefix:name have been split into "module":"name"
|
||||
* In other words, from JSON RFC7951 to XML namespace trees
|
||||
* @param[in] x XML tree. Must be yang populated.
|
||||
* @param[in] yspec Yang spec
|
||||
* @param[out] xerr Reason for invalid tree returned as netconf err msg or NULL
|
||||
* @retval 1 OK
|
||||
* @retval 0 Invalid, wrt namespace. xerr set
|
||||
* @retval -1 Error
|
||||
* @see RFC7951 Sec 4 and 6.8
|
||||
*/
|
||||
static int
|
||||
jsonvaluestr(cxobj *xb)
|
||||
json2xml_decode_identityref(cxobj *x,
|
||||
yang_stmt *y,
|
||||
cxobj **xerr)
|
||||
{
|
||||
int retval = 1;
|
||||
cxobj *xp;
|
||||
yang_stmt *yp;
|
||||
enum rfc_6020 keyword;
|
||||
int retval = -1;
|
||||
char *namespace;
|
||||
char *body;
|
||||
cxobj *xb;
|
||||
cxobj *xa;
|
||||
char *prefix = NULL;
|
||||
char *id = NULL;
|
||||
yang_stmt *ymod;
|
||||
yang_stmt *yspec;
|
||||
cvec *nsc = NULL;
|
||||
char *prefix2 = NULL;
|
||||
cbuf *cbv = NULL;
|
||||
|
||||
clicon_debug(1, "%s", __FUNCTION__);
|
||||
yspec = ys_spec(y);
|
||||
if ((xb = xml_body_get(x)) == NULL)
|
||||
goto ok;
|
||||
body = xml_value(xb);
|
||||
if (nodeid_split(body, &prefix, &id) < 0)
|
||||
goto done;
|
||||
/* prefix is a module name -> find module */
|
||||
if (prefix){
|
||||
if ((ymod = yang_find_module_by_name(yspec, prefix)) != NULL){
|
||||
namespace = yang_find_mynamespace(ymod);
|
||||
/* Is this namespace in the xml context?
|
||||
* (yes) use its prefix (unless it is NULL)
|
||||
* (no) insert a xmlns:<prefix> statement
|
||||
* Get the whole namespace context from x
|
||||
*/
|
||||
if (xml_nsctx_node(x, &nsc) < 0)
|
||||
goto done;
|
||||
clicon_debug(1, "%s prefix:%s body:%s namespace:%s",
|
||||
__FUNCTION__, prefix, body, namespace);
|
||||
if (!xml_nsctx_get_prefix(nsc, namespace, &prefix2)){
|
||||
/* (no) insert a xmlns:<prefix> statement
|
||||
* Get yang prefix from import statement of my mod */
|
||||
if (yang_find_prefix_by_namespace(y, namespace, &prefix2) == 0){
|
||||
#ifndef IDENTITYREF_KLUDGE
|
||||
/* Just get the prefix from the module's own namespace */
|
||||
if (netconf_unknown_namespace_xml(xerr, "application",
|
||||
namespace,
|
||||
"No local prefix corresponding to namespace") < 0)
|
||||
goto done;
|
||||
goto fail;
|
||||
#endif
|
||||
}
|
||||
/* if prefix2 is NULL here, we get the canonical prefix */
|
||||
if (prefix2 == NULL)
|
||||
prefix2 = yang_find_myprefix(ymod);
|
||||
/* Add "xmlns:prefix2=namespace" */
|
||||
if ((xa = xml_new(prefix2, x, NULL)) == NULL)
|
||||
goto done;
|
||||
xml_type_set(xa, CX_ATTR);
|
||||
if (xml_prefix_set(xa, "xmlns") < 0)
|
||||
goto done;
|
||||
if (xml_value_set(xa, namespace) < 0)
|
||||
goto done;
|
||||
}
|
||||
/* Here prefix2 is valid and can be NULL
|
||||
Change body prefix to prefix2:id */
|
||||
if ((cbv = cbuf_new()) == NULL){
|
||||
clicon_err(OE_XML, errno, "cbuf_new");
|
||||
goto done;
|
||||
}
|
||||
if (prefix2)
|
||||
cprintf(cbv, "%s:%s", prefix2, id);
|
||||
else
|
||||
cprintf(cbv, "%s", id);
|
||||
|
||||
if (xml_value_set(xb, cbuf_get(cbv)) < 0)
|
||||
goto done;
|
||||
}
|
||||
else{
|
||||
if (netconf_unknown_namespace_xml(xerr, "application",
|
||||
prefix,
|
||||
"No module corresponding to prefix") < 0)
|
||||
goto done;
|
||||
goto fail;
|
||||
}
|
||||
} /* prefix */
|
||||
ok:
|
||||
retval = 1;
|
||||
done:
|
||||
if (prefix)
|
||||
free(prefix);
|
||||
if (id)
|
||||
free(id);
|
||||
if (nsc)
|
||||
xml_nsctx_free(nsc);
|
||||
if (cbv)
|
||||
cbuf_free(cbv);
|
||||
return retval;
|
||||
fail:
|
||||
retval = 0;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/*! Decode leaf/leaf_list types from JSON to XML after parsing and yang
|
||||
*
|
||||
* Assume an xml tree where prefix:name have been split into "module":"name"
|
||||
* In other words, from JSON RFC7951 to XML namespace trees
|
||||
*
|
||||
* @param[in] x XML tree. Must be yang populated. After json parsing
|
||||
* @param[in] yspec Yang spec
|
||||
* @param[out] xerr Reason for invalid tree returned as netconf err msg or NULL
|
||||
* @retval 1 OK
|
||||
* @retval 0 Invalid, wrt namespace. xerr set
|
||||
* @retval -1 Error
|
||||
* @see RFC7951 Sec 4 and 6.8
|
||||
*/
|
||||
int
|
||||
json2xml_decode(cxobj *x,
|
||||
cxobj **xerr)
|
||||
{
|
||||
int retval = -1;
|
||||
yang_stmt *y;
|
||||
enum rfc_6020 keyword;
|
||||
cxobj *xc;
|
||||
int ret;
|
||||
yang_stmt *ytype;
|
||||
|
||||
if ((y = xml_spec(x)) != NULL){
|
||||
keyword = yang_keyword_get(y);
|
||||
if (keyword == Y_LEAF || keyword == Y_LEAF_LIST){
|
||||
if (yang_type_get(y, NULL, &ytype, NULL, NULL, NULL, NULL, NULL) < 0)
|
||||
goto done;
|
||||
if (ytype)
|
||||
if (strcmp(yang_argument_get(ytype),"identityref")==0){
|
||||
if ((ret = json2xml_decode_identityref(x, y, xerr)) < 0)
|
||||
goto done;
|
||||
if (ret == 0)
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
xc = NULL;
|
||||
while ((xc = xml_child_each(x, xc, CX_ELMNT)) != NULL){
|
||||
if ((ret = json2xml_decode(xc, xerr)) < 0)
|
||||
goto done;
|
||||
if (ret == 0)
|
||||
goto fail;
|
||||
}
|
||||
retval = 1;
|
||||
done:
|
||||
return retval;
|
||||
fail:
|
||||
retval = 0;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/*! Encode leaf/leaf_list identityref type from XML to JSON
|
||||
* @param[in] x XML body node
|
||||
* @param[in] body body string
|
||||
* @param[in] ys Yang spec of parent
|
||||
* @param[out] cb Encoded string
|
||||
*/
|
||||
static int
|
||||
xml2json_encode_identityref(cxobj *xb,
|
||||
char *body,
|
||||
yang_stmt *yp,
|
||||
cbuf *cb)
|
||||
{
|
||||
int retval = -1;
|
||||
char *prefix = NULL;
|
||||
char *id = NULL;
|
||||
char *namespace = NULL;
|
||||
yang_stmt *ymod;
|
||||
yang_stmt *yspec;
|
||||
yang_stmt *my_ymod;
|
||||
|
||||
my_ymod = ys_module(yp);
|
||||
yspec = ys_spec(yp);
|
||||
if (nodeid_split(body, &prefix, &id) < 0)
|
||||
goto done;
|
||||
/* prefix is xml local -> get namespace */
|
||||
if (xml2ns(xb, prefix, &namespace) < 0)
|
||||
goto done;
|
||||
/* We got the namespace, now get the module */
|
||||
// clicon_debug(1, "%s body:%s prefix:%s namespace:%s", __FUNCTION__, body, prefix, namespace);
|
||||
#ifdef IDENTITYREF_KLUDGE
|
||||
if (namespace == NULL){
|
||||
/* If we dont find namespace here, we assume it is because of a missing
|
||||
* xmlns that should be there, as a kludge we search for its (own)
|
||||
* prefix in mymodule.
|
||||
*/
|
||||
if ((ymod = yang_find_module_by_prefix_yspec(yspec, prefix)) != NULL)
|
||||
cprintf(cb, "%s:%s", yang_argument_get(ymod), id);
|
||||
else
|
||||
cprintf(cb, "%s", id);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
if ((ymod = yang_find_module_by_namespace(yspec, namespace)) != NULL){
|
||||
|
||||
if (ymod == my_ymod)
|
||||
cprintf(cb, "%s", id);
|
||||
else{
|
||||
cprintf(cb, "%s:%s", yang_argument_get(ymod), id);
|
||||
}
|
||||
}
|
||||
else
|
||||
cprintf(cb, "%s", id);
|
||||
}
|
||||
retval = 0;
|
||||
done:
|
||||
if (prefix)
|
||||
free(prefix);
|
||||
if (id)
|
||||
free(id);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Encode leaf/leaf_list types from XML to JSON
|
||||
* @param[in] x XML body
|
||||
* @param[in] ys Yang spec of parent
|
||||
* @param[out] cb0 Encoded string
|
||||
*/
|
||||
static int
|
||||
xml2json_encode(cxobj *xb,
|
||||
cbuf *cb0)
|
||||
{
|
||||
int retval = -1;
|
||||
cxobj *xp;
|
||||
yang_stmt *yp;
|
||||
enum rfc_6020 keyword;
|
||||
yang_stmt *ytype;
|
||||
char *restype; /* resolved type */
|
||||
char *origtype=NULL; /* original type */
|
||||
char *body;
|
||||
enum cv_type cvtype;
|
||||
int quote = 1; /* Quote value w string: "val" */
|
||||
cbuf *cb = NULL; /* the variable itself */
|
||||
|
||||
if ((cb = cbuf_new()) ==NULL){
|
||||
clicon_err(OE_XML, errno, "cbuf_new");
|
||||
goto done;
|
||||
}
|
||||
body = xml_value(xb);
|
||||
if ((xp = xml_parent(xb)) == NULL ||
|
||||
(yp = xml_spec(xp)) == NULL)
|
||||
goto done; /* unknown */
|
||||
(yp = xml_spec(xp)) == NULL){
|
||||
cprintf(cb, "%s", body);
|
||||
goto ok; /* unknown */
|
||||
}
|
||||
keyword = yang_keyword_get(yp);
|
||||
if ((keyword == Y_LEAF || keyword == Y_LEAF_LIST))
|
||||
switch (yang_type2cv(yp)){
|
||||
switch (keyword){
|
||||
case Y_LEAF:
|
||||
case Y_LEAF_LIST:
|
||||
if (yang_type_get(yp, &origtype, &ytype, NULL, NULL, NULL, NULL, NULL) < 0)
|
||||
goto done;
|
||||
restype = ytype?yang_argument_get(ytype):NULL;
|
||||
cvtype = yang_type2cv(yp);
|
||||
switch (cvtype){
|
||||
case CGV_STRING:
|
||||
if (ytype){
|
||||
if (strcmp(restype, "identityref")==0){
|
||||
if (xml2json_encode_identityref(xb, body, yp, cb) < 0)
|
||||
goto done;
|
||||
}
|
||||
else
|
||||
cprintf(cb, "%s", body);
|
||||
}
|
||||
else
|
||||
cprintf(cb, "%s", body);
|
||||
break;
|
||||
case CGV_INT8:
|
||||
case CGV_INT16:
|
||||
case CGV_INT32:
|
||||
|
|
@ -301,18 +569,42 @@ jsonvaluestr(cxobj *xb)
|
|||
case CGV_UINT64:
|
||||
case CGV_DEC64:
|
||||
case CGV_BOOL:
|
||||
retval = 0;
|
||||
cprintf(cb, "%s", body);
|
||||
quote = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
cprintf(cb, "%s", body);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
cprintf(cb, "%s", body);
|
||||
break;
|
||||
}
|
||||
ok:
|
||||
/* write into original cb0
|
||||
* includign quoting and encoding
|
||||
*/
|
||||
if (quote){
|
||||
cprintf(cb0, "\"");
|
||||
json_str_escape_cdata(cb0, cbuf_get(cb));
|
||||
}
|
||||
else
|
||||
cprintf(cb0, "%s", cbuf_get(cb));
|
||||
if (quote)
|
||||
cprintf(cb0, "\"");
|
||||
retval = 0;
|
||||
done:
|
||||
if (cb)
|
||||
cbuf_free(cb);
|
||||
if (origtype)
|
||||
free(origtype);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Do the actual work of translating XML to JSON
|
||||
* @param[out] cb Cligen text buffer containing json on exit
|
||||
* @param[in] x XML tree structure containing XML to translate
|
||||
* @param[in] yp Parent yang spec needed for body
|
||||
* @param[in] arraytype Does x occur in a array (of its parent) and how?
|
||||
* @param[in] level Indentation level
|
||||
* @param[in] pretty Pretty-print output (2 means debug)
|
||||
|
|
@ -381,17 +673,10 @@ xml2json1_cbuf(cbuf *cb,
|
|||
arraytype2str(arraytype),
|
||||
childtype2str(childt));
|
||||
switch(arraytype){
|
||||
case BODY_ARRAY:{
|
||||
if (jsonvaluestr(x)) { /* Only print quotation if string-type */
|
||||
cprintf(cb, "\"");
|
||||
if (json_str_escape_cdata(cb, xml_value(x)) < 0)
|
||||
goto done;
|
||||
cprintf(cb, "\"");
|
||||
}
|
||||
else /* No quotation marks */
|
||||
cprintf(cb, "%s", xml_value(x));
|
||||
case BODY_ARRAY: /* Only place in fn where body is printed */
|
||||
if (xml2json_encode(x, cb) < 0)
|
||||
goto done;
|
||||
break;
|
||||
}
|
||||
case NO_ARRAY:
|
||||
if (!flat){
|
||||
cprintf(cb, "%*s\"", pretty?(level*JSON_INDENT):0, "");
|
||||
|
|
@ -739,7 +1024,7 @@ xml2json_vec(FILE *f,
|
|||
|
||||
/*! Translate from JSON module:name to XML default ns: xmlns="uri" recursively
|
||||
* Assume an xml tree where prefix:name have been split into "module":"name"
|
||||
* In other words, from JSON RFC7951 to XML namespace trees
|
||||
* In other words, from JSON to XML namespace trees
|
||||
*
|
||||
* @param[in] yspec Yang spec
|
||||
* @param[in,out] x XML tree. Translate it in-line
|
||||
|
|
@ -749,8 +1034,9 @@ xml2json_vec(FILE *f,
|
|||
* @retval -1 Error
|
||||
* @note the opposite - xml2ns is made inline in xml2json1_cbuf
|
||||
* Example: <top><module:input> --> <top><input xmlns="">
|
||||
* @see RFC7951 Sec 4
|
||||
*/
|
||||
int
|
||||
static int
|
||||
json_xmlns_translate(yang_stmt *yspec,
|
||||
cxobj *x,
|
||||
cxobj **xerr)
|
||||
|
|
@ -809,17 +1095,17 @@ json_xmlns_translate(yang_stmt *yspec,
|
|||
* @param[in] yspec If set, also do yang validation
|
||||
* @param[in] name Log string, typically filename
|
||||
* @param[out] xt XML top of tree typically w/o children on entry (but created)
|
||||
* @param[out] xerr Reason for invalid returned as netconf err msg
|
||||
* @param[out] xerr Reason for invalid returned as netconf err msg
|
||||
*
|
||||
* @see _xml_parse for XML variant
|
||||
* @retval 1 OK and valid
|
||||
* @retval 0 Invalid (only if yang spec)
|
||||
* @retval -1 Error with clicon_err called
|
||||
* @retval 1 OK and valid
|
||||
* @retval 0 Invalid (only if yang spec)
|
||||
* @retval -1 Error with clicon_err called
|
||||
* @see http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
|
||||
* @see RFC 7951
|
||||
*/
|
||||
static int
|
||||
json_parse(char *str,
|
||||
json_parse(char *str,
|
||||
yang_stmt *yspec,
|
||||
const char *name,
|
||||
cxobj *xt,
|
||||
|
|
@ -855,6 +1141,12 @@ json_parse(char *str,
|
|||
goto done;
|
||||
if (xml_apply0(xt, CX_ELMNT, xml_sort, NULL) < 0)
|
||||
goto done;
|
||||
/* Now find leafs with identityrefs (+transitive) and translate
|
||||
* prefixes in values to XML namespaces */
|
||||
if ((ret = json2xml_decode(xt, xerr)) < 0)
|
||||
goto done;
|
||||
if (ret == 0) /* XXX necessary? */
|
||||
goto fail;
|
||||
}
|
||||
retval = 1;
|
||||
done:
|
||||
|
|
@ -871,7 +1163,7 @@ json_parse(char *str,
|
|||
*
|
||||
* @param[in] str String containing JSON
|
||||
* @param[in] yspec Yang specification, or NULL
|
||||
* @param[in,out] xt On success a top of XML parse tree is created with name 'top'
|
||||
* @param[in,out] xt Top object, if not exists, on success it is created with name 'top'
|
||||
* @param[out] xerr Reason for invalid returned as netconf err msg
|
||||
*
|
||||
* @code
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ nscache_set(cxobj *x,
|
|||
* @param[out] namespace URI namespace (or NULL). Note pointer into xml tree
|
||||
* @retval 0 OK
|
||||
* @retval -1 Error
|
||||
* @see xmlns_check XXX can these be merged?
|
||||
* @see xmlns_check
|
||||
* @see xmlns_set cache is set
|
||||
* @note, this function uses a cache.
|
||||
*/
|
||||
|
|
@ -307,8 +307,12 @@ xml2ns(cxobj *x,
|
|||
}
|
||||
/* If no parent, return default namespace if defined */
|
||||
#ifdef USE_NETCONF_NS_AS_DEFAULT
|
||||
else
|
||||
ns = NETCONF_BASE_NAMESPACE;
|
||||
else{
|
||||
if (prefix == NULL)
|
||||
ns = NETCONF_BASE_NAMESPACE;
|
||||
else
|
||||
ns = NULL;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
/* Set default namespace cache (since code is at this point,
|
||||
|
|
|
|||
|
|
@ -332,24 +332,6 @@ validate_leafref(cxobj *xt,
|
|||
goto done;
|
||||
}
|
||||
|
||||
/* Get module from its own prefix
|
||||
* This is really not a valid usecase, a kludge for the identityref derived
|
||||
* list workaround (IDENTITYREF_KLUDGE)
|
||||
*/
|
||||
static yang_stmt *
|
||||
yang_find_module_by_prefix_yspec(yang_stmt *yspec,
|
||||
char *prefix)
|
||||
{
|
||||
yang_stmt *ymod = NULL;
|
||||
yang_stmt *yprefix;
|
||||
|
||||
while ((ymod = yn_each(yspec, ymod)) != NULL)
|
||||
if (ymod->ys_keyword == Y_MODULE &&
|
||||
(yprefix = yang_find(ymod, Y_PREFIX, NULL)) != NULL &&
|
||||
strcmp(yang_argument_get(yprefix), prefix) == 0)
|
||||
return ymod;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*! Validate xml node of type identityref, ensure value is a defined identity
|
||||
* Check if a given node has value derived from base identity. This is
|
||||
|
|
@ -2340,6 +2322,7 @@ xml_spec_populate_rpc(clicon_handle h,
|
|||
* xml_apply(xc, CX_ELMNT, xml_spec_populate, yspec)
|
||||
* @endcode
|
||||
*/
|
||||
#undef DEBUG
|
||||
int
|
||||
xml_spec_populate(cxobj *x,
|
||||
void *arg)
|
||||
|
|
@ -2355,17 +2338,36 @@ xml_spec_populate(cxobj *x,
|
|||
yspec = (yang_stmt*)arg;
|
||||
xp = xml_parent(x);
|
||||
name = xml_name(x);
|
||||
#ifdef DEBUG
|
||||
clicon_debug(1, "%s name:%s", __FUNCTION__, name);
|
||||
#endif
|
||||
if (xp && (yparent = xml_spec(xp)) != NULL)
|
||||
y = yang_find_datanode(yparent, name);
|
||||
else if (yspec){
|
||||
if (ys_module_by_xml(yspec, x, &ymod) < 0)
|
||||
goto done;
|
||||
/* ymod is "real" module, name may belong to included submodule */
|
||||
if (ymod != NULL)
|
||||
if (ymod != NULL){
|
||||
#ifdef DEBUG
|
||||
clicon_debug(1, "%s %s mod:%s", __FUNCTION__, name, yang_argument_get(ymod));
|
||||
#endif
|
||||
y = yang_find_schemanode(ymod, name);
|
||||
}
|
||||
#ifdef DEBUG
|
||||
else
|
||||
clicon_debug(1, "%s %s mod:NULL", __FUNCTION__, name);
|
||||
#endif
|
||||
}
|
||||
if (y)
|
||||
if (y) {
|
||||
#ifdef DEBUG
|
||||
clicon_debug(1, "%s y:%s", __FUNCTION__, yang_argument_get(y));
|
||||
#endif
|
||||
xml_spec_set(x, y);
|
||||
}
|
||||
#ifdef DEBUG
|
||||
else
|
||||
clicon_debug(1, "%s y:NULL", __FUNCTION__);
|
||||
#endif
|
||||
retval = 0;
|
||||
done:
|
||||
return retval;
|
||||
|
|
|
|||
|
|
@ -795,6 +795,64 @@ yang_find_mynamespace(yang_stmt *ys)
|
|||
return namespace;
|
||||
}
|
||||
|
||||
/*! Given a yang statement and namespace, find local prefix valid in module
|
||||
* This is useful if you want to make a "reverse" lookup, you know the
|
||||
* (global) namespace of a module, but you do not know the local prefix
|
||||
* used to access it in XML.
|
||||
* @param[in] ys Yang statement in module tree (or module itself)
|
||||
* @param[in] namespace Namspace URI as char* pointer into yang tree
|
||||
* @param[out] prefix Local prefix to access module with (direct pointer)
|
||||
* @retval 0 not found
|
||||
* @retval -1 found
|
||||
* @code
|
||||
* @note prefix NULL is not returned, if own module, then return its prefix
|
||||
* char *pfx = yang_find_prefix_by_namespace(ys, "urn:example:clixon", &prefix);
|
||||
* @endcode
|
||||
*/
|
||||
int
|
||||
yang_find_prefix_by_namespace(yang_stmt *ys,
|
||||
char *namespace,
|
||||
char **prefix)
|
||||
{
|
||||
int retval = 0; /* not found */
|
||||
yang_stmt *my_ymod; /* My module */
|
||||
char *myns; /* My ns */
|
||||
yang_stmt *yspec;
|
||||
yang_stmt *ymod;
|
||||
char *modname = NULL;
|
||||
yang_stmt *yimport;
|
||||
yang_stmt *yprefix;
|
||||
|
||||
clicon_debug(1, "%s", __FUNCTION__);
|
||||
/* First check if namespace is my own module */
|
||||
myns = yang_find_mynamespace(ys);
|
||||
if (strcmp(myns, namespace) == 0){
|
||||
*prefix = yang_find_myprefix(ys); /* or NULL? */
|
||||
goto found;
|
||||
}
|
||||
/* Next, find namespaces in imported modules */
|
||||
yspec = ys_spec(ys);
|
||||
if ((ymod = yang_find_module_by_namespace(yspec, namespace)) == NULL)
|
||||
goto notfound;
|
||||
modname = yang_argument_get(ymod);
|
||||
my_ymod = ys_module(ys);
|
||||
/* Loop through import statements to find a match with ymod */
|
||||
yimport = NULL;
|
||||
while ((yimport = yn_each(my_ymod, yimport)) != NULL) {
|
||||
if (yang_keyword_get(yimport) == Y_IMPORT &&
|
||||
strcmp(modname, yang_argument_get(yimport)) == 0){ /* match */
|
||||
yprefix = yang_find(yimport, Y_PREFIX, NULL);
|
||||
*prefix = yang_argument_get(yprefix);
|
||||
goto found;
|
||||
}
|
||||
}
|
||||
notfound:
|
||||
return retval;
|
||||
found:
|
||||
assert(*prefix);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*! If a given yang stmt has a choice/case as parent, return the choice statement
|
||||
*/
|
||||
yang_stmt *
|
||||
|
|
@ -985,16 +1043,8 @@ ys_module_by_xml(yang_stmt *ysp,
|
|||
if (ymodp)
|
||||
*ymodp = NULL;
|
||||
prefix = xml_prefix(xt);
|
||||
if (prefix){
|
||||
/* Get namespace for prefix */
|
||||
if (xml2ns(xt, prefix, &namespace) < 0)
|
||||
goto done;
|
||||
}
|
||||
else{
|
||||
/* Get default namespace */
|
||||
if (xml2ns(xt, NULL, &namespace) < 0)
|
||||
goto done;
|
||||
}
|
||||
if (xml2ns(xt, prefix, &namespace) < 0) /* prefix may be NULL */
|
||||
goto done;
|
||||
/* No namespace found, give up */
|
||||
if (namespace == NULL)
|
||||
goto ok;
|
||||
|
|
@ -1134,8 +1184,7 @@ yang_find_module_by_prefix(yang_stmt *ys,
|
|||
}
|
||||
yimport = NULL;
|
||||
while ((yimport = yn_each(my_ymod, yimport)) != NULL) {
|
||||
if (yang_keyword_get(yimport) != Y_IMPORT &&
|
||||
yang_keyword_get(yimport) != Y_INCLUDE)
|
||||
if (yang_keyword_get(yimport) != Y_IMPORT)
|
||||
continue;
|
||||
if ((yprefix = yang_find(yimport, Y_PREFIX, NULL)) != NULL &&
|
||||
strcmp(yang_argument_get(yprefix), prefix) == 0){
|
||||
|
|
@ -1143,8 +1192,7 @@ yang_find_module_by_prefix(yang_stmt *ys,
|
|||
}
|
||||
}
|
||||
if (yimport){
|
||||
if ((ymod = yang_find(yspec, Y_MODULE, yang_argument_get(yimport))) == NULL &&
|
||||
(ymod = yang_find(yspec, Y_SUBMODULE, yang_argument_get(yimport))) == NULL){
|
||||
if ((ymod = yang_find(yspec, Y_MODULE, yang_argument_get(yimport))) == NULL){
|
||||
clicon_err(OE_YANG, 0, "No module or sub-module found with prefix %s",
|
||||
prefix);
|
||||
yimport = NULL;
|
||||
|
|
@ -1155,6 +1203,25 @@ yang_find_module_by_prefix(yang_stmt *ys,
|
|||
return ymod;
|
||||
}
|
||||
|
||||
/* Get module from its own prefix
|
||||
* This is really not a valid usecase, a kludge for the identityref derived
|
||||
* list workaround (IDENTITYREF_KLUDGE)
|
||||
*/
|
||||
yang_stmt *
|
||||
yang_find_module_by_prefix_yspec(yang_stmt *yspec,
|
||||
char *prefix)
|
||||
{
|
||||
yang_stmt *ymod = NULL;
|
||||
yang_stmt *yprefix;
|
||||
|
||||
while ((ymod = yn_each(yspec, ymod)) != NULL)
|
||||
if (ymod->ys_keyword == Y_MODULE &&
|
||||
(yprefix = yang_find(ymod, Y_PREFIX, NULL)) != NULL &&
|
||||
strcmp(yang_argument_get(yprefix), prefix) == 0)
|
||||
return ymod;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*! Given a yang spec and a namespace, return yang module
|
||||
*
|
||||
* @param[in] yspec A yang specification
|
||||
|
|
|
|||
|
|
@ -8,12 +8,26 @@
|
|||
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
|
||||
|
||||
: ${clixon_util_json:=clixon_util_json}
|
||||
: ${clixon_util_xml:=clixon_util_xml}
|
||||
|
||||
fyang=$dir/json.yang
|
||||
cat <<EOF > $fyang
|
||||
module json{
|
||||
prefix ex;
|
||||
namespace "urn:example:clixon";
|
||||
identity genre {
|
||||
description
|
||||
"From RFC8040 jukebox example.
|
||||
Identity prefixes are translated from module-name to xml prefix";
|
||||
}
|
||||
identity blues {
|
||||
base genre;
|
||||
}
|
||||
typedef gtype{
|
||||
type identityref{
|
||||
base genre;
|
||||
}
|
||||
}
|
||||
leaf a{
|
||||
type int32;
|
||||
}
|
||||
|
|
@ -25,6 +39,14 @@ module json{
|
|||
type string;
|
||||
}
|
||||
}
|
||||
leaf g1 {
|
||||
description "direct type";
|
||||
type identityref { base genre; }
|
||||
}
|
||||
leaf g2 {
|
||||
description "indirect type";
|
||||
type gtype;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
|
|
@ -68,18 +90,42 @@ expecteofeq "$clixon_util_json -jpy $fyang" 0 "$JSON" "$JSONP"
|
|||
JSON='{"json:a":-23}'
|
||||
|
||||
new "json leaf back to json"
|
||||
expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON"
|
||||
expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" "$JSON"
|
||||
|
||||
JSON='{"json:c":{"a":937}}'
|
||||
new "json parse container back to json"
|
||||
expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON"
|
||||
expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" "$JSON"
|
||||
|
||||
# This should work
|
||||
# identities translation json -> xml is tricky wrt prefixes, json uses module
|
||||
# name, xml uses xml namespace prefixes (or default)
|
||||
JSON='{"json:g1":"json:blues"}'
|
||||
|
||||
new "json identity to xml"
|
||||
expecteofx "$clixon_util_json -y $fyang" 0 "$JSON" '<g1 xmlns="urn:example:clixon">blues</g1>'
|
||||
|
||||
new "json identity back to json"
|
||||
expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" '{"json:g1":"blues"}'
|
||||
|
||||
new "xml identity with explicit ns to json"
|
||||
expecteofx "$clixon_util_xml -ovjy $fyang" 0 '<g1 xmlns="urn:example:clixon" xmlns:ex="urn:example:clixon">ex:blues</g1>' '{"json:g1":"blues"}'
|
||||
|
||||
# Same with indirect type
|
||||
JSON='{"json:g2":"json:blues"}'
|
||||
|
||||
new "json indirect identity to xml"
|
||||
expecteofx "$clixon_util_json -y $fyang" 0 "$JSON" '<g2 xmlns="urn:example:clixon">blues</g2>'
|
||||
|
||||
new "json indirect identity back to json"
|
||||
expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" '{"json:g2":"blues"}'
|
||||
|
||||
new "xml indirect identity with explicit ns to json"
|
||||
expecteofx "$clixon_util_xml -ojvy $fyang" 0 '<g2 xmlns="urn:example:clixon" xmlns:ex="urn:example:clixon">ex:blues</g2>' '{"json:g2":"blues"}'
|
||||
|
||||
# XXX CDATA translation, should work bit does not
|
||||
if false; then
|
||||
JSON='{"json:c": {"s": "<![CDATA[ z > x & x < y ]]>"}}'
|
||||
new "json parse cdata xml"
|
||||
expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON"
|
||||
fi
|
||||
|
||||
|
||||
rm -rf $dir
|
||||
|
|
|
|||
|
|
@ -98,8 +98,8 @@ new "netconf get $perfreq single reqs"
|
|||
done | $clixon_netconf -qf $cfg > /dev/null; } 2>&1 | awk '/real/ {print $2}'
|
||||
|
||||
# RESTCONF get
|
||||
new "restconf get test single req XXX"
|
||||
expecteq "$(curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces/interface=e1)" 0 '{"ietf-interfaces:interface":[{"name":"e1","type":"ex:eth","enabled":true,"oper-status":"up"}]}
|
||||
new "restconf get test single req"
|
||||
expecteq "$(curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces/interface=e1)" 0 '{"ietf-interfaces:interface":[{"name":"e1","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}
|
||||
'
|
||||
|
||||
new "restconf get $perfreq single reqs"
|
||||
|
|
|
|||
|
|
@ -160,16 +160,16 @@ expecteq "$(curl -s -X GET http://localhost/restconf/data/clixon-example:state)"
|
|||
|
||||
# Exact match
|
||||
new "restconf Add subtree eth/0/0 to datastore using POST"
|
||||
expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 201 Created'
|
||||
expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 201 Created'
|
||||
|
||||
new "restconf Re-add subtree eth/0/0 which should give error"
|
||||
expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://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"}}}'
|
||||
expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}} http://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"}}}'
|
||||
|
||||
# XXX Cant get this to work
|
||||
#expecteq "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"ex:eth\",\"enabled\":true}}} http://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"}}}'
|
||||
#expecteq "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"clixon-example:eth\",\"enabled\":true}}} http://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"}}}'
|
||||
|
||||
new "restconf Check interfaces eth/0/0 added"
|
||||
expectfn "curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}}}
|
||||
expectfn "curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces" 0 '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}\]}}}
|
||||
'
|
||||
|
||||
new "restconf delete interfaces"
|
||||
|
|
@ -179,15 +179,12 @@ new "restconf Check empty config"
|
|||
expectfn "curl -sG http://localhost/restconf/data/clixon-example:state" 0 "$state
|
||||
"
|
||||
|
||||
# XXX: gives <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
|
||||
# <interface xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
|
||||
new "restconf Add interfaces subtree eth/0/0 using POST"
|
||||
expectfn 'curl -s -X POST -d {"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}} http://localhost/restconf/data/ietf-interfaces:interfaces' 0 ""
|
||||
# XXX cant get this to work
|
||||
#expecteq "$(curl -s -X POST -d '{"interface":{"name":"eth/0/0","type\":"ex:eth","enabled":true}}' http://localhost/restconf/data/interfaces)" 0 ""
|
||||
expectpart "$(curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}')" 0 ""
|
||||
#expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces -d {"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' 0 ""
|
||||
|
||||
new "restconf Check eth/0/0 added config"
|
||||
expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}}
|
||||
expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}}
|
||||
'
|
||||
|
||||
new "restconf Check eth/0/0 added state"
|
||||
|
|
@ -195,7 +192,7 @@ expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state)" 0 '
|
|||
'
|
||||
|
||||
new "restconf Re-post eth/0/0 which should generate error"
|
||||
expecteq "$(curl -s -X POST -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://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"}}}
'
|
||||
expecteq "$(curl -s -X POST -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://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"}}}
'
|
||||
|
||||
new "Add leaf description using POST"
|
||||
expecteq "$(curl -s -X POST -d '{"ietf-interfaces:description":"The-first-interface"}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 ""
|
||||
|
|
@ -204,7 +201,7 @@ new "Add nothing using POST"
|
|||
expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0' 0 '"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":" on line 1: syntax error at or before:'
|
||||
|
||||
new "restconf Check description added"
|
||||
expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","description":"The-first-interface","type":"ex:eth","enabled":true,"oper-status":"up"}]}}
|
||||
expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}}
|
||||
'
|
||||
|
||||
new "restconf delete eth/0/0"
|
||||
|
|
@ -217,10 +214,10 @@ new "restconf Re-Delete eth/0/0 using none should generate error"
|
|||
expecteq "$(curl -s -X DELETE http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 '{"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"
|
||||
expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 ""
|
||||
expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 ""
|
||||
|
||||
new "restconf get subtree"
|
||||
expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}}
|
||||
expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}}
|
||||
'
|
||||
|
||||
new "restconf rpc using POST json"
|
||||
|
|
@ -262,10 +259,10 @@ if [ -z "$match" ]; then
|
|||
fi
|
||||
|
||||
new "restconf Add subtree without key (expected error)"
|
||||
expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key, expected '"'"'=restval'"'"'"}}}
'
|
||||
expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key, expected '"'"'=restval'"'"'"}}}
'
|
||||
|
||||
new "restconf Add subtree with too many keys (expected error)"
|
||||
expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}}
'
|
||||
expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}}
'
|
||||
|
||||
new "Kill restconf daemon"
|
||||
stop_restconf
|
||||
|
|
|
|||
|
|
@ -337,13 +337,12 @@ expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+xml' ht
|
|||
|
||||
new "4.5. PUT replace content"
|
||||
# XXX should be: jbox:alternative --> example-jukebox:alternative
|
||||
expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"jbox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content"
|
||||
expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"example-jukebox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content"
|
||||
|
||||
new "4.5. PUT replace content (xml encoding)"
|
||||
expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '<album xmlns="http://example.com/ns/example-jukebox" xmlns:jbox="http://example.com/ns/example-jukebox"><name>Wasting Light</name><genre>jbox:alternative</genre><year>2011</year></album>')" 0 "HTTP/1.1 204 No Content"
|
||||
|
||||
new "4.5. PUT create new"
|
||||
# XXX should be: jbox:alternative --> example-jukebox:alternative
|
||||
new "4.5. PUT create new identity"
|
||||
expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
|
|||
|
||||
: ${clixon_util_xml:=clixon_util_xml -o} # -o is output
|
||||
|
||||
sleep 1 # mysterious fail, maybe this helps?
|
||||
|
||||
new "xml parse"
|
||||
expecteof "$clixon_util_xml" 0 "<a><b/></a>" "^<a><b/></a>$"
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ usage(char *argv0)
|
|||
"where options are\n"
|
||||
"\t-h \t\tHelp\n"
|
||||
"\t-D <level> \tDebug\n"
|
||||
"\t-j \t\tOutput as JSON\n"
|
||||
"\t-j \t\tOutput as JSON (default is as XML)\n"
|
||||
"\t-l <s|e|o> \tLog on (s)yslog, std(e)rr, std(o)ut (stderr is default)\n"
|
||||
"\t-p \t\tPretty-print output\n"
|
||||
"\t-y <filename> \tyang filename to parse (must be stand-alone)\n" ,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue