/* * ***** BEGIN LICENSE BLOCK ***** Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren Copyright (C) 2017-2019 Olof Hagsand Copyright (C) 2020-2022 Olof Hagsand and Rubicon Communications, LLC(Netgate) 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 ***** * The example have the following optional arguments that you can pass as * argc/argv after -- in clixon_backend: * -a <..> Register callback for this yang action * -r enable the reset function * -s enable the state function * -S read state data from file, otherwise construct it programmatically (requires -s) * -i read state file on init not by request for optimization (requires -sS ) * -u enable upgrade function - auto-upgrade testing * -U general-purpose upgrade * -t enable transaction logging (call syslog for every transaction) * -v Failing validate and commit if is present (synthetic error) */ #include #include #include #include #include #include #include #include #include #include #include /* clicon */ #include /* Clicon library functions. */ #include /* These include signatures for plugin and transaction callbacks. */ #include /* Command line options to be passed to getopt(3) */ #define BACKEND_EXAMPLE_OPTS "a:rsS:x:iuUtv:" /*! Yang action * Start backend with -- -a * where instance-id points to an action node in some YANG * Hard-coded to action "reset" from RFC7950 7.15 */ static char *_action_instanceid = NULL; /*! Variable to control if reset code is run. * The reset code inserts "extra XML" which assumes ietf-interfaces is * loaded, and this is not always the case. * Start backend with -- -r */ static int _reset = 0; /*! Variable to control if state code is run * The state code adds extra non-config data * Start backend with -- -s */ static int _state = 0; /*! File where state XML is read from, if _state is true -- -sS * Primarily for testing * Start backend with -- -sS */ static char *_state_file = NULL; /*! XPath to register for pagination state XML from file, * if _state is true -- -sS -x * Primarily for testing * Start backend with -- -sS -x */ static char *_state_xpath = NULL; /*! Read state file init on startup instead of on request * Primarily for testing: -i * Start backend with -- -siS */ static int _state_file_cached = 0; /*! Cache control of read state file pagination example, * keep xml tree cache as long as db is locked */ static cxobj *_state_xml_cache = NULL; /* XML cache */ static int _state_file_transaction = 0; /*! Variable to control module-specific upgrade callbacks. * If set, call test-case for upgrading ietf-interfaces, otherwise call * auto-upgrade * Start backend with -- -u */ static int _module_upgrade = 0; /*! Variable to control general-purpose upgrade callbacks. * Start backend with -- -U */ static int _general_upgrade = 0; /*! Variable to control transaction logging (for debug) * If set, call syslog for every transaction callback * Start backend with -- -t */ static int _transaction_log = 0; /* forward */ static int example_stream_timer_setup(clicon_handle h); int main_begin(clicon_handle h, transaction_data td) { if (_transaction_log) transaction_log(h, td, LOG_NOTICE, __FUNCTION__); return 0; } /*! This is called on validate (and commit). Check validity of candidate */ int main_validate(clicon_handle h, transaction_data td) { if (_transaction_log) transaction_log(h, td, LOG_NOTICE, __FUNCTION__); return 0; } int main_complete(clicon_handle h, transaction_data td) { if (_transaction_log) transaction_log(h, td, LOG_NOTICE, __FUNCTION__); return 0; } /*! This is called on commit. Identify modifications and adjust machine state */ int main_commit(clicon_handle h, transaction_data td) { cxobj *target = transaction_target(td); /* wanted XML tree */ cxobj **vec = NULL; int i; int len; cvec *nsc = NULL; if (_transaction_log) transaction_log(h, td, LOG_NOTICE, __FUNCTION__); /* Create namespace context for xpath */ if ((nsc = xml_nsctx_init(NULL, "urn:ietf:params:xml:ns:yang:ietf-interfaces")) == NULL) goto done; /* Get all added i/fs */ if (xpath_vec_flag(target, nsc, "//interface", XML_FLAG_ADD, &vec, &len) < 0) return -1; if (clicon_debug_get()) for (i=0; ifaultEthernet0major") < 0) goto done; if (example_stream_timer_setup(h) < 0) goto done; retval = 0; done: return retval; } /*! Set up example stream notification timer */ static int example_stream_timer_setup(clicon_handle h) { struct timeval t, t1; gettimeofday(&t, NULL); t1.tv_sec = 5; t1.tv_usec = 0; timeradd(&t, &t1, &t); return clixon_event_reg_timeout(t, example_stream_timer, h, "example stream timer"); } /*! Smallest possible RPC declaration for test * Yang/XML: * If the RPC operation invocation succeeded and no output parameters * are returned, the contains a single element defined * in [RFC6241]. */ static int empty_rpc(clicon_handle h, /* Clicon handle */ cxobj *xe, /* Request: */ cbuf *cbret, /* Reply eg ... */ void *arg, /* client_entry */ void *regarg) /* Argument given at register */ { cprintf(cbret, "", NETCONF_BASE_NAMESPACE); return 0; } /*! More elaborate example RPC for testing * The RPC returns the incoming parameters */ static int example_rpc(clicon_handle h, /* Clicon handle */ cxobj *xe, /* Request: */ cbuf *cbret, /* Reply eg ... */ void *arg, /* client_entry */ void *regarg) /* Argument given at register */ { int retval = -1; cxobj *x = NULL; cxobj *xp; char *namespace; char *msgid; /* get namespace from rpc name, return back in each output parameter */ if ((namespace = xml_find_type_value(xe, NULL, "xmlns", CX_ATTR)) == NULL){ clicon_err(OE_XML, ENOENT, "No namespace given in rpc %s", xml_name(xe)); goto done; } cprintf(cbret, ""); if (!xml_child_nr_type(xe, CX_ELMNT)) cprintf(cbret, ""); else { while ((x = xml_child_each(xe, x, CX_ELMNT)) != NULL) { if (xmlns_set(x, NULL, namespace) < 0) goto done; } if (clixon_xml2cbuf(cbret, xe, 0, 0, -1, 1) < 0) goto done; } cprintf(cbret, ""); retval = 0; done: return retval; } /*! This will be called as a hook right after the original system copy-config */ static int example_copy_extra(clicon_handle h, /* Clicon handle */ cxobj *xe, /* Request: */ cbuf *cbret, /* Reply eg ... */ void *arg, /* client_entry */ void *regarg) /* Argument given at register */ { int retval = -1; // fprintf(stderr, "%s\n", __FUNCTION__); retval = 0; // done: return retval; } /*! Action callback, example from RFC7950 7.15 * Note callback is hardcoded C, while registration is controlled by -- -a option */ static int example_action_reset(clicon_handle h, /* Clicon handle */ cxobj *xe, /* Request: */ cbuf *cbret, /* Reply eg ... */ void *arg, /* client_entry */ void *regarg) /* Argument given at register */ { int retval = -1; char *reset_at; if ((reset_at = xml_find_body(xe, "reset-at")) != NULL) /* Just copy input to output */ cprintf(cbret, "%s", NETCONF_BASE_NAMESPACE, reset_at); retval = 0; // done: return retval; } /*! Called to get state data from plugin by programmatically adding state * * @param[in] h Clicon handle * @param[in] nsc External XML namespace context, or NULL * @param[in] xpath String with XPATH syntax. or NULL for all * @param[out] xstate XML tree, on entry. * @retval 0 OK * @retval -1 Error * @see xmldb_get * @note this example code returns requires this yang snippet: container state { config false; description "state data for example application"; leaf-list op { type string; } } * This yang snippet is present in clixon-example.yang for example. * @see example_statefile where state is read from file and also pagination */ int example_statedata(clicon_handle h, cvec *nsc, char *xpath, cxobj *xstate) { int retval = -1; cxobj **xvec = NULL; size_t xlen = 0; cbuf *cb = cbuf_new(); int i; cxobj *xt = NULL; char *name; cvec *nsc1 = NULL; yang_stmt *yspec = NULL; if (!_state) goto ok; yspec = clicon_dbspec_yang(h); /* Example of statedata, in this case merging state data with * state information. In this case adding dummy interface operation state * to configured interfaces. * Get config according to xpath */ if ((nsc1 = xml_nsctx_init(NULL, "urn:ietf:params:xml:ns:yang:ietf-interfaces")) == NULL) goto done; if (xmldb_get0(h, "running", YB_MODULE, nsc1, "/interfaces/interface/name", 1, &xt, NULL, NULL) < 0) goto done; if (xpath_vec(xt, nsc1, "/interfaces/interface/name", &xvec, &xlen) < 0) goto done; if (xlen){ cprintf(cb, ""); for (i=0; i%sex:ethup", name); cprintf(cb, "42foo"); cprintf(cb, ""); } cprintf(cb, ""); if (clixon_xml_parse_string(cbuf_get(cb), YB_NONE, NULL, &xstate, NULL) < 0) goto done; } /* State in test_yang.sh , test_restconf.sh and test_order.sh */ if (yang_find_module_by_namespace(yspec, "urn:example:clixon") != NULL){ if (clixon_xml_parse_string("" "42" "41" "43" /* should not be ordered */ "", YB_NONE, NULL, &xstate, NULL) < 0) goto done; /* For the case when urn:example:clixon is not loaded */ } /* Event state from RFC8040 Appendix B.3.1 * Note: (1) order is by-system so is different, * (2) event-count is XOR on name, so is not 42 and 4 */ if (yang_find_module_by_namespace(yspec, "urn:example:events") != NULL){ cbuf_reset(cb); cprintf(cb, ""); cprintf(cb, "interface-down90"); cprintf(cb, "interface-up77"); cprintf(cb, ""); if (clixon_xml_parse_string(cbuf_get(cb), YB_NONE, NULL, &xstate, NULL) < 0) goto done; } ok: retval = 0; done: if (nsc1) xml_nsctx_free(nsc1); if (xt) xml_free(xt); if (cb) cbuf_free(cb); if (xvec) free(xvec); return retval; } /*! Called to get state data from plugin by reading a file, also pagination * * The example shows how to read and parse a state XML file, (which is cached in the -i case). * Return the requested xpath / pagination xstate by copying from the parsed state XML file * @param[in] h Clicon handle * @param[in] nsc External XML namespace context, or NULL * @param[in] xpath String with XPATH syntax. or NULL for all * @param[out] xstate XML tree, on entry. Copy to this * @retval 0 OK * @retval -1 Error * @see xmldb_get * @see example_statefile where state is programmatically added */ int example_statefile(clicon_handle h, cvec *nsc, char *xpath, cxobj *xstate) { int retval = -1; cxobj **xvec = NULL; size_t xlen = 0; int i; cxobj *xt = NULL; yang_stmt *yspec = NULL; FILE *fp = NULL; cxobj *x1; int ret; /* If -S is set, then read state data from file */ if (!_state || !_state_file) goto ok; yspec = clicon_dbspec_yang(h); /* Read state file if either not cached, or the cache is NULL */ if (_state_file_cached == 0 || _state_xml_cache == NULL){ if ((fp = fopen(_state_file, "r")) == NULL){ clicon_err(OE_UNIX, errno, "open(%s)", _state_file); goto done; } if ((xt = xml_new("config", NULL, CX_ELMNT)) == NULL) goto done; if ((ret = clixon_xml_parse_file(fp, YB_MODULE, yspec, &xt, NULL)) < 0) goto done; if (_state_file_cached) _state_xml_cache = xt; } if (_state_file_cached) xt = _state_xml_cache; if (xpath_vec(xt, nsc, "%s", &xvec, &xlen, xpath) < 0) goto done; /* Mark elements to copy: * For every node found in x0, mark the tree as changed */ for (i=0; i xlen) upper = xlen; } /* Mark elements to copy: * For every node found in x0, mark the tree as changed */ for (i=lower; imd_status) /* skip if there is proper module-state in datastore */ goto ok; yspec = clicon_dbspec_yang(h); /* Get all yangs */ /* Get canonical namespaces for using "normalized" prefixes */ if (xml_nsctx_yangspec(yspec, &nsc) < 0) goto done; /* 1. Remove paths */ for (pp = remove_map; *pp; ++pp){ /* Find all nodes matching n */ if (xpath_vec(xt, nsc, "%s", &xvec, &xlen, *pp) < 0) goto done; /* Remove them */ /* Loop through all nodes matching mypath and change theoir namespace */ for (i=0; ims_s0; ms++){ char *mypath; char *mynamespace; char *myprefix = NULL; mypath = ms->ms_s0; mynamespace = ms->ms_s1; if (xml_nsctx_get_prefix(nsc, mynamespace, &myprefix) == 0){ clicon_err(OE_XML, ENOENT, "Namespace %s not found in canonical namespace map", mynamespace); goto done; } /* Find all nodes matching mypath */ if (xpath_vec(xt, nsc, "%s", &xvec, &xlen, mypath) < 0) goto done; /* Loop through all nodes matching mypath and change theoir namespace */ for (i=0; i..., ..., ..., " "loex:loopback" "", YB_MODULE, yspec, &xt, NULL) < 0) goto done; /* Replace parent w first child */ if (xml_rootchild(xt, 0, &xt) < 0) goto done; if ((cbret = cbuf_new()) == NULL){ clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } /* Merge user reset state */ if ((ret = xmldb_put(h, (char*)db, OP_MERGE, xt, clicon_username_get(h), cbret)) < 0) goto done; if (ret == 0){ clicon_err(OE_XML, 0, "Error when writing to XML database: %s", cbuf_get(cbret)); goto done; } ok: retval = 0; done: if (cbret) cbuf_free(cbret); if (xt != NULL) xml_free(xt); return retval; } /*! Plugin start. * * Called when application is "started", (almost) all initialization is complete * Backend: daemon is in the background. If daemon privileges are dropped * this callback is called *before* privileges are dropped. * @param[in] h Clicon handle */ int example_start(clicon_handle h) { int retval = -1; yang_stmt *yspec; yang_stmt *ya = NULL; /* Register action callback, example from RFC7950 7.15 * Can not be made in _init since YANG is not loaded * Note that callback is hardcoded here since it is C, but YANG and name of action * is not. It is enough to point via an schema-node id to the correct action, * such as "/sfarm:server/sfarm:reset" */ if (_action_instanceid){ if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } if (yang_abs_schema_nodeid(yspec, _action_instanceid, &ya) == 0){ if (ya && action_callback_register(h, ya, example_action_reset, NULL) < 0) goto done; } } retval = 0; done: return retval; } /*! Plugin daemon. * @param[in] h Clicon handle * * plugin_daemon is called once after daemonization has been made but before lowering of privileges * the main event loop is entered. */ int example_daemon(clicon_handle h) { int retval = -1; int ret; FILE *fp = NULL; yang_stmt *yspec; cxobj *xerr = NULL; /* Read state file (or should this be in init/start?) */ if (_state && _state_file && _state_file_cached){ yspec = clicon_dbspec_yang(h); if ((fp = fopen(_state_file, "r")) == NULL){ clicon_err(OE_UNIX, errno, "open(%s)", _state_file); goto done; } /* Need to be yang bound for eg xml_copy_marked() in example_pagination */ if ((ret = clixon_xml_parse_file(fp, YB_MODULE, yspec, &_state_xml_cache, &xerr)) < 0) goto done; if (ret == 0){ xml_print(stderr, xerr); goto done; } } retval = 0; done: if (fp) fclose(fp); return retval; } int example_exit(clicon_handle h) { if (_state_xml_cache){ xml_free(_state_xml_cache); _state_xml_cache = NULL; } return 0; } /* Forward declaration */ clixon_plugin_api *clixon_plugin_init(clicon_handle h); static clixon_plugin_api api = { "example", /* name */ clixon_plugin_init, /* init - must be called clixon_plugin_init */ example_start, /* start */ example_exit, /* exit */ example_extension, /* yang extensions */ .ca_daemon=example_daemon, /* daemon */ .ca_reset=example_reset, /* reset */ .ca_statedata=example_statedata, /* statedata : Note fn is switched if -sS */ .ca_lockdb=example_lockdb, /* Database lock changed state */ .ca_trans_begin=main_begin, /* trans begin */ .ca_trans_validate=main_validate, /* trans validate */ .ca_trans_complete=main_complete, /* trans complete */ .ca_trans_commit=main_commit, /* trans commit */ .ca_trans_commit_done=main_commit_done, /* trans commit done */ .ca_trans_revert=main_revert, /* trans revert */ .ca_trans_end=main_end, /* trans end */ .ca_trans_abort=main_abort, /* trans abort */ .ca_datastore_upgrade=example_upgrade, /* general-purpose upgrade. */ }; /*! Backend plugin initialization * @param[in] h Clixon handle * @retval NULL Error with clicon_err set * @retval api Pointer to API struct * In this example, you can pass -r, -s, -u to control the behaviour, mainly * for use in the test suites. */ clixon_plugin_api * clixon_plugin_init(clicon_handle h) { struct timeval retention = {0,0}; int argc; /* command-line options (after --) */ char **argv; int c; clicon_debug(1, "%s backend", __FUNCTION__); /* Get user command-line options (after --) */ if (clicon_argv_get(h, &argc, &argv) < 0) goto done; opterr = 0; optind = 1; while ((c = getopt(argc, argv, BACKEND_EXAMPLE_OPTS)) != -1) switch (c) { case 'a': _action_instanceid = optarg; break; case 'r': _reset = 1; break; case 's': /* state callback */ _state = 1; break; case 'S': /* state file (requires -s) */ _state_file = optarg; break; case 'x': /* state xpath (requires -sS) */ _state_xpath = optarg; break; case 'i': /* read state file on init not by request (requires -sS */ _state_file_cached = 1; break; case 'u': /* module-specific upgrade */ _module_upgrade = 1; break; case 'U': /* general-purpose upgrade */ _general_upgrade = 1; break; case 't': /* transaction log */ _transaction_log = 1; break; } if (_state_file){ api.ca_statedata = example_statefile; /* Switch state data callback */ if (_state_xpath){ /* State pagination callbacks */ if (clixon_pagination_cb_register(h, example_pagination, _state_xpath, NULL) < 0) goto done; } } /* Example stream initialization: * 1) Register EXAMPLE stream * 2) setup timer for notifications, so something happens on stream * 3) setup stream callbacks for notification to push channel */ if (clicon_option_exists(h, "CLICON_STREAM_RETENTION")) retention.tv_sec = clicon_option_int(h, "CLICON_STREAM_RETENTION"); if (stream_add(h, "EXAMPLE", "Example event stream", 1, &retention) < 0) goto done; /* Enable nchan pub/sub streams * assumes: CLIXON_PUBLISH_STREAMS, eg configure --enable-publish */ if (clicon_option_exists(h, "CLICON_STREAM_PUB") && stream_publish(h, "EXAMPLE") < 0) goto done; if (example_stream_timer_setup(h) < 0) goto done; /* Register callback for routing rpc calls */ /* From example.yang (clicon) */ if (rpc_callback_register(h, empty_rpc, NULL, "urn:example:clixon", "empty"/* Xml tag when callback is made */ ) < 0) goto done; /* Same as example but with optional input/output */ if (rpc_callback_register(h, example_rpc, NULL, "urn:example:clixon", "optional"/* Xml tag when callback is made */ ) < 0) goto done; /* Same as example but with optional input/output */ if (rpc_callback_register(h, example_rpc, NULL, "urn:example:clixon", "example"/* Xml tag when callback is made */ ) < 0) goto done; /* Called before the regular system copy_config callback * If you want to have it called _after_ the system callback, place this call in * the _start function. */ if (rpc_callback_register(h, example_copy_extra, NULL, NETCONF_BASE_NAMESPACE, "copy-config" ) < 0) goto done; /* Upgrade callback: if you start the backend with -- -u you will get the * test interface example. Otherwise the auto-upgrade feature is enabled. */ if (_module_upgrade){ if (upgrade_callback_register(h, upgrade_interfaces, "urn:example:interfaces", NULL) < 0) goto done; } else if (upgrade_callback_register(h, xml_changelog_upgrade, NULL, NULL) < 0) goto done; /* Return plugin API */ return &api; done: return NULL; }