494 lines
16 KiB
C
494 lines
16 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;
|
|
}
|
|
if (xmlstr) {
|
|
free(xmlstr);
|
|
xmlstr = NULL;
|
|
}
|
|
if (snmpval) {
|
|
free(snmpval);
|
|
snmpval = NULL;
|
|
}
|
|
} 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;
|
|
}
|