diff --git a/CHANGELOG.md b/CHANGELOG.md index 064f8b7f..597cecf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,16 +35,21 @@ * [3.3.2](#332) Aug 27 2017 * [3.3.1](#331) June 7 2017 +## SNMP branch + +* Clixon SNMP frontend + * Support of SNMP for retreiving and setting values via net-snmp using a MIB-YANG mapping defined in RFC6643. + * For details, see [SNMP section of user manual](https://clixon-docs.readthedocs.io/en/latest/snmp.html) + * YANG `clixon-config@2022-03-21.yang` changes: + * Added option: + * `CLICON_SNMP_AGENT_SOCK` + * Thanks Siklu for sponshoring + ## 5.8.0 Planned: July 2022 ### New features -* Clixon SNMP frontend - * Support of SNMP for retreiving and setting values via netsnmp using a MIB-YANG mapping defined in RFC6643. - * For more details, see [SNMP section of user manual](https://clixon-docs.readthedocs.io/en/latest/snmp.html) - * Thanks Siklu for sponshoring - * YANG Action (RFC 7950 Section 7.15) * Register action callback with `action_callback_register()`, see main example * Remains: check list keys, validate output diff --git a/apps/snmp/snmp_main.c b/apps/snmp/snmp_main.c index e2d3e1c5..e823ac87 100644 --- a/apps/snmp/snmp_main.c +++ b/apps/snmp/snmp_main.c @@ -58,59 +58,413 @@ /* Command line options to be passed to getopt(3) */ #define SNMP_OPTS "hD:f:l:o:" -#if 1 // XXX hardcoded MIB object from https://github.com/net-snmp/subagent-example/blob/master/example-demon.c -/* cp NET-SNMP-TUTORIAL-MIB.txt ~/.snmp/mibs/ - * sudo /usr/local/sbin/snmpd -Lo -C --rwcommunity=public --master=agentx -f - * sudo example_demon - * snmpget -v 2c -c public localhost NET-SNMP-TUTORIAL-MIB::nstAgentSubagentObject.0 - */ -/*! - * our initialization routine, automatically called by the agent - * (to get called, the function name must match init_FILENAME()) - * the variable we want to tie an OID to. The agent will handle all - * * GET and SET requests to this variable changing it's value as needed. - */ -static long nstAgentSubagentObject = 2; +#if 1 // XXX hardcoded from https://github.com/net-snmp/net-snmp/blob/master/agent/mibgroup/testhandler.c -static void -init_nstAgentSubagentObject(clicon_handle h) +Netsnmp_Node_Handler my_test_handler; +Netsnmp_Node_Handler my_test_table_handler; +Netsnmp_Node_Handler my_data_table_handler; +Netsnmp_Node_Handler my_test_instance_handler; + +static oid my_test_oid[4] = { 1, 2, 3, 4 }; +static oid my_table_oid[4] = { 1, 2, 3, 5 }; +static oid my_instance_oid[5] = { 1, 2, 3, 6, 1 }; +static oid my_data_table_oid[4] = { 1, 2, 3, 7 }; +static oid my_data_ulong_instance[4] = { 1, 2, 3, 9 }; + +u_long my_ulong = 42; + +void +init_testhandler(void) { - static oid nstAgentSubagentObject_oid[] = - { 1, 3, 6, 1, 4, 1, 8072, 2, 4, 1, 1, 2, 0 }; + /* + * we're registering at .1.2.3.4 + */ + netsnmp_handler_registration *my_test; + netsnmp_table_registration_info *table_info; + u_long ind1; + netsnmp_table_data *table; + netsnmp_table_row *row; + + clicon_debug(1, "%s", __FUNCTION__); + + /* + * basic handler test + */ + netsnmp_register_handler(netsnmp_create_handler_registration + ("myTest", my_test_handler, my_test_oid, 4, + HANDLER_CAN_RONLY)); + + /* + * instance handler test + */ + + netsnmp_register_instance(netsnmp_create_handler_registration + ("myInstance", my_test_instance_handler, + my_instance_oid, 5, HANDLER_CAN_RWRITE)); + + netsnmp_register_ulong_instance("myulong", + my_data_ulong_instance, 4, + &my_ulong, NULL); + + /* + * table helper test + */ + + my_test = netsnmp_create_handler_registration("myTable", + my_test_table_handler, + my_table_oid, 4, + HANDLER_CAN_RONLY); + if (!my_test) + return; + + table_info = SNMP_MALLOC_TYPEDEF(netsnmp_table_registration_info); + if (table_info == NULL) + return; + + netsnmp_table_helper_add_indexes(table_info, ASN_INTEGER, ASN_INTEGER, + 0); + table_info->min_column = 3; + table_info->max_column = 3; + netsnmp_register_table(my_test, table_info); + + /* + * data table helper test + */ + /* + * we'll construct a simple table here with two indexes: an + * integer and a string (why not). It'll contain only one + * column so the data pointer is merely the data in that + * column. + */ + + table = netsnmp_create_table_data("data_table_test"); + + netsnmp_table_data_add_index(table, ASN_INTEGER); + netsnmp_table_data_add_index(table, ASN_OCTET_STR); + + /* + * 1 partridge in a pear tree + */ + row = netsnmp_create_table_data_row(); + ind1 = 1; + netsnmp_table_row_add_index(row, ASN_INTEGER, &ind1, sizeof(ind1)); + netsnmp_table_row_add_index(row, ASN_OCTET_STR, "partridge", + strlen("partridge")); + row->data = NETSNMP_REMOVE_CONST(void *, "pear tree"); + netsnmp_table_data_add_row(table, row); + + /* + * 2 turtle doves + */ + row = netsnmp_create_table_data_row(); + ind1 = 2; + netsnmp_table_row_add_index(row, ASN_INTEGER, &ind1, sizeof(ind1)); + netsnmp_table_row_add_index(row, ASN_OCTET_STR, "turtle", + strlen("turtle")); + row->data = NETSNMP_REMOVE_CONST(void *, "doves"); + netsnmp_table_data_add_row(table, row); + + /* + * we're going to register it as a normal table too, so we get the + * automatically parsed column and index information + */ + table_info = SNMP_MALLOC_TYPEDEF(netsnmp_table_registration_info); + if (table_info == NULL) + return; + + netsnmp_table_helper_add_indexes(table_info, ASN_INTEGER, + ASN_OCTET_STR, 0); + table_info->min_column = 3; + table_info->max_column = 3; + + netsnmp_register_read_only_table_data( + netsnmp_create_handler_registration("12days", my_data_table_handler, my_data_table_oid, 4, + HANDLER_CAN_RONLY), + table, table_info); + +} + +int +my_test_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + + oid myoid1[] = { 1, 2, 3, 4, 5, 6 }; + static u_long accesses = 0; clicon_debug(1, "%s", __FUNCTION__); /* - * a debugging statement. Run the agent with -DnstAgentSubagentObject to see - * the output of this debugging statement. + * loop through requests */ - DEBUGMSGTL(("nstAgentSubagentObject", - "Initializing the nstAgentSubagentObject module\n")); + while (requests) { + netsnmp_variable_list *var = requests->requestvb; - /* - * the line below registers our variables defined above as - * accessible and makes it writable. A read only version of any - * of these registration would merely call - * register_read_only_long_instance() instead. The functions - * called below should be consistent with your MIB, however. - * - * If we wanted a callback when the value was retrieved or set - * (even though the details of doing this are handled for you), - * you could change the NULL pointer below to a valid handler - * function. - */ - DEBUGMSGTL(("nstAgentSubagentObject", - "Initalizing nstAgentSubagentObject scalar integer. Default value = %ld\n", - nstAgentSubagentObject)); + DEBUGMSGTL(("testhandler", " oid:")); + DEBUGMSGOID(("testhandler", var->name, var->name_length)); + DEBUGMSG(("testhandler", "\n")); - netsnmp_register_long_instance("nstAgentSubagentObject", - nstAgentSubagentObject_oid, - OID_LENGTH(nstAgentSubagentObject_oid), - &nstAgentSubagentObject, NULL); + switch (reqinfo->mode) { + case MODE_GET: + if (netsnmp_oid_equals(var->name, var->name_length, myoid1, 6) + == 0) { + snmp_set_var_typed_value(var, ASN_INTEGER, + (u_char *) & accesses, + sizeof(accesses)); + return SNMP_ERR_NOERROR; + } + break; - DEBUGMSGTL(("nstAgentSubagentObject", - "Done initalizing nstAgentSubagentObject module\n")); + case MODE_GETNEXT: + if (snmp_oid_compare(var->name, var->name_length, myoid1, 6) + < 0) { + snmp_set_var_objid(var, myoid1, 6); + snmp_set_var_typed_value(var, ASN_INTEGER, + (u_char *) & accesses, + sizeof(accesses)); + return SNMP_ERR_NOERROR; + } + break; + + default: + netsnmp_set_request_error(reqinfo, requests, SNMP_ERR_GENERR); + break; + } + + requests = requests->next; + } + return SNMP_ERR_NOERROR; } -#endif // XXX Hardcoded + +/* + * functionally this is a simply a multiplication table for 12x12 + */ + +#define MAX_COLONE 12 +#define MAX_COLTWO 12 +#define RESULT_COLUMN 3 +int +my_test_table_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + + netsnmp_table_registration_info + *handler_reg_info = + (netsnmp_table_registration_info *) handler->prev->myvoid; + + netsnmp_table_request_info *table_info; + u_long result; + int x, y; + + DEBUGMSGTL(("testhandler", "Got request:\n")); + + while (requests) { + netsnmp_variable_list *var = requests->requestvb; + + if (requests->processed != 0) + continue; + + DEBUGMSGTL(("testhandler_table", "Got request:\n")); + DEBUGMSGTL(("testhandler_table", " oid:")); + DEBUGMSGOID(("testhandler_table", var->name, var->name_length)); + DEBUGMSG(("testhandler_table", "\n")); + + table_info = netsnmp_extract_table_info(requests); + if (table_info == NULL) { + requests = requests->next; + continue; + } + + switch (reqinfo->mode) { + case MODE_GETNEXT: + /* + * beyond our search range? + */ + if (table_info->colnum > RESULT_COLUMN) + break; + + /* + * below our minimum column? + */ + if (table_info->colnum < RESULT_COLUMN || + /* + * or no index specified + */ + table_info->indexes->val.integer == NULL) { + table_info->colnum = RESULT_COLUMN; + x = 0; + y = 0; + } else { + x = *(table_info->indexes->val.integer); + y = *(table_info->indexes->next_variable->val.integer); + } + + if (table_info->number_indexes == + handler_reg_info->number_indexes) { + y++; /* GETNEXT is basically just y+1 for this table */ + if (y > MAX_COLTWO) { /* (with wrapping) */ + y = 0; + x++; + } + } + if (x <= MAX_COLONE) { + result = x * y; + + *(table_info->indexes->val.integer) = x; + *(table_info->indexes->next_variable->val.integer) = y; + netsnmp_table_build_result(reginfo, requests, + table_info, ASN_INTEGER, + (u_char *) & result, + sizeof(result)); + } + + break; + + case MODE_GET: + if (var->type == ASN_NULL) { /* valid request if ASN_NULL */ + /* + * is it the right column? + */ + if (table_info->colnum == RESULT_COLUMN && + /* + * and within the max boundries? + */ + *(table_info->indexes->val.integer) <= MAX_COLONE && + *(table_info->indexes->next_variable->val.integer) + <= MAX_COLTWO) { + + /* + * then, the result is column1 * column2 + */ + result = *(table_info->indexes->val.integer) * + *(table_info->indexes->next_variable->val.integer); + snmp_set_var_typed_value(var, ASN_INTEGER, + (u_char *) & result, + sizeof(result)); + } + } + break; + + } + + requests = requests->next; + } + + return SNMP_ERR_NOERROR; +} + +#define TESTHANDLER_SET_NAME "my_test" +int +my_test_instance_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + + static u_long accesses = 42; + u_long *accesses_cache = NULL; + + clicon_debug(1, "%s", __FUNCTION__); + + switch (reqinfo->mode) { + case MODE_GET: + snmp_set_var_typed_value(requests->requestvb, ASN_UNSIGNED, + (u_char *) & accesses, sizeof(accesses)); + break; + +#ifndef NETSNMP_NO_WRITE_SUPPORT + case MODE_SET_RESERVE1: + if (requests->requestvb->type != ASN_UNSIGNED) + netsnmp_set_request_error(reqinfo, requests, + SNMP_ERR_WRONGTYPE); + break; + + case MODE_SET_RESERVE2: + /* + * store old info for undo later + */ + accesses_cache = netsnmp_memdup(&accesses, sizeof(accesses)); + if (accesses_cache == NULL) { + netsnmp_set_request_error(reqinfo, requests, + SNMP_ERR_RESOURCEUNAVAILABLE); + return SNMP_ERR_NOERROR; + } + netsnmp_request_add_list_data(requests, + netsnmp_create_data_list + (TESTHANDLER_SET_NAME, + accesses_cache, free)); + break; + + case MODE_SET_ACTION: + /* + * update current + */ + accesses = *(requests->requestvb->val.integer); + DEBUGMSGTL(("testhandler", "updated accesses -> %lu\n", accesses)); + break; + + case MODE_SET_UNDO: + accesses = + *((u_long *) netsnmp_request_get_list_data(requests, + TESTHANDLER_SET_NAME)); + break; + + case MODE_SET_COMMIT: + case MODE_SET_FREE: + /* + * nothing to do + */ + break; +#endif /* NETSNMP_NO_WRITE_SUPPORT */ + } + + return SNMP_ERR_NOERROR; +} + +int +my_data_table_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + + char *column3; + netsnmp_table_request_info *table_info; + netsnmp_table_row *row; + + clicon_debug(1, "%s", __FUNCTION__); + + while (requests) { + if (requests->processed) { + requests = requests->next; + continue; + } + + /* + * extract our stored data and table info + */ + row = netsnmp_extract_table_row(requests); + table_info = netsnmp_extract_table_info(requests); + if (!table_info || !row || !row->data) + continue; + column3 = (char *) row->data; + + /* + * there's only one column, we don't need to check if it's right + */ + netsnmp_table_data_build_result(reginfo, reqinfo, requests, row, + table_info->colnum, + ASN_OCTET_STR, (u_char*)column3, + strlen(column3)); + requests = requests->next; + } + return SNMP_ERR_NOERROR; +} + +#endif /*! Signal terminates process * Just set exit flag for proper exit in event loop @@ -194,7 +548,8 @@ static int clixon_snmp_init(clicon_handle h, int logdst) { - int retval = -1; + int retval = -1; + char *sockpath = NULL; clicon_debug(1, "%s", __FUNCTION__); if (logdst == CLICON_LOG_SYSLOG) @@ -204,14 +559,22 @@ clixon_snmp_init(clicon_handle h, /* make a agentx client. */ netsnmp_ds_set_boolean(NETSNMP_DS_APPLICATION_ID, NETSNMP_DS_AGENT_ROLE, 1); + if ((sockpath = clicon_option_str(h, "CLICON_SNMP_AGENT_SOCK")) == NULL){ + clicon_err(OE_SNMP, 0, "CLICON_SNMP_AGENT_SOCK not set"); + goto done; + } /* XXX: This should be configurable. */ - netsnmp_ds_set_string(NETSNMP_DS_APPLICATION_ID, NETSNMP_DS_AGENT_X_SOCKET, "unix:/tmp/clixon_snmp.sock"); + netsnmp_ds_set_string(NETSNMP_DS_APPLICATION_ID, NETSNMP_DS_AGENT_X_SOCKET, sockpath); /* initialize the agent library */ init_agent(__PROGRAM__); /* XXX Hardcoded, replace this with generic MIB */ +#if 1 + init_testhandler(); +#else init_nstAgentSubagentObject(h); +#endif /* example-demon will be used to read example-demon.conf files. */ init_snmp(__PROGRAM__); @@ -266,7 +629,6 @@ snmp_terminate(clicon_handle h) return 0; } - /*! Usage help routine * @param[in] h Clixon handle * @param[in] argv0 command line @@ -468,6 +830,7 @@ main(int argc, if (dbg) clicon_option_dump(h, dbg); + /* main event loop */ if (clixon_event_loop(h) < 0) goto done; retval = 0; diff --git a/test/lib.sh b/test/lib.sh index 5008d1c0..b8fa2a4c 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -182,6 +182,8 @@ BUSER=clicon : ${clixon_backend:=clixon_backend} +: ${clixon_snmp:=$(type -p clixon_snmp)} + # Source the site-specific definitions for test script variables, if site.sh # exists. The variables defined in site.sh override any variables of the same # names in the environment in the current execution. diff --git a/test/test_snmp.sh b/test/test_snmp.sh index 793cba63..694391e6 100755 --- a/test/test_snmp.sh +++ b/test/test_snmp.sh @@ -17,12 +17,15 @@ if [ ${WITH_NETSNMP} != "yes" ]; then fi snmpd=$(type -p snmpd) -snmpget="$(type -p snmpget) -c public -v2c localhost:1161 " -snmpset="$(type -p snmpset) -c public -v2c localhost:1161 " -clixon_snmp="/usr/local/sbin/clixon_snmp" +snmpget="$(type -p snmpget) -On -c public -v2c localhost:1161 " +snmpset="$(type -p snmpset) -On -c public -v2c localhost:1161 " + cfg=$dir/conf_startup.xml fyang=$dir/clixon-example.yang +# AgentX unix socket +SOCK=/tmp/clixon_snmp.sock + cat < $cfg $cfg @@ -31,6 +34,7 @@ cat < $cfg $dir/$APPNAME.sock /var/tmp/$APPNAME.pidfile $dir + unix:$SOCK EOF @@ -47,8 +51,8 @@ function testinit(){ new "kill old snmp daemons" sudo killall snmpd - new "Starting snmpd" - $snmpd --rwcommunity=public --master=agentx --agentXSocket=unix:/tmp/clixon_snmp.sock udp:127.0.0.1:1161 + new "Starting $snmpd --rwcommunity=public --master=agentx --agentXSocket=unix:/tmp/clixon_snmp.sock udp:127.0.0.1:1161" + $snmpd --rwcommunity=public --master=agentx --agentXSocket=unix:$SOCK udp:127.0.0.1:1161 pgrep snmpd if [ $? != 0 ]; then @@ -65,14 +69,14 @@ function testinit(){ sudo pkill -f clixon_backend new "Starting backend" - start_backend -s init -f $cfg -- -s + start_backend -s init -f $cfg # Kill old clixon_snmp, if any new "Terminating any old clixon_snmp processes" sudo killall clixon_snmp new "Starting clixon_snmp" - $clixon_snmp -f $cfg & + $clixon_snmp -f $cfg -D $DBG -l s & sleep 1 @@ -89,14 +93,16 @@ function testexit(){ new "SNMP tests" testinit +OID=".1.2.3.6.1" + new "Test SNMP get for default value" -expectpart "$($snmpget .1.3.6.1.4.1.8072.2.4.1.1.2.0)" 0 "NET-SNMP-EXAMPLES-MIB::netSnmpExamples.4.1.1.2.0 = INTEGER: 2" +expectpart "$($snmpget $OID)" 0 "$OID = Gauge32: 42" new "Set new value to OID" -expectpart "$($snmpset .1.3.6.1.4.1.8072.2.4.1.1.2.0 i 1234)" 0 "NET-SNMP-EXAMPLES-MIB::netSnmpExamples.4.1.1.2.0 = INTEGER: 1234" +expectpart "$($snmpset $OID u 1234)" 0 "$OID = Gauge32: 1234" new "Get new value" -expectpart "$($snmpget .1.3.6.1.4.1.8072.2.4.1.1.2.0)" 0 "NET-SNMP-EXAMPLES-MIB::netSnmpExamples.4.1.1.2.0 = INTEGER: 1234" +expectpart "$($snmpget $OID)" 0 "$OID = Gauge32: 1234" new "Cleaning up" testexit diff --git a/yang/clixon/clixon-config@2022-03-21.yang b/yang/clixon/clixon-config@2022-03-21.yang index ac07045d..7becc7e2 100644 --- a/yang/clixon/clixon-config@2022-03-21.yang +++ b/yang/clixon/clixon-config@2022-03-21.yang @@ -1132,5 +1132,17 @@ module clixon-config { 0 means no limit"; } + leaf CLICON_SNMP_AGENT_SOCK { + type string; + default "unix:/tmp/clixon_snmp.sock"; + description + "String description of AgentX socket that clixon_snmp listens to. + For example, for net-snmpd, the socket is created by using the following: + --agentXSocket=unix: + This string currently only supports UNIX socket path. + Note also that the user should consider setting permissions appropriately + XXX: This should be in later yang revision and documented as added when + merged with master"; + } } }