#!/usr/bin/env bash
#
# an auxiliary script to produce a "stub" snmp-ups subdriver from
# SNMP data from a real agent or from dump files
#
# Version: 0.18
#
# See also: docs/snmp-subdrivers.txt
#
# Copyright (C)
# 2011 - 2012 Arnaud Quette <arnaud.quette@free.fr>
# 2015 - 2022 Eaton (author: Arnaud Quette <ArnaudQuette@Eaton.com>)
# 2011 - 2025 Jim Klimov <jimklimov+nut@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# TODO:
# - Prepend sysDescription (.1.3.6.1.2.1.1.1.0) to have some more visibility
# - extend to SNMP v3 (auth.)

usage() {
	echo "Usage: $0 [options] [file]"
	echo "Options:"
	echo " -h, --help          -- show this message and quit"
	echo " -n name             -- subdriver name (use natural capitalization)"
	echo " -M DIRLIST          -- colon separated list of directories to also search for MIBs"
	echo " -k                  -- keep temporary files (for debugging)"
	echo ""
	echo "mode 1: get SNMP data from a real agent"
	echo " -H host_address     -- SNMP host IP address or name"
	echo " -c community        -- SNMP v1 community name (default: public)"
	echo " -s XXXX             -- override SNMP OID entry point (sysOID). Ex: '.1.3.6.1.4.1.534.10'"
	echo ""
	echo "mode 2: get data from files (snmpwalk dumps of 'sysOID' subtree)"
	echo " -s XXXX             -- SNMP OID entry point (sysOID). Ex: '.1.3.6.1.4.1.534.6.6.7'"
	echo " file1 file2         -- read from files instead of an host (using Net SNMP)"
	echo "                        file1: numeric SNMP walk (snmpwalk -On ... <sysOID>)"
	echo "                        file2: string SNMP walk (snmpwalk -Os ... <sysOID>)"
	echo ""
	echo "mode 3: get data from 1 file (numeric snmpwalk dump of the whole SNMP tree)"
	echo "        The sysOID is extracted from the dump, and only the pointed subtree is used"
	echo "        A MIB file MUST be provided, and is used to produce the string SNMP walk"
	echo " file1               -- read from file instead of an host (using Net SNMP)"
	echo "                        file1: numeric SNMP walk (snmpwalk -On ... <sysOID>)"
	echo ""

	echo "Notes:"
	echo " For both modes, prefer to copy the specific MIB file(s) for your device in the $0 script directory"
	echo " So that it is automatically taken into account for the string name resolution of OIDs"
	echo " Otherwise, use \"-M.\" option"
	echo ""
	echo "Example:"
	echo "mode 1: $0 -H 192.168.0.1 -n mibname -c mycommunity"
	echo "mode 2: (using sysOID .1.3.6.1.4.1.534.6.6.7)"
	echo " snmpwalk -On -v1 -c mycommunity 192.168.0.1 .1.3.6.1.4.1.534.6.6.7 2>/dev/null 1> numeric-walk-file"
	echo " snmpwalk -Os -v1 -m ALL -M+. -c mycommunity 192.168.0.1 .1.3.6.1.4.1.534.6.6.7 2>/dev/null 1> string-walk-file"
	echo " $0 -s .1.3.6.1.4.1.534.6.6.7 numeric-walk-file string-walk-file"
	echo "mode 3:"
	echo " snmpwalk -On -v1 -c mycommunity 192.168.0.1 .1 2>/dev/null 1> numeric-walk-file"
	echo " $0 numeric-walk-file"
	echo ""
	echo " You may alos need to install additional packages:"
	echo " - 'snmp' package (on Debian) for the base commands (snmpget, snmpwalk, snmptranslate)"
	echo " - 'snmp-mibs-downloader' package (on Debian) to get all standard MIBs"
}

