clixon/apps/snmp/snmp_stream.c
2024-08-01 06:51:28 +02:00

486 lines
15 KiB
C

/*
*
***** 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 <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
/* cligen */
#include <cligen/cligen.h>
/* clixon */
#include <clixon/clixon.h>
/* net-snmp */
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>
#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 sc_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, "<rpc xmlns=\"%s\" %s><create-subscription xmlns=\"%s\"><stream>%s</stream>",
NETCONF_BASE_NAMESPACE, NETCONF_MESSAGE_ID_ATTR, EVENT_RFC5277_NAMESPACE, stream);
cprintf(cb, "</create-subscription></rpc>]]>]]>");
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->sc_socket, snmp_stream_cb);
close(sc->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, "<rpc xmlns=\"%s\" %s><get><filter type=\"subtree\"><netconf xmlns=\"%s\"><streams/></netconf></filter></get></rpc>]]>]]>",
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; i<streams_num; i++){
socket = -1;
if(snmp_stream_subscribe(h, streams[i], &socket) < 0)
goto done;
if (socket != -1){
/* Listen to backend socket */
if (clixon_event_reg_fd(socket,
snmp_stream_cb,
h,
"stream socket") < 0)
goto done;
if ((stream_socket = calloc(1, sizeof(struct stream_socket))) == NULL){
clixon_err(OE_SNMP, errno, "malloc");
goto done;
}
stream_socket->sc_socket = 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;
}