/* * ***** 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 *****************************************************************************/ #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-config help function * xfilter is a filter expression starting with * only supported * needs refactoring: move the lower part (after xfilter) up to get-config or * sub-function., focus on xfilter part here. * @param[in] h Clicon handle * @param[in] xfilter Parsed xpath expression * @param[out] cb Output xml stream. For reply * @param[out] cb_err Error xml stream. For error reply * @param[in] xorig Original request as xml tree. * @param[in] target Database name * @see netconf_get_config * 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: ]]>]]> */ /*! Variant for xmldb */ static int netconf_filter_xmldb(clicon_handle h, cxobj *xfilter, enum filter_option foption, cbuf *cb, cbuf *cb_err, cxobj *xorig, char *source) { cxobj *xdb = NULL; cxobj *xc; cxobj *xfilterconf = NULL; int retval = -1; char *selector; /* Default subtree filter */ switch (foption){ case FILTER_SUBTREE: /* Get the whole database as xml */ if (xmldb_get(h, source, "/", &xdb, NULL, NULL) < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", NULL, "read-registry"); goto done; } /* Add under as reply */ if ((xc = xml_insert(xdb, "configuration")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", NULL, "filtering"); goto done; } if (xfilter) xfilterconf = xpath_first(xfilter, "//configuration"); /* xml_filter removes parts of xml tree not matching */ if (xfilterconf != NULL){ if (xml_filter(xfilterconf, xc) < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", NULL, "filtering"); goto done; } } if (xml_child_nr(xc)) clicon_xml2cbuf(cb, xc, 0, 1); break; case FILTER_XPATH: if ((selector = xml_find_value(xfilter, "select")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-attribute", "protocol", "error", NULL, "select"); goto done; } if (xmldb_get(h, source, selector, &xdb, NULL, NULL) < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", NULL, "read-registry"); goto done; } if ((xc = xml_insert(xdb, "configuration")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", NULL, "filtering"); goto done; } if (xml_child_nr(xc)) clicon_xml2cbuf(cb, xc, 0, 1); break; default: netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", "filter type not supported", "type"); goto done; } retval = 0; done: if (xdb) xml_free(xdb); return retval; } /*! Get configuration * @param[in] h Clicon handle * @param[in] xorig Sub-tree (under xorig) at ... level. * @param[out] cb Output xml stream. For reply * @param[out] cb_err Error xml stream. For error reply * @param[in] xorig Original request as xml tree. * @note Only filter type="xpath" is supported | | Example: ]]>]]> */ int netconf_get_config(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { cxobj *xfilter; /* filter */ int retval = -1; char *source; enum filter_option foption = FILTER_SUBTREE; char *ftype = NULL; if ((source = netconf_get_target(h, xn, "source")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "source"); goto done; } /* ie ... */ if ((xfilter = xpath_first(xn, "filter")) != NULL) { if ((ftype = xml_find_value(xfilter, "type")) != NULL) if (strcmp(ftype, "xpath")==0) foption = FILTER_XPATH; } if (netconf_filter_xmldb(h, xfilter, foption, cb, cb_err, xorig, source) < 0) goto done; retval = 0; done: return retval; } /*! Get options from netconf edit-config * * ... * (merge | none | replace) * (stop-on-error | continue-on-error ) * (set | test-then-set | test-only) * * * */ static int get_edit_opts(cxobj *xn, enum operation_type *op, enum test_option *testopt, enum error_option *erropt, cbuf *cb_err, cxobj *xorig) { int retval = -1; cxobj *x; char *optstr; if ((x = xpath_first(xn, "default-operation")) != NULL){ if ((optstr = xml_body(x)) != NULL){ if (strcmp(optstr, "replace") == 0) *op = OP_REPLACE; else if (strcmp(optstr, "merge") == 0) *op = OP_MERGE; else if (strcmp(optstr, "none") == 0) *op = OP_NONE; else{ netconf_create_rpc_error(cb_err, xorig, "invalid-value", "protocol", "error", NULL, NULL); goto done; } } } 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{ netconf_create_rpc_error(cb_err, xorig, "invalid-value", "protocol", "error", NULL, NULL); goto done; } } } 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{ netconf_create_rpc_error(cb_err, xorig, "invalid-value", "protocol", "error", NULL, NULL); goto done; } } } retval = 0; done: return retval; } /*! 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) * * only 'config' supported * error-option: only stop-on-error supported * test-option: not supported * * @note delete/remove not implemented * */ int netconf_edit_config(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { int retval = -1; int ret; enum operation_type operation = OP_MERGE; enum test_option testopt = TEST_THEN_SET; enum error_option erropt = STOP_ON_ERROR; cxobj *xc; /* config */ cxobj *xcc; /* child of config */ char *target; /* db */ cbuf *cbxml = NULL; char *xmlstr; /* must have target, and it should be candidate */ if ((target = netconf_get_target(h, xn, "target")) == NULL || strcmp(target, "candidate")){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "target"); goto done; } if (get_edit_opts(xn, &operation, &testopt, &erropt, cb_err, xorig) < 0) goto done; /* operation is OP_REPLACE, OP_MERGE, or OP_NONE pass all to backend */ if ((xc = xpath_first(xn, "config")) != NULL){ /* application-specific code registers 'config' */ if ((ret = netconf_plugin_callbacks(h, xc, cb, cb_err, xorig)) < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "protocol", "error", NULL, "Validation"); goto done; } if ((cbxml = cbuf_new()) == NULL) goto done; if ((xcc = xml_child_i(xc, 0)) != NULL) if (clicon_xml2cbuf(cbxml, xcc, 0, 0) < 0) goto done; xmlstr = cbuf_get(cbxml); if (clicon_rpc_xmlput(h, target, operation, "", /* api-path */ xmlstr) < 0){ netconf_create_rpc_error(cb_err, xorig, "access-denied", "protocol", "error", NULL, "edit_config"); goto done; } } netconf_ok_set(1); retval = 0; done: if (cbxml) cbuf_free(cbxml); unchunk_group(__FUNCTION__); return retval; } /* */ int netconf_copy_config(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { char *source, *target; /* filenames */ int retval = -1; if ((source = netconf_get_target(h, xn, "source")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "source"); goto done; } if ((target = netconf_get_target(h, xn, "target")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "source"); goto done; } #ifdef notyet /* If target is locked and not by this client, then return an error */ if (target_locked(&client) > 0 && client != ce_nr){ netconf_create_rpc_error(cb_err, xorig, "access-denied", "protocol", "error", NULL, "lock"); goto done; } #endif if (clicon_rpc_copy(h, source, target) < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "protocol", "error", NULL, "Internal error"); goto done; } netconf_ok_set(1); retval = 0; done: unchunk_group(__FUNCTION__); return retval; } /*! Delete configuration Delete a configuration datastore. The configuration datastore cannot be deleted. */ int netconf_delete_config(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { char *target; /* filenames */ int retval = -1; if ((target = netconf_get_target(h, xn, "target")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "target"); goto done; } if (strcmp(target, "candidate")){ netconf_create_rpc_error(cb_err, xorig, "bad-element", "protocol", "error", NULL, "target"); goto done; } if (clicon_rpc_change(h, "candidate", OP_REMOVE, "/", "") < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "protocol", "error", NULL, "Internal error"); goto done; } if (xmldb_init(h, target) < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "protocol", "error", NULL, "Internal error"); goto done; } netconf_ok_set(1); retval = 0; done: return retval; } /*! Close a (user) session */ int netconf_close_session(cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { cc_closed++; netconf_ok_set(1); return 0; } /*! Lock a database XXX */ int netconf_lock(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { char *target; int retval = -1; if ((target = netconf_get_target(h, xn, "target")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "source"); goto done; } netconf_ok_set(1); retval = 0; done: return retval; } /*! Unlock a database XXX */ int netconf_unlock(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { char *target; int retval = -1; if ((target = netconf_get_target(h, xn, "target")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "target"); goto done; } /* XXX notyet */ netconf_ok_set(1); retval = 0; done: return retval; } /*! Kill other user sessions PID XXX */ int netconf_kill_session(cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { #ifdef notyet cxobj *xsessionid; char *str, *ep; long i; if ((xsessionid = xpath_first(xn, "//session-id")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "session-id"); return -1; } if (xml_find_value(xsessionid, "body") == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "session-id"); return -1; } if ((str = xml_find_value(xsessionid, "body")) != NULL){ i = strtol(str, &ep, 0); if ((i == LONG_MIN || i == LONG_MAX) && errno) return -1; if ((ce = find_ce_bynr(i)) == NULL){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "protocol", "error", NULL, "No such client"); return -1; } } // ce_change_state(ce, CS_CLOSED, "Received close-session msg"); netconf_ok_set(1); #endif return 0; } /*! Commit candidate -> running :candidate */ int netconf_commit(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { int retval = -1; if (clicon_rpc_commit(h, "candidate", "running", 0, 0) < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "protocol", "error", NULL, "Internal error"); goto done; } netconf_ok_set(1); retval = 0; done: unchunk_group(__FUNCTION__); return retval; } /*! Discard all changes in candidate / revert to running :candidate */ int netconf_discard_changes(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { int retval = -1; if (clicon_rpc_copy(h, "running", "candidate") < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "protocol", "error", NULL, "Internal error"); goto done; } netconf_ok_set(1); retval = 0; done: unchunk_group(__FUNCTION__); return retval; } /*! Check the semantic consistency of candidate :validate */ int netconf_validate(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { char *target; int retval = -1; if ((target = netconf_get_target(h, xn, "source")) == NULL){ netconf_create_rpc_error(cb_err, xorig, "missing-element", "protocol", "error", NULL, "target"); goto done; } /* Do validation */ netconf_ok_set(1); retval = 0; done: return retval; } /* * netconf_notification_cb * 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; int eof; char *event = NULL; int level; int retval = -1; cbuf *cb; cxobj *xe = NULL; /* event xml */ enum clicon_msg_type type; 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, __FUNCTION__) < 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; } /* multiplex on message type: we only expect notify */ type = ntohs(reply->op_type); switch (type){ case CLICON_MSG_NOTIFY: if (clicon_msg_notify_decode(reply, &level, &event, __FUNCTION__) < 0) goto done; /* parse event */ if (0){ /* XXX CLICON events are not xml */ if (clicon_xml_parse_string(&event, &xe) < 0) goto done; /* 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 */ break; } } /* 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, ""); cprintf(cb, ""); cprintf(cb, "%s", event); cprintf(cb, ""); cprintf(cb, ""); add_postamble(cb); /* Send it to listening client on stdout */ if (netconf_output(1, cb, "notification") < 0){ cbuf_free(cb); goto done; } cbuf_free(cb); break; default: clicon_err(OE_PROTO, 0, "%s: unexpected reply: %d", __FUNCTION__, type); goto done; break; } retval = 0; done: if (xe != NULL) xml_free(xe); // if (event) // free(event); unchunk_group(__FUNCTION__); 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 */ static int netconf_create_subscription(clicon_handle h, cxobj *xn, cbuf *cb, cbuf *cb_err, cxobj *xorig) { cxobj *xstream; cxobj *xfilter; cxobj *xfilter2 = NULL; char *stream = NULL; int s; char *ftype; int retval = -1; if ((xstream = xpath_first(xn, "//stream")) != NULL) stream = xml_find_value(xstream, "body"); if (stream == NULL) stream = "NETCONF"; if ((xfilter = xpath_first(xn, "//filter")) != NULL){ if ((ftype = xml_find_value(xfilter, "type")) != NULL){ if (strcmp(ftype, "xpath") != 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", "only xpath filter type supported", "type"); goto done; } } /* xfilter2 is applied to netconf_notification_cb below and is only freed on exit since no unreg is made. */ if ((xfilter2 = xml_dup(xfilter)) == NULL){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", NULL, "Internal server error"); goto done; } } if (clicon_rpc_subscription(h, 1, stream, MSG_NOTIFY_XML, "", &s) < 0){ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "application", "error", NULL, "Internal protocol error"); goto done; } if (event_reg_fd(s, netconf_notification_cb, xfilter2, "notification socket") < 0) goto done; netconf_ok_set(1); retval = 0; done: 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] xorig Original request as xml tree. * @param[in] xn Sub-tree (under xorig) at ... level. * @param[out] cb Output xml stream. For reply * @param[out] cb_err Error xml stream. For error reply */ int netconf_rpc_dispatch(clicon_handle h, cxobj *xorig, cxobj *xn, cbuf *cb, cbuf *cb_err) { cxobj *xe; int ret = 0; xe = NULL; while ((xe = xml_child_each(xn, xe, CX_ELMNT)) != NULL) { if (strcmp(xml_name(xe), "close-session") == 0) return netconf_close_session(xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "get-config") == 0) return netconf_get_config(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "edit-config") == 0) return netconf_edit_config(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "copy-config") == 0) return netconf_copy_config(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "delete-config") == 0) return netconf_delete_config(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "kill-session") == 0) /* TBD */ return netconf_kill_session(xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "lock") == 0) /* TBD */ return netconf_lock(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "unlock") == 0) /* TBD */ return netconf_unlock(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "commit") == 0) return netconf_commit(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "discard-changes") == 0) return netconf_discard_changes(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "validate") == 0) return netconf_validate(h, xe, cb, cb_err, xorig); else if (strcmp(xml_name(xe), "create-subscription") == 0) return netconf_create_subscription(h, xe, cb, cb_err, xorig); else{ if ((ret = netconf_plugin_callbacks(h, xe, cb, cb_err, xorig)) < 0) return -1; if (ret == 0){ /* not handled by callback */ netconf_create_rpc_error(cb_err, xorig, "operation-failed", "rpc", "error", xml_name(xe), "Not recognized"); ret = -1; } } } return ret; } /* * netconf_create_rpc_reply * Create an rpc reply msg in cb * Then send it using send_msg_to_client() */ int netconf_create_rpc_reply(cbuf *cb, /* msg buffer */ cxobj *xr, /* orig request */ char *body, int ok ) { cxobj *xn, *xa; add_preamble(cb); cprintf(cb, ""); if (ok) /* Just _maybe_ we should send data instead of ok if (if there is any) even if ok is set? */ cprintf(cb, ""); else{ cprintf(cb, ""); cprintf(cb, "%s", body); cprintf(cb, ""); } cprintf(cb, ""); add_postamble(cb); return 0; } /* * netconf_create_rpc_error * Create an rpc error msg in cb * (Then it should be sent at a later stage using send_msg_to_client() ) * Arguments: * cb msg buffer to construct netconf message in * xr original request including an "rpc" tag * ... arguments to rpc-error reply message */ int netconf_create_rpc_error(cbuf *cb, /* msg buffer */ cxobj *xr, /* orig request */ char *tag, char *type, char *severity, char *message, char *info) { add_error_preamble(cb, tag); cprintf(cb, ""); cprintf(cb, ""); if (tag) cprintf(cb, "%s", tag); cprintf(cb, "%s", type); cprintf(cb, "%s", severity); if (message) cprintf(cb, "%s", message); if (info) cprintf(cb, "%s", info); cprintf(cb, ""); cprintf(cb, ""); add_error_postamble(cb); return 0; }