# tools
[ -n "${GREP}" ] || { GREP="`command -v grep`" && [ x"${GREP}" != x ] || { echo "$0: FAILED to locate GREP tool" >&2 ; exit 1 ; } ; }
[ -n "${EGREP}" ] || { if ( [ x"`echo a | $GREP -E '(a|b)'`" = xa ] ) 2>/dev/null ; then EGREP="$GREP -E" ; else EGREP="`command -v egrep`" ; fi && [ x"${EGREP}" != x ] || { echo "$0: FAILED to locate EGREP tool" >&2 ; exit 1 ; } ; }

# variables
DRIVER=""
KEEP=""
HOSTNAME=""
MIBS_DIRLIST="+."
COMMUNITY="public"
DEVICE_SYSOID=""
SYSOID=""
MODE=0

if (command -v mktemp) >/dev/null ; then true ; else
# Have a simple (unsafe, unfeatured) fallback implementation:
mktemp() {
    if [ x"$1" = x"-d" ] ; then
        shift
        mkdir -p "$1.$$" || return
    else
        cat /dev/null > "$1.$$" || return
    fi
    echo "$1.$$"
}
fi

# constants
NAME=gen-snmp-subdriver
TMPDIR="${TEMPDIR:-/tmp}"
SYSOID_NUMBER=".1.3.6.1.2.1.1.2.0"
DEBUG="`mktemp \"$TMPDIR/$NAME-DEBUG.XXXXXX\"`"
DFL_NUMWALKFILE="`mktemp \"$TMPDIR/$NAME-NUMWALK.XXXXXX\"`"
DFL_STRWALKFILE="`mktemp \"$TMPDIR/$NAME-STRWALK.XXXXXX\"`"
TMP_NUMWALKFILE="`mktemp \"$TMPDIR/$NAME-TMP-NUMWALK.XXXXXX\"`"
TMP_STRWALKFILE="`mktemp \"$TMPDIR/$NAME-TMP-STRWALK.XXXXXX\"`"

# Platforms vary with tooling abilitites...
TOLOWER="cat"
for TR_VARIANT in "tr 'A-Z' 'a-z'" "tr '[:upper:]' '[:lower:]'" "tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'" ; do
    if [ x"`echo C | $TR_VARIANT`" = xc ] ; then
        TOLOWER="$TR_VARIANT"
        break
    fi
done

TOUPPER="cat"
for TR_VARIANT in "tr 'a-z' 'A-Z'" "tr '[:lower:]' '[:upper:]'" "tr 'abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" ; do
    if [ x"`echo c | $TR_VARIANT`" = xC ] ; then
        TOUPPER="$TR_VARIANT"
        break
    fi
done

get_snmp_data() {
	# 1) get the sysOID (points the mfr specif MIB), apart if there's an override
	if [ -z "$SYSOID" ]
	then
		SYSOID="`snmpget -On -v1 -c \"$COMMUNITY\" -Ov \"$HOSTNAME\" \"$SYSOID_NUMBER\" | cut -d' ' -f2`"
		echo "sysOID retrieved: ${SYSOID}"
	else
		echo "Using the provided sysOID override ($SYSOID)"
	fi
	DEVICE_SYSOID="$SYSOID"

	OID_COUNT=0
	while (test "$OID_COUNT" -eq 0)
	do
		# 2) get the content of the mfr specif MIB
		echo "Retrieving SNMP information. This may take some time"
		snmpwalk -On -v1 -c "$COMMUNITY" "$HOSTNAME" "$SYSOID" 2>/dev/null 1> "$DFL_NUMWALKFILE"
		snmpwalk -Os -v1 -m ALL -M"$MIBS_DIRLIST" -c "$COMMUNITY" "$HOSTNAME" "$SYSOID" 2>/dev/null 1> "$DFL_STRWALKFILE"

		# 3) test return value of the walk, and possibly ramp-up the path to get something.
		# The sysOID mechanism only works if we're pointed somehow in the right direction
		# i.e. doesn't work if sysOID is .1.3.6.1.4.1.705.1 and data is at .1.3.6.1.4.1.534...
		# Ex: sysOID = ".1.X.Y.Z"
		# try with ".1.X.Y.Z", if fails try with .1.X.Y", if fails try with .1.X"...
		OID_COUNT="`cat $NUMWALKFILE | wc -l`"
		if [ $OID_COUNT -eq 0 ]; then
			# ramp-up the provided sysOID by removing the last .x part
			SYSOID=${SYSOID%.*}
			echo "Warning: sysOID provided no data! Trying with a level up using $SYSOID"
		fi
	done
	return $OID_COUNT
}

