diff --git a/hello/src/Makefile.in b/hello/src/Makefile.in index 9b2d27d..f6cc0c9 100644 --- a/hello/src/Makefile.in +++ b/hello/src/Makefile.in @@ -55,11 +55,21 @@ CPPFLAGS = @CPPFLAGS@ -fPIC YANG_INSTALLDIR = $(datadir)/clixon/$(APPNAME) # Local yang files to install -YANGSPECS = clixon-hello@2019-04-17.yang +YANGSPECS = clixon-hello@2024-05-25.yang .PHONY: all clean depend install -all: +# Example backend plugin +# There may also be restconf/cli/netconf plugins which are not covered here, see +# eg clixon main example +PLUGIN = $(APPNAME)_backend_plugin.so +PLUGIN_SRC = $(APPNAME)_backend_plugin.c +PLUGIN_OBJ = $(PLUGIN_SRC:%.c=%.o) + +all: $(PLUGIN) + +$(PLUGIN): $(PLUGIN_OBJ) + $(CC) -Wall -shared $(LDFLAGS) -o $@ -lc $< -lclixon -lclixon_backend .SUFFIXES: .c .o @@ -70,23 +80,26 @@ all: CLISPECS = $(APPNAME)_cli.cli clean: + rm -f $(PLUGIN) $(PLUGIN_OBJ) distclean: clean rm -f Makefile *~ .depend -install: $(CLISPECS) $(APPNAME).xml autocli.xml +install: $(CLISPECS) $(APPNAME).xml autocli.xml $(PLUGIN) install -d -m 0755 $(DESTDIR)$(sysconfdir)/clixon install -d -m 0755 $(DESTDIR)$(sysconfdir)/clixon/$(APPNAME) install -m 0644 $(APPNAME).xml $(DESTDIR)$(sysconfdir)/clixon install -m 0644 autocli.xml $(DESTDIR)$(sysconfdir)/clixon/$(APPNAME) install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME) install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME)/clispec + install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME)/backend install -m 0644 $(CLISPECS) $(DESTDIR)$(libdir)/$(APPNAME)/clispec install -d -m 0755 $(DESTDIR)$(localstatedir) install -d -m 0755 $(DESTDIR)$(localstatedir)/$(APPNAME) install -m 0644 startup_db $(DESTDIR)$(localstatedir)/$(APPNAME)/ install -d -m 0755 $(DESTDIR)$(YANG_INSTALLDIR) install -m 0644 $(YANGSPECS) $(DESTDIR)$(YANG_INSTALLDIR) + install -m 0644 $(INSTALLFLAGS) $(PLUGIN) $(DESTDIR)$(libdir)/$(APPNAME)/backend uninstall: rm -f $(DESTDIR)$(sysconfdir)/clixon/$(APPNAME).xml diff --git a/hello/src/clixon-hello@2019-04-17.yang b/hello/src/clixon-hello@2019-04-17.yang deleted file mode 100644 index 89c30ac..0000000 --- a/hello/src/clixon-hello@2019-04-17.yang +++ /dev/null @@ -1,14 +0,0 @@ -module clixon-hello { - yang-version 1.1; - namespace "urn:example:hello"; - prefix he; - revision 2019-04-17 { - description - "Clixon hello world example"; - } - container hello{ - container world{ - presence true; - } - } -} diff --git a/hello/src/clixon-hello@2024-05-25.yang b/hello/src/clixon-hello@2024-05-25.yang new file mode 100644 index 0000000..ee5ebef --- /dev/null +++ b/hello/src/clixon-hello@2024-05-25.yang @@ -0,0 +1,24 @@ +module clixon-hello { + yang-version 1.1; + namespace "urn:example:hello"; + prefix he; + revision 2024-05-25 { + description + "Converted to an enumeration"; + } + revision 2019-04-17 { + description + "Clixon hello world example"; + } + container hello { + leaf to { + type enumeration { + enum city; + enum state; + enum country; + enum world; + } + description "To whom are we saying hello?"; + } + } +} diff --git a/hello/src/hello.xml.in b/hello/src/hello.xml.in index 6e129df..9851d49 100644 --- a/hello/src/hello.xml.in +++ b/hello/src/hello.xml.in @@ -7,6 +7,7 @@ clixon-hello hello @LIBDIR@/hello/clispec + @LIBDIR@/hello/backend @LOCALSTATEDIR@/hello/hello.sock @LOCALSTATEDIR@/hello/hello.pidfile diff --git a/hello/src/hello_backend_plugin.c b/hello/src/hello_backend_plugin.c new file mode 100644 index 0000000..bd4b2a0 --- /dev/null +++ b/hello/src/hello_backend_plugin.c @@ -0,0 +1,491 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2021-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 ***** + */ + +#include +#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 + +/* + * This file reflects the "system state" of hello world. If it + * exists, hello world is set. If it does not exit, then hello world + * is not set. If it exists, it contains the "to" value. This is + * what is set in the commit call and fetched in the statedata call. + */ +#define WORLD_FILE "/tmp/world" + +#define HELLO_NAMESPACE "urn:example:hello" + +/* + * Set this to 1 to print out the XML data received in validate. + * Only really useful if you run clixon_backend in foreground (-F). + */ +#define DEBUG_XML_STRINGS 0 + +/* + * The operation to perform on the data. This is allocated in the + * begin function, freed in the end function, set in the validate + * function, and implemented in the commit function. + */ +struct hello_data { + char to[10]; /* On HELLO_ADD, the value to put into WORLD_FILE. */ + enum { + HELLO_NOP, /* Don't do anything. */ + HELLO_ADD, /* Create or update the WORLD_FILE. */ + HELLO_DEL /* Delete the WORLD_FILE. */ + } op; +}; + +/* + * Allocate a structure for us to store the results of validation in. + * This is so we don't have to parse again in the commit. + */ +static int +hello_begin(clicon_handle h, transaction_data td) { + struct hello_data *data; + int rv; + + clixon_debug(CLIXON_DBG_DEFAULT, "Entry\n"); + data = malloc(sizeof(*data)); + if (!data) { + clixon_err(OE_XML, 0, "Could not allocate memory"); + return -1; + } + data->op = HELLO_NOP; + memset(data->to, 0, sizeof(data->to)); + rv = transaction_arg_set(td, data); + if (rv) { + free(data); + return rv; + } + return 0; +} + +/* + * Free the data for the transaction. + */ +static int +hello_end(clicon_handle h, transaction_data td) { + struct hello_data *data = transaction_arg(td); + + clixon_debug(CLIXON_DBG_DEFAULT, "Entry\n"); + free(data); + return 0; +} + +static const char *valid_tos[] = { "city", "state", "country", "world", NULL }; + +/* + * Validate that the current XML object is of the form: + * + * world + * + * Returns the contents of "to" in str. + * + * Return value is 0 for not found, 1 for found, and -1 for error. + */ +static int +find_hello_to(cxobj *vec, const char **str) +{ + bool to_found = false; + cxobj *c = NULL; + int i; + char *s, *ns; + + if (strcmp(xml_name(vec), "hello") != 0) + return 0; + + /* + * Get the namespace of this object. You can't use xml2ns() here, + * it will search up the tree for a namespace, and and possible + * return the default namespace. We want it for this object only. + */ + ns = xml_find_type_value(vec, NULL, "xmlns", CX_ATTR); + if (!ns || strcmp(ns, HELLO_NAMESPACE) != 0) + return 0; + + while ((c = xml_child_each(vec, c, CX_ELMNT))) { + if (strcmp(xml_name(c), "to") != 0) { + clixon_err(OE_XML, 0, "Non-\"to\" in hello vec: %s", + xml_name(c)); + return -1; + } + if (to_found) { + clixon_err(OE_XML, 0, "Multiple \"to\" in hello vec"); + return -1; + } + + s = xml_body(c); + if (!s) { + clixon_err(OE_XML, 0, "The \"to\" element doesn't have a value"); + return -1; + } + + for (i = 0; valid_tos[i]; i++) { + if (strcmp(valid_tos[i], s) == 0) + break; + } + if (!valid_tos[i]) { + clixon_err(OE_XML, 0, "Invalid \"to\" element: %s", s); + return -1; + } + + *str = valid_tos[i]; + to_found = true; + } + + if (to_found) + return 1; + return 0; +} + +#if DEBUG_XML_STRINGS +static const char *xml_flag_strs[] = { + "mark", + "transient", + "add", + "del", + "change", + "none", + "default", + "top", + "bodykey", + "anydata", + NULL +}; + +static void +print_xml_flags(FILE *f, uint16_t flags) +{ + unsigned int i; + + for (i = 0; xml_flag_strs[i]; i++) { + if (flags & 1 << i) + fprintf(f, " %s", xml_flag_strs[i]); + } +} + +static void +print_xml(FILE *f, int indent, cxobj *x) +{ + char *name, *value, *prefix, *s; + uint16_t flags; + cxobj *c; + unsigned int i; + + if (!x) + return; + name = xml_name(x); + value = xml_value(x); + prefix = xml_prefix(x); + flags = xml_flag(x, 0xffff); + for (i = 0; i < indent; i++) + fputc(' ', f); + fputs(name, f); + if (value) { + fputc('=', f); + fputs(value, f); + } + if (prefix) { + fputc('(', f); + fputs(prefix, f); + fputc(')', f); + } + print_xml_flags(f, flags); + + s = xml_body(x); + if (s) { + fprintf(f, ": %s\n", s); + } else { + fputc('\n', f); + for (i = 0; (c = xml_child_i(x, i)); i++) + print_xml(f, indent + 2, c); + } +} +#endif + +/* + * There are two basic ways to process the data given to validate (and + * commit). This define chooses which. Setting to 1 causes validate + * to go through the add/delete/changed xml tree and handle those. + * Setting to 0 caused validate to directly process the source (the + * old values) and target (the new values) trees for values that are + * flagged ADD, DELETE, or CHANGED. + */ +#define PROCESS_VECS 0 + +/* + * Validate the XML. If we find that the WORLD_FILE needs to change, + * we modify the hello_data struct to reflect what needs to be done. + */ +static int +hello_validate(clicon_handle h, transaction_data td) { + struct hello_data *data = transaction_arg(td); + int rv; + const char *place; +#if PROCESS_VECS + cxobj **vec; + size_t i, len; +#else + cxobj *x, *xt; +#endif + + clixon_debug(CLIXON_DBG_DEFAULT, "Entry\n"); +#if DEBUG_XML_STRINGS + clixon_debug(CLIXON_DBG_DEFAULT, "src:\n"); + print_xml(stdout, 2, transaction_src(td)); + clixon_debug(CLIXON_DBG_DEFAULT, "target:\n"); + print_xml(stdout, 2, transaction_target(td)); + clixon_debug(CLIXON_DBG_DEFAULT, "transaction:\n"); + transaction_print(stdout, td); +#endif + +#if PROCESS_VECS + /* First look through dvec to see if we need to delete WORLD_FILE. */ + vec = transaction_dvec(td); + len = transaction_dlen(td); + for (i = 0; i < len; i++) { + rv = find_hello_to(vec[i], &place); + if (rv == -1) + return -1; + if (!rv) + continue; + strncpy(data->to, place, sizeof(data->to) - 1); + data->op = HELLO_DEL; + } + + /* Next look through avec to see if we need to add WORLD_FILE. */ + vec = transaction_avec(td); + len = transaction_alen(td); + for (i = 0; i < len; i++) { + + rv = find_hello_to(vec[i], &place); + if (rv == -1) + return -1; + if (!rv) + continue; + strncpy(data->to, place, sizeof(data->to) - 1); + data->op = HELLO_ADD; + } + + /* + * We do not look at the old data (_scvec), we just look at the + * changed data. The only thing that can change is what is in + * "to", so we check the parent of "to" to verify it. + */ + len = transaction_clen(td); + vec = transaction_tcvec(td); + for (i = 0; i < len; i++) { + cxobj *p = xml_parent(vec[i]); + + if (!p) + continue; + + rv = find_hello_to(p, &place); + if (rv == -1) + return -1; + if (!rv) + continue; + strncpy(data->to, place, sizeof(data->to) - 1); + data->op = HELLO_ADD; + } +#else + /* + * Get the previous data. Here we only care about deletes, we + * pick up changes in the target tree. + */ + xt = transaction_src(td); + x = NULL; + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { + rv = find_hello_to(x, &place); + if (rv == -1) + return -1; + if (!rv) + continue; + if (xml_flag(x, XML_FLAG_DEL)) { + strncpy(data->to, place, sizeof(data->to) - 1); + data->op = HELLO_DEL; + } + } + + /* + * Now go through the new data. We look for additions and + * changes, they are treated the same for this. + */ + xt = transaction_target(td); + x = NULL; + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { + rv = find_hello_to(x, &place); + if (rv == -1) + return -1; + if (!rv) + continue; + if (xml_flag(x, XML_FLAG_ADD | XML_FLAG_CHANGE)) { + strncpy(data->to, place, sizeof(data->to) - 1); + data->op = HELLO_ADD; + } + } +#endif + + return 0; +} + +/* + * Take the value of hello_data and apply it. If a change was + * requested, the validate call will have set the data appropriately. + */ +static int +hello_commit(clicon_handle h, transaction_data td) { + struct hello_data *data = transaction_arg(td); + FILE *f; + int rv; + + clixon_debug(CLIXON_DBG_DEFAULT, "op: %d\n", data->op); + switch (data->op) { + case HELLO_DEL: + rv = remove(WORLD_FILE); + if (rv < 0 && errno != ENOENT) { + clixon_err(OE_XML, 0, "Error deleting %s: %s", + WORLD_FILE, strerror(errno)); + return -1; + } + break; + + case HELLO_ADD: + f = fopen("/tmp/world", "w"); + if (!f) { + clixon_err(OE_XML, 0, "Error creating %s: %s", + WORLD_FILE, strerror(errno)); + return -1; + } + fprintf(f, "%s", data->to); + fclose(f); + + case HELLO_NOP: + break; + } + return 0; +} + +/* + * Retrieve the current state of WORLD_FILE from the filesystem and + * return it in the XML structure xtop. + */ +static int +hello_statedata(clixon_handle h, cvec *nsc, char *xpath, cxobj *xtop) +{ + int rv = -1; + cxobj **xvec = NULL; + FILE *f = fopen(WORLD_FILE, "r"); + char to[10], xmlstr[200]; + int k; + char *s; + int found = 0; + + clixon_debug(CLIXON_DBG_DEFAULT, "file: %p\n", f); + clixon_debug(CLIXON_DBG_DEFAULT, " xpath=%s\n", xpath); + for (k = 0; (s = cvec_i_str(nsc, k)) != NULL; k++) { + clixon_debug(CLIXON_DBG_DEFAULT, " nsc(%d)=%s\n", k, s); + if (strcmp(s, HELLO_NAMESPACE) == 0) + found = 1; + } + if (!found) + return -1; + + if (f) { + memset(to, 0, sizeof(to)); + k = fread(to, 1, 9, f); + fclose(f); + + if (!k) { + clixon_err(OE_XML, 0, "Empty %s contents", WORLD_FILE); + goto done; + } + for (k = 0; valid_tos[k]; k++) { + if (strcmp(valid_tos[k], to) == 0) + break; + } + if (!valid_tos[k]) { + clixon_err(OE_XML, 0, "Invalid %s contents: %s", WORLD_FILE, to); + goto done; + } + + snprintf(xmlstr, sizeof(xmlstr), + "%s", + HELLO_NAMESPACE, to); + if (clixon_xml_parse_string(xmlstr, YB_NONE, NULL, &xtop, NULL) < 0) + goto done; + } else { + if (clixon_xml_parse_string("", YB_NONE, NULL, &xtop, NULL) < 0) + goto done; + } + rv = 0; + done: + if (xvec) + free(xvec); + return rv; +} + +static clixon_plugin_api api = { + .ca_name = "hello backend", + .ca_init = clixon_plugin_init, + .ca_statedata = hello_statedata, + .ca_trans_begin = hello_begin, + .ca_trans_end = hello_end, + .ca_trans_commit = hello_commit, + .ca_trans_validate = hello_validate, +}; + +clixon_plugin_api * +clixon_plugin_init(clicon_handle h) { + clixon_debug(CLIXON_DBG_DEFAULT, "Entry\n"); + return &api; +}