lundi 1 juin 2020

Simple ad hoc SNMP

SNMP est l'acronyme de 'Simple Network Management Protocol' et, comme tout ce qui est simple, cela fini par devenir compliqué...

Ci-dessous un petit programme en 'C', utilisant la librairie Net-snmp pour extraire quelques valeurs de la dskTable en SNMP version 2c.

Le programme fait une peu plus d'une centaine de lignes. On passe en paramètre la cible, la 'community string' et un index. Une session est ouverte vers la machine cible avec sa 'community string', un PDU est construit pour interroger 4 variables. Les 4 OIDs sont construites à partir de l'OID de base et un index passé en paramètre au programme. Le programme envoie la requête et reçoit la réponse. On suppose que la réponse reflète la requête et que l'on reçoit, dans l'ordre, les chaînes de caractères dskPath, dskDevice et les entiers dskUsed et dskTotal. Il ne reste plus qu'à les imprimer. Tous les problèmes possibles ne sont pas traités en profondeur mais il y a quand même un minimum de 'sanity checks'.

#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>

#define DSK_PATH 2
#define DSK_DEVICE 3
#define DSK_TOTAL 6
#define DSK_USED 8
unsigned char oid_str[128];
char *oid_template = ".1.3.6.1.4.1.2021.9.1.%d.%d"; /* What, idx */

void build_part_req(netsnmp_pdu *pdu, int idx, int what)
 {
 oid oid_buf[128];
 size_t oid_len;

 sprintf(oid_str, oid_template, what, idx);
 if (!snmp_parse_oid(oid_str, oid_buf, &oid_len))
  {
  fprintf(stderr, "error snmp_parse_oid()\n");
  exit(-1);
  }
 snmp_add_null_var(pdu, oid_buf, oid_len);
 }
netsnmp_pdu *build_req(int idx)
 {
 netsnmp_pdu *pdu;

 pdu = snmp_pdu_create(SNMP_MSG_GET);

 build_part_req(pdu, idx, DSK_PATH);
 build_part_req(pdu, idx, DSK_DEVICE);
 build_part_req(pdu, idx, DSK_USED);
 build_part_req(pdu, idx, DSK_TOTAL);
 return(pdu);
 }
void use_it(struct snmp_pdu *response)
 {
 char buf[1024];
 struct variable_list *vp;
 char dskPath[1024];
 char dskDevice[1024];
 long dskUsed;
 long dskTotal;

 if (response->errstat == SNMP_ERR_NOERROR)
  { /* vp->type (4=str; 2=int), vp->val.string, *vp->val.integer ; snprint_variable(), snprint_value() */
  vp = response->variables;
  strcpy(dskPath, vp->val.string); /* DSK_PATH */
  vp = vp->next_variable;
  strcpy(dskDevice, vp->val.string); /* DSK_DEVICE */
  vp = vp->next_variable;
  dskUsed = *vp->val.integer;  /* DSK_USED */
  vp = vp->next_variable;
  dskTotal = *vp->val.integer;  /* DSK_TOTAL */
  printf("%ld/%ld for %s on %s\n", dskUsed, dskTotal, dskPath, dskDevice);
  }
 else {
  printf("Uhm... : response->errstat = 0x%lx\n", response->errstat);
  }
 }
struct snmp_session *open_session(char *host, char *community)
 {
 struct snmp_session ss, *sp;

 init_snmp("get_snmp_fs");
 snmp_sess_init(&ss);
 ss.version = SNMP_VERSION_2c;
 ss.peername = strdup(host);
 ss.community = strdup(community);
 ss.community_len = strlen(community);
 if ((sp = snmp_open(&ss)) == NULL)
  {
  snmp_perror("snmp_open");
  exit(-1);
  }
 return(sp);
 }
void main (int argc, char **argv)
 {
 struct snmp_session *sp;
 netsnmp_pdu *req, *resp;
 char  *host, *community;
 int  index, status;

 if (argc != 4)
  {
  fprintf(stderr, "usage is '%s <host> <community> <index>\n", argv[0]);
  exit(-1);
  }
 host = argv[1];
 community = argv[2];
 index = atoi(argv[3]);

 sp = open_session(host, community);
 req = build_req(index);

 if ((status = snmp_synch_response(sp, req, &resp)) == STAT_SUCCESS)
  use_it(resp);
 else fprintf(stderr, "snmp_synch_response() returns %d\n", status);

 snmp_free_pdu(resp);
 snmp_close(sp);

 exit(0);
 }


Le Makefile, lui aussi, réduit à sa plus simple expression est :

CFLAGS=-I. `net-snmp-config --cflags`
BUILDLIBS=`net-snmp-config --libs`

get_snmp_fs: get_snmp_fs.o
 gcc -o get_snmp_fs get_snmp_fs.o $(BUILDLIBS)




Et maintenant, le test...



On regarde d'abord un disque avec df(1). On retrouve son indice en faisant un snmpwalk(1). On peut retrouver plusieurs valeurs avec snmpget(1) et les comparer avec ce que le programme ci-dessus donne.

$ df -h |grep sda1
/dev/sda1       916G  159G  711G  19% /


$ snmpwalk localhost -Os -m /usr/share/snmp/mibs/UCD-SNMP-MIB.txt -v 2c -c public  dskPath
dskPath.1 = STRING: /
dskPath.2 = STRING: /var
dskPath.3 = STRING: /
dskPath.4 = STRING: /run
dskPath.5 = STRING: /dev/shm
dskPath.6 = STRING: /run/lock
dskPath.7 = STRING: /sys/fs/cgroup


$ snmpget localhost -Os -m /usr/share/snmp/mibs/UCD-SNMP-MIB.txt -v 2c -c public  dskPath.1 dskDevice.1 dskUsed.1 dskTotal.1
dskPath.1 = STRING: /
dskDevice.1 = STRING: /dev/sda1
dskUsed.1 = INTEGER: 166654692
dskTotal.1 = INTEGER: 960379920


$ ./get_snmp_fs localhost public 1
166654708/960379920 for / on /dev/sda1


Les machines actuelles sont tellement rapides qu'il est difficile de mesurer la différence entre un programme ne faisant qu'un échange de PDUs et un programme qui doit, en plus, analyser une MIB (sinon plusieurs)... J'avais été motivé par le fait que Nagios utilise souvent des scripts shell lançant des tas de processus pour exécuter des fonctions élémentaires.