generate_C() {
	# create file names, lowercase
	LDRIVER="`echo \"$DRIVER\" | $TOLOWER`"
	UDRIVER="`echo \"$DRIVER\" | $TOUPPER`"
	# keep dashes in name for files
	CFILE="$LDRIVER-mib.c"
	HFILE="$LDRIVER-mib.h"
	# but replace with underscores for the structures and defines
	LDRIVER="`echo \"$LDRIVER\" | tr - _`"
	UDRIVER="`echo \"$UDRIVER\" | tr - _`"

	# generate header file
	# NOTE: with <<-EOF leading TABs are all stripped
	echo "Creating $HFILE"
	cat > "$HFILE" <<-EOF
	/* ${HFILE} - subdriver to monitor ${DRIVER} SNMP devices with NUT
	 *
	 *  Copyright (C)
	 *  2011 - 2016	Arnaud Quette <arnaud.quette@free.fr>
	 *
	 *  This program is free software; you can redistribute it and/or modify
	 *  it under the terms of the GNU General Public License as published by
	 *  the Free Software Foundation; either version 2 of the License, or
	 *  (at your option) any later version.
	 *
	 *  This program is distributed in the hope that it will be useful,
	 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
	 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	 *  GNU General Public License for more details.
	 *
	 *  You should have received a copy of the GNU General Public License
	 *  along with this program; if not, write to the Free Software
	 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
	 */

	#ifndef ${UDRIVER}_MIB_H
	#define ${UDRIVER}_MIB_H

	#include "main.h"
	#include "snmp-ups.h"

	extern mib2nut_info_t ${LDRIVER};

	#endif /* ${UDRIVER}_MIB_H */
	EOF

	# generate source file
	# create heading boilerblate
	# NOTE: with <<-EOF leading TABs are all stripped
	echo "Creating $CFILE"
	cat > "$CFILE" <<-EOF
	/* ${CFILE} - subdriver to monitor ${DRIVER} SNMP devices with NUT
	 *
	 *  Copyright (C)
	 *  2011 - 2016	Arnaud Quette <arnaud.quette@free.fr>
	 *
	 *  Note: this subdriver was initially generated as a "stub" by the
	 *  gen-snmp-subdriver script. It must be customized!
	 *
	 *  This program is free software; you can redistribute it and/or modify
	 *  it under the terms of the GNU General Public License as published by
	 *  the Free Software Foundation; either version 2 of the License, or
	 *  (at your option) any later version.
	 *
	 *  This program is distributed in the hope that it will be useful,
	 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
	 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	 *  GNU General Public License for more details.
	 *
	 *  You should have received a copy of the GNU General Public License
	 *  along with this program; if not, write to the Free Software
	 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
	 */

	#include "${HFILE}"

	#define ${UDRIVER}_MIB_VERSION  "0.01"

	#define ${UDRIVER}_SYSOID       "${DEVICE_SYSOID}"

	/* To create a value lookup structure (as needed on the 2nd line of the example
	 * below), use the following kind of declaration, outside of the present snmp_info_t[]:
	 * static info_lkp_t onbatt_info[] = {
	 * 	info_lkp_default(1, "OB"),
	 * 	info_lkp_default(2, "OL"),
	 * 	info_lkp_sentinel
	 * };
	 */

	/* ${UDRIVER} Snmp2NUT lookup table */
	static snmp_info_t ${LDRIVER}_mib[] = {

		/* Data format:
		 * snmp_info_default(info_type, info_flags, info_len, OID, dfl, flags, oid2info),
		 *
		 *	info_type:	NUT INFO_ or CMD_ element name
		 *	info_flags:	flags to set in addinfo
		 *	info_len:	length of strings if ST_FLAG_STRING, multiplier otherwise
		 *	OID: SNMP OID or NULL
		 *	dfl: default value
		 *	flags: snmp-ups internal flags (FIXME: ...)
		 *	oid2info: lookup table between OID and NUT values
		 *
		 * Example:
		 * snmp_info_default("input.voltage", 0, 0.1, ".1.3.6.1.4.1.705.1.6.2.1.2.1", "", SU_INPUT_1, NULL),
		 * snmp_info_default("ups.status", ST_FLAG_STRING, SU_INFOSIZE, ".1.3.6.1.4.1.705.1.7.3.0", "", SU_FLAG_OK | SU_STATUS_BATT, onbatt_info),
		 *
		 * To create a value lookup structure (as needed on the 2nd line), use the
		 * following kind of declaration, outside of the present snmp_info_t[]:
		 * static info_lkp_t onbatt_info[] = {
		 * 	info_lkp_default(1, "OB"),
		 * 	info_lkp_default(2, "OL"),
		 * 	info_lkp_sentinel
		 * };
		 */

		/* standard MIB items; if the vendor MIB contains better OIDs for
		 * this (e.g. with daisy-chain support), consider adding those here
		 */
	EOF

	# Same file, indented text (TABs not stripped):
	cat >> "$CFILE" <<EOF
	snmp_info_default("device.description", ST_FLAG_STRING | ST_FLAG_RW, SU_INFOSIZE, ".1.3.6.1.2.1.1.1.0", NULL, SU_FLAG_OK, NULL),
	snmp_info_default("device.contact", ST_FLAG_STRING | ST_FLAG_RW, SU_INFOSIZE, ".1.3.6.1.2.1.1.4.0", NULL, SU_FLAG_OK, NULL),
	snmp_info_default("device.location", ST_FLAG_STRING | ST_FLAG_RW, SU_INFOSIZE, ".1.3.6.1.2.1.1.6.0", NULL, SU_FLAG_OK, NULL),

/* Please revise values discovered by data walk for mappings to
 * docs/nut-names.txt and group the rest under the ifdef below:
 */
#if WITH_UNMAPPED_DATA_POINTS
EOF

	# extract OID string paths, one by one
	LINENB="0"
	while IFS= read -r line; do
		LINENB="`expr $LINENB + 1`"
		FULL_STR_OID="$line"
		STR_OID="`echo \"$line\" | cut -d'.' -f1`"
		echo "$line" | ${GREP} STRING > /dev/null
		if [ $? -eq 0 ]; then
			ST_FLAG_TYPE="ST_FLAG_STRING"
			SU_INFOSIZE="SU_INFOSIZE"
		else
			ST_FLAG_TYPE="0"
			SU_INFOSIZE="1"
		fi
		# get the matching numeric OID
		NUM_OID="`sed -n \"${LINENB}p\" \"${NUMWALKFILE}\" | cut -d' ' -f1`"
		printf "\t/* ${FULL_STR_OID} */\n\tsnmp_info_default(\"unmapped.${STR_OID}\", ${ST_FLAG_TYPE}, ${SU_INFOSIZE}, \"${NUM_OID}\", NULL, SU_FLAG_OK, NULL),\n"
	done < "${STRWALKFILE}" >> "${CFILE}"

	# append footer (TABs not stripped):
	cat >> "$CFILE" <<EOF
#endif	/* if WITH_UNMAPPED_DATA_POINTS */

	/* end of structure. */
	snmp_info_sentinel
};

mib2nut_info_t  ${LDRIVER} = { "${LDRIVER}", ${UDRIVER}_MIB_VERSION, NULL, NULL, ${LDRIVER}_mib, ${UDRIVER}_DEVICE_SYSOID };
EOF

	return
}

