/* * ***** BEGIN LICENSE BLOCK ***** Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren 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 ***** * * Code for handling netconf rpc messages according to RFC 4741 and RFC 5277 *****************************************************************************/ #ifdef HAVE_CONFIG_H #include "clixon_config.h" /* generated by config & autoconf */ #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* cligen */ #include /* clicon */ #include #include "clixon_netconf.h" #include "netconf_lib.h" #include "netconf_filter.h" #include "netconf_plugin.h" #include "netconf_rpc.h" /* * */ /*! Get configuration * @param[in] h Clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK * @note filter type subtree and xpath is supported, but xpath is preferred, and * better performance and tested. Please use xpath. * * * * | * * * * * * | * * * * * * * * * Example: * * * ]]>]]> * Variants of the functions where x-axis is the variants of the clause * and y-axis is whether a or is present. * | no filter | filter subnet | filter xpath | * -----------------+-----------+---------------+--------------+ * no config | | | | * -----------------+-----------+---------------+--------------+ * config/select | - | | | * -----------------+-----------+---------------+--------------+ * Example requests of each: * no filter + no config ]]>]]> * filter subnet + no config: ]]>]]> * filter xpath + select all: ]]>]]> * filter subtree + config: ]]>]]> * filter xpath + select: ]]>]]> */ static int netconf_get_config(clicon_handle h, cxobj *xn, cxobj **xret) { cxobj *xfilter; /* filter */ int retval = -1; char *source; char *ftype = NULL; cxobj *xfilterconf; cxobj *xconf; if ((source = netconf_get_target(xn, "source")) == NULL){ xml_parse_va(xret, NULL, "" "missing-element" "protocol" "error" "source" ""); goto ok; } /* ie ... */ if ((xfilter = xpath_first(xn, "filter")) != NULL) ftype = xml_find_value(xfilter, "type"); if (ftype == NULL || strcmp(ftype, "xpath")==0){ if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; } else if (strcmp(ftype, "subtree")==0){ /* Default rfc filter is subtree. I prefer xpath and use it internally. Get whole subtree and then filter aftwerwards. This is suboptimal. Therefore please use xpath. */ if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; if (xfilter && (xfilterconf = xpath_first(xfilter, "//configuration"))!= NULL && (xconf = xpath_first(*xret, "/rpc-reply/data")) != NULL){ /* xml_filter removes parts of xml tree not matching */ if ((strcmp(xml_name(xfilterconf), xml_name(xconf))!=0) || xml_filter(xfilterconf, xconf) < 0){ xml_parse_va(xret, NULL, "" "operation-failed" "applicatio" "error" "filtering" ""); } } } else{ xml_parse_va(xret, NULL, "" "operation-failed" "applicatio" "error" "filter type not supported" "type" ""); } ok: /* netconf error is not fatal */ retval = 0; done: return retval; } /*! Get options from netconf edit-config * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] op Operation type, eg merge,replace,... * @param[out] testopt test option, eg set, test * @param[out] erropt Error option, eg stop-on-error * @retval -1 Fatal Error * @retval 0 parameter error, xret returns error * @retval 1 OK, op, testopt and erropt set * @example * * ... * (merge | none | replace) * (stop-on-error | continue-on-error ) * (set | test-then-set | test-only) * */ static int get_edit_opts(cxobj *xn, enum test_option *testopt, enum error_option *erropt, cxobj **xret) { int retval = -1; cxobj *x; char *optstr; if ((x = xpath_first(xn, "test-option")) != NULL){ if ((optstr = xml_body(x)) != NULL){ if (strcmp(optstr, "test-then-set") == 0) *testopt = TEST_THEN_SET; else if (strcmp(optstr, "set") == 0) *testopt = SET; else if (strcmp(optstr, "test-only") == 0) *testopt = TEST_ONLY; else goto parerr; } } if ((x = xpath_first(xn, "error-option")) != NULL){ if ((optstr = xml_body(x)) != NULL){ if (strcmp(optstr, "stop-on-error") == 0) *erropt = STOP_ON_ERROR; else if (strcmp(optstr, "continue-on-error") == 0) *erropt = CONTINUE_ON_ERROR; else goto parerr; } } retval = 1; /* hunky dory */ return retval; parerr: /* parameter error, xret set */ xml_parse_va(xret, NULL, "" "invalid-value" "protocol" "error" ""); return 0; } /*! Netconf edit configuration Write the change on a tmp file, then load that into candidate configuration. (merge | none | replace) (stop-on-error | continue-on-error ) (set | test-then-set | test-only) CLIXON addition: * * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK * @retval 0 OK, xret points to valid return, either ok or rpc-error * @retval -1 Error * only 'config' supported * error-option: only stop-on-error supported * test-option: not supported * * @note erropt, testopt only supports default */ static int netconf_edit_config(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; int optret; enum operation_type operation = OP_MERGE; enum test_option testopt = TEST_THEN_SET;/* only supports this */ enum error_option erropt = STOP_ON_ERROR; /* only supports this */ cxobj *xc; /* config */ cxobj *x; cxobj *xfilter; char *ftype = NULL; char *target; /* db */ /* must have target, and it should be candidate */ if ((target = netconf_get_target(xn, "target")) == NULL || strcmp(target, "candidate")){ xml_parse_va(xret, NULL, "" "missing-element" "protocol" "error" "target" ""); goto ok; } /* CLICON addition, eg /> */ if ((xfilter = xpath_first(xn, "filter")) != NULL) { if ((ftype = xml_find_value(xfilter, "type")) != NULL) if (strcmp(ftype,"restconf")){ xml_parse_va(xret, NULL, "" "invalid-value" "protocol" "error" ""); goto ok; } } if ((x = xpath_first(xn, "default-operation")) != NULL){ if (xml_operation(xml_body(x), &operation) < 0){ xml_parse_va(xret, NULL, "" "invalid-value" "protocol" "error" ""); goto ok; } } if ((optret = get_edit_opts(xn, &testopt, &erropt, xret)) < 0) goto done; if (optret == 0) /* error in opt parameters */ goto ok; /* not supported opts */ if (testopt!=TEST_THEN_SET || erropt!=STOP_ON_ERROR){ xml_parse_va(xret, NULL, "" "operation-not-supported" "protocol" "error" ""); goto ok; } /* operation is OP_REPLACE, OP_MERGE, or OP_NONE pass all to backend */ if ((xc = xpath_first(xn, "config")) != NULL){ #if 0 /* application-specific code registers 'config' */ if ((ret = netconf_plugin_callbacks(h, xc, xret)) < 0){ goto ok; } #endif if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; } ok: retval = 0; done: return retval; } /*! Netconf copy configuration * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_copy_config(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; char *source; char *target; /* filenames */ if ((source = netconf_get_target(xn, "source")) == NULL){ xml_parse_va(xret, NULL, "" "missing-element" "protocol" "error" "source" ""); goto ok; } if ((target = netconf_get_target(xn, "target")) == NULL){ xml_parse_va(xret, NULL, "" "missing-element" "protocol" "error" "target" ""); goto ok; } if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; ok: retval = 0; done: return retval; } /*! Delete configuration Delete a configuration datastore. The configuration datastore cannot be deleted. * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_delete_config(clicon_handle h, cxobj *xn, cxobj **xret) { char *target; /* filenames */ int retval = -1; if ((target = netconf_get_target(xn, "target")) == NULL || strcmp(target, "running")==0){ xml_parse_va(xret, NULL, "" "missing-element" "protocol" "error" "target" ""); goto ok; } if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; ok: retval = 0; done: return retval; } /*! Lock a database * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_lock(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; char *target; if ((target = netconf_get_target(xn, "target")) == NULL){ xml_parse_va(xret, NULL, "" "missing-element" "protocol" "error" "target" ""); goto ok; } if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; ok: retval = 0; done: return retval; } /*! Unlock a database XXX * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_unlock(clicon_handle h, cxobj *xn, cxobj **xret) { return netconf_lock(h, xn, xret); } /*! Get running configuration and device state information * * * @param[in] h Clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK * @note filter type subtree and xpath is supported, but xpath is preferred, and * better performance and tested. Please use xpath. * * @example * * ]]>]]> */ static int netconf_get(clicon_handle h, cxobj *xn, cxobj **xret) { cxobj *xfilter; /* filter */ int retval = -1; char *ftype = NULL; cxobj *xfilterconf; cxobj *xconf; /* ie ... */ if ((xfilter = xpath_first(xn, "filter")) != NULL) ftype = xml_find_value(xfilter, "type"); if (ftype == NULL || strcmp(ftype, "xpath")==0){ if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; } else if (strcmp(ftype, "subtree")==0){ /* Default rfc filter is subtree. I prefer xpath and use it internally. Get whole subtree and then filter aftwerwards. This is suboptimal. Therefore please use xpath. */ if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; if (xfilter && (xfilterconf = xpath_first(xfilter, "//configuration"))!= NULL && (xconf = xpath_first(*xret, "/rpc-reply/data")) != NULL){ /* xml_filter removes parts of xml tree not matching */ if ((strcmp(xml_name(xfilterconf), xml_name(xconf))!=0) || xml_filter(xfilterconf, xconf) < 0){ xml_parse_va(xret, NULL, "" "operation-failed" "applicatio" "error" "filtering" ""); } } } else{ xml_parse_va(xret, NULL, "" "operation-failed" "applicatio" "error" "filter type not supported" "type" ""); } // ok: /* netconf error is not fatal */ retval = 0; done: return retval; } /*! Close a (user) session * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_close_session(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; cc_closed++; if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; retval = 0; done: return retval; } /*! Kill other user sessions PID * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_kill_session(clicon_handle h, cxobj *xn, cxobj **xret) { int retval=-1; cxobj *xs; if ((xs = xpath_first(xn, "//session-id")) == NULL){ xml_parse_va(xret, NULL, "" "missing-element" "protocol" "error" "session-id" ""); goto ok; } if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; ok: retval = 0; done: return retval; } /*! Check the semantic consistency of candidate :validate * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_validate(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; char *target; if ((target = netconf_get_target(xn, "source")) == NULL){ xml_parse_va(xret, NULL, "" "missing-element" "protocol" "error" "target" ""); goto ok; } if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; ok: retval = 0; done: return retval; } /*! Commit candidate -> running :candidate * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_commit(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; retval = 0; done: return retval; } /*! Discard all changes in candidate / revert to running :candidate * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_discard_changes(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; retval = 0; done: return retval; } /*! Called when a notification has happened on backend * and this session has registered for that event. * Filter it and forward it. 2007-07-08T00:01:00Z fault Ethernet0 major */ static int netconf_notification_cb(int s, void *arg) { cxobj *xfilter = (cxobj *)arg; char *selector; struct clicon_msg *reply = NULL; int eof; char *event = NULL; int retval = -1; cbuf *cb; cxobj *xe = NULL; /* event xml */ cxobj *xt = NULL; /* top xml */ if (0){ fprintf(stderr, "%s\n", __FUNCTION__); /* debug */ xml_print(stderr, xfilter); /* debug */ } /* get msg (this is the reason this function is called) */ if (clicon_msg_rcv(s, &reply, &eof) < 0) goto done; /* handle close from remote end: this will exit the client */ if (eof){ clicon_err(OE_PROTO, ESHUTDOWN, "%s: Socket unexpected close", __FUNCTION__); close(s); errno = ESHUTDOWN; event_unreg_fd(s, netconf_notification_cb); if (xfilter) xml_free(xfilter); goto done; } if (clicon_msg_decode(reply, &xt) < 0) goto done; if ((xe = xpath_first(xt, "//event")) != NULL) event = xml_body(xe); /* parse event */ if (0){ /* XXX CLICON events are not xml */ /* find and apply filter */ if ((selector = xml_find_value(xfilter, "select")) == NULL) goto done; if (xpath_first(xe, selector) == NULL) { fprintf(stderr, "%s no match\n", __FUNCTION__); /* debug */ } } /* create netconf message */ if ((cb = cbuf_new()) == NULL){ clicon_err(OE_PLUGIN, errno, "%s: cbuf_new", __FUNCTION__); goto done; } add_preamble(cb); /* Make it well-formed netconf xml */ cprintf(cb, "%s", event); add_postamble(cb); /* Send it to listening client on stdout */ if (netconf_output(1, cb, "notification") < 0){ cbuf_free(cb); goto done; } fflush(stdout); cbuf_free(cb); retval = 0; done: if (xt != NULL) xml_free(xt); if (reply) free(reply); return retval; } /* RESULT # If not present, events in the default NETCONF stream will be sent. XPATH-EXPR<(filter> # only for replay (NYI) # only for replay (NYI) Dont support replay * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK */ static int netconf_create_subscription(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; cxobj *xfilter; int s; char *ftype; if ((xfilter = xpath_first(xn, "//filter")) != NULL){ if ((ftype = xml_find_value(xfilter, "type")) != NULL){ if (strcmp(ftype, "xpath") != 0){ xml_parse_va(xret, NULL, "" "operation-failed" "application" "error" "only xpath filter type supported" "type" ""); goto ok; } } } if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, &s) < 0) goto done; if (event_reg_fd(s, netconf_notification_cb, xfilter?xml_dup(xfilter):NULL, "notification socket") < 0) goto done; ok: retval = 0; done: return retval; } /*! See if there is any callback registered for this tag * * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at child of rpc: . * @param[out] xret Return XML, error or OK * * @retval -1 Error * @retval 0 OK, not found handler. * @retval 1 OK, handler called */ static int netconf_application_rpc(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; yang_spec *yspec = NULL; /* application yspec */ yang_stmt *yrpc = NULL; yang_stmt *yinput; yang_stmt *youtput; cxobj *xoutput; cbuf *cb = NULL; /* First check system / netconf RPC:s */ if ((cb = cbuf_new()) == NULL){ clicon_err(OE_UNIX, 0, "cbuf_new"); goto done; } /* Find yang rpc statement, return yang rpc statement if found Check application RPC */ if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec"); goto done; } cbuf_reset(cb); // if (xml_namespace(xn)) cprintf(cb, "/%s:%s", xml_namespace(xn), xml_name(xn)); // else // cprintf(cb, "/%s", xml_name(xn)); /* XXX not accepdted by below */ /* Find yang rpc statement, return yang rpc statement if found */ if (yang_abs_schema_nodeid(yspec, cbuf_get(cb), &yrpc) < 0) goto done; /* Check if found */ if (yrpc != NULL){ if ((yinput = yang_find((yang_node*)yrpc, Y_INPUT, NULL)) != NULL){ xml_spec_set(xn, yinput); /* needed for xml_spec_populate */ if (xml_apply(xn, CX_ELMNT, xml_spec_populate, yinput) < 0) goto done; if (xml_apply(xn, CX_ELMNT, (xml_applyfn_t*)xml_yang_validate_all, NULL) < 0) goto done; if (xml_yang_validate_add(xn, NULL) < 0) goto done; } /* * 1. Check xn arguments with input statement. * 2. Send to backend as clicon_msg-encode() * 3. In backend to similar but there call actual backend */ if (clicon_rpc_netconf_xml(h, xml_parent(xn), xret, NULL) < 0) goto done; /* Sanity check of outgoing XML */ if ((youtput = yang_find((yang_node*)yrpc, Y_OUTPUT, NULL)) != NULL){ xoutput=xpath_first(*xret, "/"); xml_spec_set(xoutput, youtput); /* needed for xml_spec_populate */ if (xml_apply(xoutput, CX_ELMNT, xml_spec_populate, youtput) < 0) goto done; if (xml_apply(xoutput, CX_ELMNT, (xml_applyfn_t*)xml_yang_validate_all, NULL) < 0) goto done; if (xml_yang_validate_add(xoutput, NULL) < 0) goto done; } retval = 1; /* handled by callback */ goto done; } retval = 0; done: if (cb) cbuf_free(cb); return retval; } /*! The central netconf rpc dispatcher. Look at first tag and dispach to sub-functions. * Call plugin handler if tag not found. If not handled by any handler, return * error. * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] xret Return XML, error or OK * @retval 0 OK, can also be netconf error * @retval -1 Error, fatal */ int netconf_rpc_dispatch(clicon_handle h, cxobj *xn, cxobj **xret) { int retval = -1; cxobj *xe; yang_spec *yspec = NULL; /* Check incoming RPC against system / netconf RPC:s */ if ((yspec = clicon_netconf_yang(h)) == NULL){ clicon_err(OE_YANG, ENOENT, "No netconf yang spec"); goto done; } xe = NULL; while ((xe = xml_child_each(xn, xe, CX_ELMNT)) != NULL) { if (strcmp(xml_name(xe), "get-config") == 0){ if (netconf_get_config(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "edit-config") == 0){ if (netconf_edit_config(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "copy-config") == 0){ if (netconf_copy_config(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "delete-config") == 0){ if (netconf_delete_config(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "lock") == 0) { if (netconf_lock(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "unlock") == 0){ if (netconf_unlock(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "get") == 0){ if (netconf_get(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "close-session") == 0){ if (netconf_close_session(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "kill-session") == 0) { if (netconf_kill_session(h, xe, xret) < 0) goto done; } /* Validate capability :validate */ else if (strcmp(xml_name(xe), "validate") == 0){ if (netconf_validate(h, xe, xret) < 0) goto done; } /* Candidate configuration capability :candidate */ else if (strcmp(xml_name(xe), "commit") == 0){ if (netconf_commit(h, xe, xret) < 0) goto done; } else if (strcmp(xml_name(xe), "discard-changes") == 0){ if (netconf_discard_changes(h, xe, xret) < 0) goto done; } /* RFC 5277 :notification */ else if (strcmp(xml_name(xe), "create-subscription") == 0){ if (netconf_create_subscription(h, xe, xret) < 0) goto done; } /* Others */ else { /* Look for local (client-side) netconf plugins. This feature may no * longer be necessary as generic RPC:s should be handled by backend. */ if ((retval = netconf_plugin_callbacks(h, xe, xret)) < 0) goto done; if (retval == 0) if ((retval = netconf_application_rpc(h, xe, xret)) < 0) goto done; if (retval == 0){ /* not handled by callback */ xml_parse_va(xret, NULL, "" "operation-failed" "rpc" "error" "%s" "Not recognized" "", xml_name(xe)); goto done; } } } retval = 0; done: return retval; }