diff --git a/apps/snmp/Makefile.in b/apps/snmp/Makefile.in index 46d2ec16..fa2a2866 100644 --- a/apps/snmp/Makefile.in +++ b/apps/snmp/Makefile.in @@ -93,6 +93,7 @@ APPSRC += snmp_main.c APPSRC += snmp_register.c APPSRC += snmp_handler.c APPSRC += snmp_lib.c +APPSRC += snmp_stream.c APPOBJ = $(APPSRC:.c=.o) diff --git a/apps/snmp/snmp_main.c b/apps/snmp/snmp_main.c index 4bb5d13c..8ec4eeb0 100644 --- a/apps/snmp/snmp_main.c +++ b/apps/snmp/snmp_main.c @@ -68,6 +68,7 @@ #include "snmp_lib.h" #include "snmp_register.h" +#include "snmp_stream.h" /* Command line options to be passed to getopt(3) */ #define SNMP_OPTS "hVD:f:l:C:o:z" @@ -112,6 +113,7 @@ snmp_terminate(clixon_handle h) cxobj *x = NULL; char *pidfile = clicon_snmp_pidfile(h); + clixon_snmp_stream_shutdown(h); snmp_shutdown(__FUNCTION__); shutdown_agent(); clixon_snmp_api_agent_cleanup(); @@ -582,6 +584,9 @@ main(int argc, /* Init and traverse mib-translated yangs and register callbacks */ if (clixon_snmp_traverse_mibyangs(h) < 0) goto done; + /* init snmp stream (traps) */ + if (clixon_snmp_stream_init(h) < 0) + goto done; /* Write pid-file */ if (pidfile_write(pidfile) < 0) diff --git a/apps/snmp/snmp_stream.c b/apps/snmp/snmp_stream.c new file mode 100644 index 00000000..da250aff --- /dev/null +++ b/apps/snmp/snmp_stream.c @@ -0,0 +1,475 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2024 Olof Hagsand + 2024 Mico Micic and Moser-Baer AG + + 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 ***** + */ + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + +#include +#include +#include + +/* cligen */ +#include + +/* clixon */ +#include + +/* net-snmp */ +#include +#include +#include + +#include "snmp_lib.h" + +/* + * SNMP v2 notification OID + */ +static oid notificationOid[] = {1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0}; + +struct stream_socket{ + qelem_t sc_q; /* queue header */ + int socket; /* socket */ +}; +/* Linked list of sockets used to listen for events + */ +static struct stream_socket *STREAM_SOCKETS = NULL; + +/*! Read smiv2:oid for given YANG node + * + * @param[in] ys YANG node + * @param[out] objid oid + * @param[out] objidlen oid length + * @retval 0 OK + * @retval -1 Error + */ +int +get_oid_for_yang_node(yang_stmt *ys, oid *objid, size_t *objidlen) +{ + int retval = -1; + int exist = 0; + char *oidstr = NULL; + + if (yang_extension_value_opt(ys, "smiv2:oid", &exist, &oidstr) < 0) + goto done; + if (exist == 0 || oidstr == NULL){ + clixon_debug(CLIXON_DBG_SNMP, "oid not found as SMIv2 yang extension of %s", yang_argument_get(ys)); + goto done; + } + if (snmp_parse_oid(oidstr, objid, objidlen) == NULL){ + clixon_err(OE_SNMP, errno, "snmp_parse_oid"); + goto done; + } + + retval = 0; +done: + return retval; +} + +/*! Add snmp trap v2 oid to the variable list and bind the given notification oid as value. + * + * @param[in] var_list Variable list + * @param[in] notify_oid OID of the notification to set as value + * @param[in] oid_len Length of notify_oid + */ +void +add_snmp_trapv2_oid(netsnmp_variable_list **var_list, oid *notify_oid, int oid_len) +{ + snmp_varlist_add_variable( + var_list, + notificationOid, + OID_LENGTH(notificationOid), + ASN_OBJECT_ID, + (const unsigned char*)notify_oid, oid_len * sizeof(oid) + ); +} + +/*! Add snmp var binding for all child nodes of cxparent. + * + * All child elements are read recursively. A var binding is created for each LEAF element + * with an existing oid and added to the snmp variable list. + * + * @param[in] var_list Variable list + * @param[in] cxparent Parent xml node + * @param[in] ys YANG node + */ +int +add_snmp_var_bindings(netsnmp_variable_list **var_list, cxobj *cxparent, yang_stmt *ys) +{ + int retval = -1; + int ret; + cxobj *xncont = NULL; /* notification context xml */ + yang_stmt *ychild; + char *xmlstr = NULL; + char *body = NULL; + int asn1type; + char *reason = NULL; + unsigned char *snmpval = NULL; + size_t snmplen = 0; + oid objid[MAX_OID_LEN] = {0,}; /* Leaf */ + size_t objidlen = MAX_OID_LEN; + + while ((xncont = xml_child_each(cxparent, xncont, CX_ELMNT)) != NULL){ + char *node_name = xml_name(xncont); + if ((ychild = yang_find(ys, 0, node_name)) != NULL) { + switch (yang_keyword_get(ychild)){ + case Y_LEAF: /* only Y_LEAF type is supported here */ + if (get_oid_for_yang_node(ychild, objid, &objidlen) < 0) + goto done; + if ((body = xml_body(xncont)) != NULL){ + if ((ret = type_xml2snmp_pre(body, ychild, &xmlstr)) < 0) + goto done; + if (ret == 0){ + clixon_debug(CLIXON_DBG_SNMP, "invalid data type for %s", node_name); + goto done; + } + } + if (type_yang2asn1(ychild, &asn1type, 1) < 0) + goto done; + if ((ret = type_xml2snmp(xmlstr, ychild, &asn1type, &snmpval, &snmplen, &reason)) < 0) + goto done; + if (ret == 0){ + clixon_debug(CLIXON_DBG_SNMP, "%s", reason); + goto done; + } + snmp_varlist_add_variable(var_list, objid, objidlen, asn1type, snmpval, snmplen); + break; + case Y_CONTAINER: /* process child nodes */ + add_snmp_var_bindings(var_list, xncont, ychild); + break; + default: + clixon_debug(CLIXON_DBG_SNMP, "type %s not supported in snmp trap", yang_key2str(yang_keyword_get(ychild))); + break; + } + } else { + clixon_debug(CLIXON_DBG_SNMP, "no yang def found for %s", node_name); + } + } + + retval = 0; +done: + if (reason) + free(reason); + if (xmlstr) + free(xmlstr); + if (snmpval) + free(snmpval); + return retval; +} + +/*! Publish snmp notification (V2 trap) for the given notification data (xml node). + * + * The OID is read from the YANG definition. If the notification contains data, all values + * are bound to the corresponding OIDs and sent with the snmp trap. + * + * @param[in] s Socket + * @param[in] arg expected to be clixon_handle + * @retval 0 OK + * @retval -1 Error + */ +static int +snmp_publish_notification(clixon_handle h, cxobj *xn) +{ + int retval = -1; + cxobj *xncont = NULL; /* notification content xml */ + yang_stmt *ymoddata = NULL; /* notification yang module */ + yang_stmt *yspec; + yang_stmt *ydef; + oid objid[MAX_OID_LEN] = {0,}; + size_t objidlen = MAX_OID_LEN; + netsnmp_variable_list *var_list = NULL; + + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clixon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } + while ((xncont = xml_child_each(xn, xncont, CX_ELMNT)) != NULL){ + char *node_name = xml_name(xncont); + /* Skip eventTime. It is the only pre-defined child node as defined in RFC 5277 */ + if (strcmp(node_name, "eventTime") != 0){ + if (ys_module_by_xml(yspec, xncont, &ymoddata) < 0) /* get yang module for notification */ + goto done; + if ((ydef = yang_find(ymoddata, Y_NOTIFICATION, node_name)) != NULL) { + if (get_oid_for_yang_node(ydef, objid, &objidlen) < 0) + goto done; + add_snmp_trapv2_oid(&var_list, objid, objidlen); + add_snmp_var_bindings(&var_list, xncont, ydef); + clixon_debug(CLIXON_DBG_SNMP, "sending snmp trap for %s", yang_argument_get(ydef)); + send_v2trap(var_list); + } + } + } + retval = 0; + + done: + if (var_list) + snmp_free_varbind(var_list); + + return retval; +} + +/*! Callback when stream notifications arrive from backend + * + * @param[in] s Socket + * @param[in] arg expected to be clixon_handle + * @retval 0 OK + * @retval -1 Error + */ +static int +snmp_stream_cb(int s, void *arg) +{ + int retval = -1; + clixon_handle h = (clixon_handle)arg; + int eof; + cxobj *xtop = NULL; /* top xml */ + cxobj *xn; /* notification xml */ + cbuf *cbmsg = NULL; + int ret; + + clixon_debug(CLIXON_DBG_SNMP, ""); + if (clixon_msg_rcv11(s, NULL, 0, &cbmsg, &eof) < 0) + goto done; + /* handle close from remote end: this will exit the client */ + if (eof){ + clixon_debug(CLIXON_DBG_SNMP, "eof"); + clixon_err(OE_PROTO, ESHUTDOWN, "Socket unexpected close"); + errno = ESHUTDOWN; + clixon_exit_set(1); + goto done; + } + if ((ret = clixon_xml_parse_string(cbuf_get(cbmsg), YB_NONE, NULL, &xtop, NULL)) < 0) + goto done; + if (ret == 0){ + clixon_err(OE_XML, EFAULT, "Invalid notification"); + goto done; + } + if ((xn = xpath_first(xtop, NULL, "notification")) == NULL) + goto ok; + /* forward notification as snmp trap */ + if(snmp_publish_notification(h, xn) < 0) + goto done; + + ok: + retval = 0; + done: + clixon_debug(CLIXON_DBG_SNMP, "retval: %d", retval); + if (xtop != NULL) + xml_free(xtop); + if (cbmsg) + cbuf_free(cbmsg); + return retval; +} + +/*! Subscribe to all backend notifications and create streaming socket + * + * @param[in] h Clixon handle + * @param[in] stream Name of the stream to subscribe + * @param[out] socket Socket to receive backend notifications + * @retval 0 OK + * @retval -1 Error + */ +static int +snmp_stream_subscribe(clixon_handle h, char *stream, int *socket) +{ + int retval = -1; + cxobj *xret = NULL; + cxobj *xe; + cbuf *cb = NULL; + int s; /* socket */ + + clixon_debug(CLIXON_DBG_SNMP, "Subscribing stream: %s", stream); + *socket = -1; + if ((cb = cbuf_new()) == NULL){ + clixon_err(OE_SNMP, errno, "cbuf_new"); + goto done; + } + cprintf(cb, "%s", + NETCONF_BASE_NAMESPACE, NETCONF_MESSAGE_ID_ATTR, EVENT_RFC5277_NAMESPACE, stream); + cprintf(cb, "]]>]]>"); + if (clicon_rpc_netconf(h, cbuf_get(cb), &xret, &s) < 0) + goto done; + if ((xe = xpath_first(xret, NULL, "rpc-reply/rpc-error")) != NULL){ + goto done; + } + *socket = s; + retval = 0; + + done: + clixon_debug(CLIXON_DBG_SNMP, "retval: %d", retval); + if (xret) + xml_free(xret); + if (cb) + cbuf_free(cb); + return retval; +} + +int +clixon_snmp_stream_shutdown(clixon_handle h) +{ + struct stream_socket *sc; + + while ((sc = STREAM_SOCKETS) != NULL){ + DELQ(sc, STREAM_SOCKETS, struct stream_socket *); + clixon_event_unreg_fd(sc->socket, snmp_stream_cb); + close(sc->socket); + free(sc); + } + return 0; +} + +/*! Read all streams from backend by calling the corresponding RFC5277 get RPC + * + * @param[in] h Clixon handle + * @param[out] streams List of stream names (malloced must be freed) + * @param[out] count Number of entries in streams list + * @retval 0 OK + * @retval -1 Error + */ +int +get_all_streams_from_backend(clixon_handle h, char ***streams, int *count) +{ + int retval = -1; + cxobj *xret = NULL; + cxobj *xnchild = NULL; + cxobj *xe; + cxobj *xname; + cbuf *cb = NULL; + int cnt = 0; + char **st = NULL; + + clixon_debug(CLIXON_DBG_SNMP, ""); + *count = 0; + *streams = NULL; + if ((cb = cbuf_new()) == NULL){ + clixon_err(OE_SNMP, errno, "cbuf_new"); + goto done; + } + /* get alle streams from backend */ + cprintf(cb, "]]>]]>", + NETCONF_BASE_NAMESPACE, NETCONF_MESSAGE_ID_ATTR, EVENT_RFC5277_NAMESPACE); + if (clicon_rpc_netconf(h, cbuf_get(cb), &xret, NULL) < 0) + goto done; + if ((xe = xpath_first(xret, NULL, "rpc-reply/rpc-error")) != NULL) + goto done; + if ((xe = xpath_first(xret, NULL, "rpc-reply/data/netconf/streams")) == NULL) { + clixon_debug(CLIXON_DBG_SNMP, "No streams provided by backend"); + goto ok; + } + while ((xnchild = xml_child_each(xe, xnchild, CX_ELMNT)) != NULL){ + if ((xname = xpath_first(xnchild, NULL, "name")) != NULL){ + char *stream_name = xml_body(xname); + if (cnt == 0) { + if((st = malloc(sizeof(char*))) == NULL){ + clixon_err(OE_SNMP, errno, "malloc"); + goto done; + } + } + else{ + if((st = realloc(st, (cnt+1) * sizeof(char*))) == NULL){ + clixon_err(OE_SNMP, errno, "realloc"); + goto done; + } + } + st[cnt] = calloc(strlen(stream_name)+1, sizeof(char)); + strcpy(st[cnt], stream_name); + cnt++; + } + } + + ok: + *count = cnt; + *streams = st; + retval = 0; + + done: + clixon_debug(CLIXON_DBG_SNMP, "retval: %d", retval); + if (xret) + xml_free(xret); + if (cb) + cbuf_free(cb); + return retval; +} + +/*! Init snmp stream (traps) by subscribing to all backend notifications + * + * @param[in] h Clixon handle + * @retval 0 OK + * @retval -1 Error + * @see snmp_stream_subscribe + */ +int +clixon_snmp_stream_init(clixon_handle h) +{ + int retval = -1; + struct stream_socket *stream_socket; + char **streams = NULL; + int streams_num = 0; + int socket = -1; + + clixon_debug(CLIXON_DBG_SNMP, ""); + if (get_all_streams_from_backend(h, &streams, &streams_num) < 0) + goto done; + for (int i=0; isocket = socket; + ADDQ(stream_socket, STREAM_SOCKETS); + } + } + + retval = 0; + + done: + if (streams){ + for (int i = 0; i < streams_num; i++) + free(streams[i]); + free(streams); + } + + clixon_debug(CLIXON_DBG_SNMP, "retval: %d", retval); + return retval; +} \ No newline at end of file diff --git a/apps/snmp/snmp_stream.h b/apps/snmp/snmp_stream.h new file mode 100644 index 00000000..a6fc650e --- /dev/null +++ b/apps/snmp/snmp_stream.h @@ -0,0 +1,46 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2024 Olof Hagsand + 2024 Mico Micic and Moser-Baer AG + + 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 ***** + + */ + +#ifndef _SNMP_STREAM_H_ +#define _SNMP_STREAM_H_ + +/* + * Prototypes + */ +int clixon_snmp_stream_init(clixon_handle h); +int clixon_snmp_stream_shutdown(clixon_handle h); + +#endif /* _SNMP_STREAM_H_ */