# process command line options
while [ $# -gt 0 ]; do
	if [ $# -gt 1 -a "$1" = "-n" ]; then
		DRIVER="$2"
		shift 2
	elif [ $# -gt 1 -a "$1" = "-M" ]; then
		MIBS_DIRLIST="$MIBS_DIRLIST:$2"
		shift 2
	elif [ "$1" = "-k" ]; then
		KEEP=yes
		shift
	elif [ $# -gt 1 -a "$1" = "-H" ]; then
		HOSTNAME="$2"
		shift 2
	elif [ $# -gt 1 -a "$1" = "-c" ]; then
		COMMUNITY="$2"
		shift 2
	elif [ $# -gt 1 -a "$1" = "-s" ]; then
		SYSOID="$2"
		shift 2
	elif echo "$1" | ${GREP} -v '^-' >/dev/null ; then
		if [ $# -gt 1 ]; then
			NUMWALKFILE="$1"
			shift
			STRWALKFILE="$1"
			shift
		else
			NUMWALKFILE="$1"
			shift
			#usage
			#exit 1
		fi
	elif [ "$1" = "--help" -o "$1" = "-h" ]; then
		usage
		exit 0
	else
		echo "Illegal option $1. Try --help for more info." >&2
		exit 1
	fi
done

# check that the needed parameters are provided, depending on the mode
if [ -z "$NUMWALKFILE" ]; then
	# mode 1: directly get SNMP data from a real agent
	echo "Mode 1 selected"
	MODE=1
	NUMWALKFILE="$DFL_NUMWALKFILE"
	STRWALKFILE="$DFL_STRWALKFILE"

	# check if Net SNMP is available
	if [ -z "`command -v snmpget`" -o -z "`command -v snmpwalk`" ] && \
	   [ -z "`which snmpget`" -o -z "`which snmpwalk`" ]; then
		echo "Net SNMP not found! snmpget and snmpwalk commands are required." >&2
		exit 1
	fi
	# hostname is also mandatory
	while [ -z "$HOSTNAME" ]; do
		printf "\n\tPlease enter the SNMP host IP address or name.\n"
		read -p "SNMP host IP name or address: " HOSTNAME < /dev/tty
		if echo "$HOSTNAME" | ${EGREP} '[^a-zA-Z0-9.-]' >/dev/null ; then
			echo "Please use only letters, digits, dash and period character"
			HOSTNAME=""
		fi
	done
	# get data from the agent
	get_snmp_data
else
	# no string walk provided, so mode 3
	if [ -z "$STRWALKFILE" ]; then
		# mode 3: get data from 1 file,
		# Filter according to sysOID on the specific subtree
		# Generate the numeric SNMP walk using this output
		# then use snmptranslate to get the string OIDs and generated the string SNMP walk
		echo "Mode 3 selected"
		MODE=3
		RAWWALKFILE="$NUMWALKFILE"
		NUMWALKFILE="$DFL_NUMWALKFILE"
		STRWALKFILE="$DFL_STRWALKFILE"

		# check for actual file existence
		if [ ! -f "$RAWWALKFILE" ]; then
			echo "SNMP walk dump file is missing on disk. Try --help for more info." >&2
			exit 1
		fi
		# Extract the sysOID
		# Format is "1.3.6.1.2.1.1.2.0 = OID: 1.3.6.1.4.1.4555.1.1.1"
		DEVICE_SYSOID="`${GREP} 1.3.6.1.2.1.1.2.0 \"$RAWWALKFILE\" | cut -d' ' -f4`"
		if [ -n "$DEVICE_SYSOID" ]; then
			echo "Found sysOID $DEVICE_SYSOID"
		else
			echo "SNMP sysOID is missing in file. Try --help for more info." >&2
			exit 1
		fi

		# Switch to the entry point, and extract the subtree
		# Extract the numeric walk
		echo -n "Extracting numeric SNMP walk..."
		${GREP} "$DEVICE_SYSOID" "$RAWWALKFILE" | ${EGREP} -v "1.3.6.1.2.1.1.2.0" 2>/dev/null 1> "$NUMWALKFILE"
		echo " done"

		# Create the string walk from a translation of the numeric one
		echo -n "Converting string SNMP walk..."
		while IFS=' = ' read NUM_OID OID_VALUE
		do
			STR_OID="`snmptranslate -Os  -m ALL -M+. \"$NUM_OID\" 2>/dev/null`"
			# Uncomment the below line to get debug logs
			#echo "Got: $STR_OID = $OID_VALUE"
			printf "."
			echo "$STR_OID = $OID_VALUE" >> "$STRWALKFILE"
		done < "$NUMWALKFILE"
		echo " done"
	else
		# mode 2: get data from files
		echo "Mode 2 selected"
		MODE=2

		# get sysOID value from command line, if needed
		while [ -z "$SYSOID" ]; do
			echo "
Please enter the value of sysOID, as displayed by snmp-ups. For example '.1.3.6.1.4.1.2254.2.4'.
You can get it using: snmpget -v1 -c XXX <host> $SYSOID_NUMBER"
			read -p "Value of sysOID: " SYSOID < /dev/tty
			if echo "$SYSOID" | ${EGREP} '[^0-9.]' >/dev/null ; then
				echo "Please use only the numeric form, with dots and digits"
				SYSOID=""
			fi
		done
		# check for actual files existence
		if [ ! -f "$NUMWALKFILE" -o  ! -f "$STRWALKFILE" ]; then
			echo "SNMP walk dump files are missing on disk. Try --help for more info." >&2
			exit 1
		fi
	fi
fi

# delete temporary files: this is called just before exiting.
cleanup () {
	rm -f "$DEBUG $DFL_NUMWALKFILE $TMP_NUMWALKFILE $DFL_STRWALKFILE $TMP_STRWALKFILE"
}
if [ -n "$KEEP" ]; then
	trap cleanup EXIT
fi

# prompt use for name of driver
while [ -z "$DRIVER" ]; do
	echo "
Please enter a name for this driver. Use only letters and numbers. Use
natural (upper- and lowercase) capitalization, e.g., 'Belkin', 'APC'."
	read -p "Name of subdriver: " DRIVER < /dev/tty
	if echo "$DRIVER" | ${EGREP} '[^a-zA-Z0-9]' >/dev/null ; then
		echo "Please use only letters and digits"
		DRIVER=""
	fi
done

# remove blank and "End of MIB" lines
TABCHAR="`printf '\t'`"
${EGREP} -v "^[ ${TABCHAR}]?\$" "${NUMWALKFILE}" | ${GREP} -v "End of MIB" > "${TMP_NUMWALKFILE}"
${EGREP} -v "^[ ${TABCHAR}]?\$" "${STRWALKFILE}" | ${GREP} -v "End of MIB" > "${TMP_STRWALKFILE}"
NUMWALKFILE="${TMP_NUMWALKFILE}"
STRWALKFILE="${TMP_STRWALKFILE}"

# FIXME: sanity checks (! -z contents -a same `wc -l`)
NUM_OID_COUNT="`cat \"$NUMWALKFILE\" | wc -l`"
STR_OID_COUNT="`cat \"$STRWALKFILE\" | wc -l`"

echo "SNMP OIDs extracted = $NUM_OID_COUNT / $NUM_OID_COUNT"

generate_C

# Display the remaining tasks
cat <<EOF
Done.

For C-style integration, do not forget to:
* bump DRIVER_VERSION in snmp-ups.c (add "0.01")
* copy "${HFILE}" and "${CFILE}" to "../../drivers"
* add #include "${HFILE}" to drivers/snmp-ups.c
* add &${LDRIVER} to drivers/snmp-ups.c:mib2nut[] list,
* add ${LDRIVER}-mib.c to snmp_ups_SOURCES in drivers/Makefile.am
* add ${LDRIVER}-mib.h to dist_noinst_HEADERS in drivers/Makefile.am
* "./autogen.sh && ./configure && make" from the top level directory
EOF
