diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ea76a9f..60efc564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Major New features * Restconf RFC 8040 increased feature compliance * RESTCONF "insert" and "point" query parameters supported + * Applies to ordered-by-user leaf and leaf-lists * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: * `201 Created` for created resources * `204 No Content` for replaced resources. @@ -22,6 +23,9 @@ * The main example explains how to implement a Yang extension in a backend plugin. ### API changes on existing features (you may need to change your code) +* Netconf edit-config "operation" attribute namespace check is enforced + * This is enforced: ` + * This was previously allowed: ` * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: * `201 Created` for created resources * `204 No Content` for replaced resources. diff --git a/README.md b/README.md index fac2ee1e..3980efa8 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,7 @@ Clixon interaction is best done posting issues, pull requests, or joining the Clixon provides a core system and can be used as-is using available Yang specifications. However, an application very quickly needs to specialize functions. Clixon is extended by writing -plugins for cli and backend. Extensions for netconf and restconf -are also available. +plugins for cli, backend, netconf and restconf. Plugins are written in C and easiest is to look at [example](example/README.md) or consulting the [FAQ](doc/FAQ.md). @@ -100,12 +99,12 @@ However, the following YANG syntax modules are not implemented (reference to RFC - action (7.15) - augment in a uses sub-clause (7.17) (module-level augment is implemented) - require-instance -- instance-identifier type +- instance-identifier type (9.13) - status (7.21.2) - YIN (13) - Yang extended Xpath functions: re-match(), deref)(), derived-from(), derived-from-or-self(), enum-value(), bit-is-set() (10.2-10.6) - Default values on leaf-lists are not supported (7.7.2) -- instance-identifier type +- Lists without keys (non-config lists may lack keys) ### Yang patterns Yang type patterns use regexps defined in [W3C XML XSD](http://www.w3.org/TR/2004/REC-xmlschema-2-20041028). XSD regexp:s are diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index f29f1b25..2e8966fb 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -558,6 +558,22 @@ restconf_terminate(clicon_handle h) * @param[in] qvec Query parameters (eg where insert/point should be) * @retval 0 OK * @retval -1 Error + * A difficulty is that RESTCONF 'point' is a complete identity-identifier encoded + * as an uri-path + * wheras NETCONF key and value are: + * The value of the "key" attribute is the key predicates of the full instance identifier + * the "value" attribute MUST also be used to specify an existing entry in the + * leaf-list. + * This means that api-path is first translated to xpath, and then strip everything + * except the key values (or leaf-list values). + * Example: + * RESTCONF URI: point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D1 + * Leaf example: + * instance-id: /ex:system/ex:service[ex:name='foo'][ex:enabled=''] + * NETCONF: yang:key="[ex:name='foo'][ex:enabled=''] + * Leaf-list example: + * instance-id: /ex:system/ex:service[.='foo'] + * NETCONF: yang:value="foo" */ int restconf_insert_attributes(cxobj *xdata, @@ -570,7 +586,11 @@ restconf_insert_attributes(cxobj *xdata, yang_stmt *y; char *attrname; int ret; - + char *xpath = NULL; + char *namespace = NULL; + cbuf *cb = NULL; + char *p; + y = xml_spec(xdata); if ((instr = cvec_find_str(qvec, "insert")) != NULL){ /* First add xmlns:yang attribute */ @@ -591,9 +611,6 @@ restconf_insert_attributes(cxobj *xdata, goto done; } if ((pstr = cvec_find_str(qvec, "point")) != NULL){ - char *xpath = NULL; - char *namespace = NULL; - cbuf *cb = NULL; if (y == NULL){ clicon_err(OE_YANG, 0, "Cannot yang resolve %s", xml_name(xdata)); goto done; @@ -614,16 +631,36 @@ restconf_insert_attributes(cxobj *xdata, clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } - cprintf(cb, "/%s", xpath); /* XXX: also prefix/namespace? */ + if (yang_keyword_get(y) == Y_LIST){ + /* translate /../x[] --> []*/ + if ((p = rindex(xpath,'/')) == NULL) + p = xpath; + p = index(p, '['); + cprintf(cb, "%s", p); + } + else{ /* LEAF_LIST */ + /* translate /../x[.='x'] --> x */ + if ((p = rindex(xpath,'\'')) == NULL){ + clicon_err(OE_YANG, 0, "Translated api->xpath %s->%s not on leaf-list canonical form: ../[.='x']", pstr, xpath); + goto done; + } + *p = '\0'; + if ((p = rindex(xpath,'\'')) == NULL){ + clicon_err(OE_YANG, 0, "Translated api->xpath %s->%s not on leaf-list canonical form: ../[.='x']", pstr, xpath); + goto done; + } + p++; + cprintf(cb, "%s", p); + } if (xml_value_set(xa, cbuf_get(cb)) < 0) goto done; - if (xpath) - free(xpath); - if (cb) - cbuf_free(cb); } retval = 0; done: + if (xpath) + free(xpath); + if (cb) + cbuf_free(cb); return retval; } diff --git a/lib/src/clixon_xml_sort.c b/lib/src/clixon_xml_sort.c index ba9b8435..bd685164 100644 --- a/lib/src/clixon_xml_sort.c +++ b/lib/src/clixon_xml_sort.c @@ -487,6 +487,15 @@ xml_search(cxobj *xp, * @param[in] key_val Key if LIST and ins is before/after, val if LEAF_LIST * @retval i Order where xn should be inserted into xp:s children * @retval -1 Error + * LIST: RFC 7950 7.8.6: + * The value of the "key" attribute is the key predicates of the + * full instance identifier (see Section 9.13) for the list entry. + * This means the value can be [x='a'] but the full instance-id should be prepended, + * such as /ex:system/ex:services[x='a'] + * + * LEAF-LIST: RFC7950 7.7.9 + * yang:insert="after" + * yang:value="3des-cbc">blowfish-cbc) */ static int @@ -501,7 +510,6 @@ xml_insert_userorder(cxobj *xp, int i; cxobj *xc; yang_stmt *yc; - char *kludge = ""; /* Cant get instance-id generic of [.. and /.. case */ switch (ins){ case INS_FIRST: @@ -534,7 +542,7 @@ xml_insert_userorder(cxobj *xp, else{ switch (yang_keyword_get(yn)){ case Y_LEAF_LIST: - if ((xc = xpath_first(xp, "%s", key_val)) == NULL) + if ((xc = xpath_first(xp, "%s[.='%s']", xml_name(xn),key_val)) == NULL) clicon_err(OE_YANG, 0, "bad-attribute: value, missing-instance: %s", key_val); else { if ((i = xml_child_order(xp, xc)) < 0) @@ -544,10 +552,8 @@ xml_insert_userorder(cxobj *xp, } break; case Y_LIST: - if (strlen(key_val) && key_val[0] == '[') - kludge = xml_name(xn); - if ((xc = xpath_first(xp, "%s%s", kludge, key_val)) == NULL) - clicon_err(OE_YANG, 0, "bad-attribute: key, missing-instance: %s%s", xml_name(xn), key_val); + if ((xc = xpath_first(xp, "%s%s", xml_name(xn), key_val)) == NULL) + clicon_err(OE_YANG, 0, "bad-attribute: key, missing-instance: %s", key_val); else { if ((i = xml_child_order(xp, xc)) < 0) clicon_err(OE_YANG, 0, "internal error xpath found but not in child list"); diff --git a/test/test_order.sh b/test/test_order.sh index 7dcf82a0..3e69645c 100755 --- a/test/test_order.sh +++ b/test/test_order.sh @@ -324,17 +324,17 @@ expecteof "$clixon_netconf -qf $cfg" 0 '71]]>]]>" +XML="71]]>]]>" expecteof "$clixon_netconf -qf $cfg" 0 "$XML" "^]]>]]>$" new "add one entry 42 after b" -XML="42]]>]]>" +XML="42]]>]]>" expecteof "$clixon_netconf -qf $cfg" 0 "$XML" "^]]>]]>$" # XXX actually not right error message, should be as RFC7950 Sec 15.7 new "add one entry 99 after Q (not found, error)" -XML="99]]>]]>" -RES="^protocoloperation-failederrorbad-attribute: value, missing-instance: y0\[.='Q'\]]]>]]>$" +XML="99]]>]]>" +RES="^protocoloperation-failederrorbad-attribute: value, missing-instance: Q]]>]]>$" expecteof "$clixon_netconf -qf $cfg" 0 "$XML" "$RES" new "check ordered-by-user: e,a,71,b,42,c,d" @@ -384,7 +384,7 @@ expecteof "$clixon_netconf -qf $cfg" 0 "99bar]]>]]>" "^protocoloperation-failederrorbad-attribute: key, missing-instance: y2\[k='Q'\]]]>]]>$" +expecteof "$clixon_netconf -qf $cfg" 0 "99bar]]>]]>" "^protocoloperation-failederrorbad-attribute: key, missing-instance: Q]]>]]>$" new "check ordered-by-user: e,a,71,b,42,c,d" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^ebarafoo71fiebbar42fumcfoodfie]]>]]>$' diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 702d19ce..5dbb7b93 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -360,11 +360,11 @@ expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" new 'B.3.4. "insert" Parameter' JSON="{\"example-jukebox:song\":[{\"index\":1,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Rope']\"}]}" -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=1' new 'B.3.4. "insert" Parameter first (RFC example says after)' JSON="{\"example-jukebox:song\":[{\"index\":0,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Bridge Burning']\"}]}" -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=0' new 'B.3.4. "insert" Parameter check order' RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" @@ -372,13 +372,11 @@ expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:j new 'B.3.5. "point" Parameter (before for more interesting order: 0,2,1)' JSON="{\"example-jukebox:song\":[{\"index\":2,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Bridge Burning']\"}]}" -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=before\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D1 )" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=before\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D1 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=2' new 'B.3.5. "point" check order (0,2,1)' RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]2/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" -expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" - -#XXX 'Location: https://example.com/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=2' +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" new 'B.3.5. "point" Parameter 3 after 2 (using PUT)' JSON="{\"example-jukebox:song\":[{\"index\":3,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Something else']\"}]}" @@ -392,23 +390,23 @@ new "restconf DELETE whole datastore" expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" new 'B.3.4. "insert/point" leaf-list 3 (not in RFC)' -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"3"}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"3"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=3' new 'B.3.4. "insert/point" leaf-list 2 first' -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=first -d '{"example-jukebox:extra":"2"}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=first -d '{"example-jukebox:extra":"2"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=2' new 'B.3.4. "insert/point" leaf-list 1 last' -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' #new 'B.3.4. "insert/point" move leaf-list 1 last' #- restconf cannot move a leaf-list(list?) item -#expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" +#expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' new 'B.3.5. "insert/point" leaf-list check order (2,3,1)' expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '231' new 'B.3.5. "point" Parameter leaf-list 4 before 3' -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' -d '{"example-jukebox:extra":"4"}' http://localhost/restconf/data?insert=before\&point=%2Fexample-jukebox%3Aextra%3D3 )" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' -d '{"example-jukebox:extra":"4"}' http://localhost/restconf/data?insert=before\&point=%2Fexample-jukebox%3Aextra%3D3 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=4' new 'B.3.5. "insert/point" leaf-list check order (2,4,3,1)' expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '2431'