Repository: kentik/ktranslate
Pull request #131 (2021-09-14 21:59:58)

Extending conversion code to handle powerset ascii bitmap
Goes from 

```
.1.3.6.1.4.1.318.1.1.1.11.1.1.0 = STRING: "0001010000000000001000000000000000000000000000000000000000000000"
```

To `Disaster: No Batteries Attached`

And other informational fields. Value of this metric is:

```
		"Informational": 1,
		"Warning":       2,
		"High":          3,
		"Disaster":      4,
```

Corresponding profile entry:

```
  - MIB: PowerNet-MIB_UPS
    symbol:
      OID: 1.3.6.1.4.1.318.1.1.1.11.1.1.0
      name: upsBasicStateOutputState
      conversion: powerset_status
```



Patch:
diff --git a/pkg/inputs/snmp/metadata/device_metadata.go b/pkg/inputs/snmp/metadata/device_metadata.go
index 93b6047..9aaf9c5 100644
--- a/pkg/inputs/snmp/metadata/device_metadata.go
+++ b/pkg/inputs/snmp/metadata/device_metadata.go
@@ -112,7 +112,17 @@ func GetDeviceMetadata(log logger.ContextL, server *gosnmp.GoSNMP, deviceMetadat
 			case string:
 				md.Customs[oidName] = vt
 			case []byte:
-				md.Customs[oidName] = string(vt)
+				if oid.Conversion != "" { // Adjust for any hard coded values here.
+					ival, sval := snmp_util.GetFromConv(pdu, oid.Conversion, log)
+					if ival > 0 {
+						md.CustomInts[oidName] = ival
+						md.Customs[kt.StringPrefix+oidName] = sval
+					} else {
+						md.Customs[oidName] = sval
+					}
+				} else {
+					md.Customs[oidName] = string(vt)
+				}
 			case net.IP:
 				md.Customs[oidName] = vt.String()
 			default:
@@ -168,7 +178,7 @@ func getTable(log logger.ContextL, g *gosnmp.GoSNMP, oid string, mib *kt.Mib, md
 		case gosnmp.OctetString:
 			value := string(variable.Value.([]byte))
 			if mib.Conversion != "" { // Adjust for any hard coded values here.
-				value = snmp_util.GetFromConv(variable, mib.Conversion, log)
+				_, value = snmp_util.GetFromConv(variable, mib.Conversion, log)
 			}
 			md.Tables[idx].Customs[oidName] = value
 		case gosnmp.IPAddress: // Does this work?
diff --git a/pkg/inputs/snmp/metadata/interface_metadata.go b/pkg/inputs/snmp/metadata/interface_metadata.go
index b061ed0..ca158e9 100644
--- a/pkg/inputs/snmp/metadata/interface_metadata.go
+++ b/pkg/inputs/snmp/metadata/interface_metadata.go
@@ -316,7 +316,8 @@ func (im *InterfaceMetadata) Poll(ctx context.Context, conf *kt.SnmpDeviceConfig
 				case SNMP_ifPhysAddress:
 					switch variable.Type {
 					case gosnmp.OctetString:
-						data.ExtraInfo[SNMP_ifPhysAddress] = snmp_util.GetFromConv(variable, "hwaddr", im.log)
+						_, sval := snmp_util.GetFromConv(variable, "hwaddr", im.log)
+						data.ExtraInfo[SNMP_ifPhysAddress] = sval
 					}
 				case SNMP_ifLastChange:
 					val := gosnmp.ToBigInt(variable.Value).Uint64()
diff --git a/pkg/inputs/snmp/metrics/device_metrics.go b/pkg/inputs/snmp/metrics/device_metrics.go
index fa87401..ab48df6 100644
--- a/pkg/inputs/snmp/metrics/device_metrics.go
+++ b/pkg/inputs/snmp/metrics/device_metrics.go
@@ -161,7 +161,14 @@ func (dm *DeviceMetrics) pollFromConfig(ctx context.Context, server *gosnmp.GoSN
 		case gosnmp.OctetString, gosnmp.BitString:
 			value := string(variable.Value.([]byte))
 			if mib.Conversion != "" { // Adjust for any hard coded values here.
-				value = snmp_util.GetFromConv(variable, mib.Conversion, dm.log)
+				ival, sval := snmp_util.GetFromConv(variable, mib.Conversion, dm.log)
+				if ival > 0 {
+					dmr.customBigInt[oidName] = ival
+					dmr.customStr[kt.StringPrefix+oidName] = sval
+					continue // we have everything we need, no need to continue processing.
+				} else {
+					value = sval
+				}
 			}
 			if mib.Enum != nil {
 				dmr.customStr[kt.StringPrefix+oidName] = value // Save the string valued field as an attribute.
diff --git a/pkg/inputs/snmp/util/util.go b/pkg/inputs/snmp/util/util.go
index d5676ea..c3f519f 100644
--- a/pkg/inputs/snmp/util/util.go
+++ b/pkg/inputs/snmp/util/util.go
@@ -13,9 +13,16 @@ import (
 
 	"github.com/gosnmp/gosnmp"
 	"github.com/kentik/ktranslate/pkg/eggs/logger"
+	"github.com/kentik/ktranslate/pkg/inputs/snmp/util/vendor"
 	"github.com/kentik/ktranslate/pkg/kt"
 )
 
+const (
+	CONV_HWADDR   = "hwaddr"
+	CONV_POWERSET = "powerset_status"
+	CONV_HEXTOINT = "hextoint"
+)
+
 var (
 	SNMP_POLL_SLEEP_TIME = 10 * time.Second
 
@@ -176,7 +183,8 @@ func GetDeviceManufacturer(server snmpWalker, log logger.ContextL) string {
 func PrettyPrint(pdu gosnmp.SnmpPDU, format string, log logger.ContextL) string {
 	switch pdu.Type {
 	case gosnmp.OctetString:
-		return GetFromConv(pdu, format, log)
+		_, s := GetFromConv(pdu, format, log)
+		return s
 	case gosnmp.IPAddress:
 		return pdu.Value.(string)
 	case gosnmp.ObjectIdentifier:
@@ -213,7 +221,7 @@ func DoWalk(device string, baseOid string, format string, conf *kt.SnmpConfig, c
 }
 
 // Handle the case of wierd ints encoded as byte arrays.
-func GetFromConv(pdu gosnmp.SnmpPDU, conv string, log logger.ContextL) string {
+func GetFromConv(pdu gosnmp.SnmpPDU, conv string, log logger.ContextL) (int64, string) {
 	defer func() {
 		if r := recover(); r != nil {
 			log.Warnf("Invalid Conversion: %s %v %v", pdu.Name, pdu.Value, r)
@@ -222,49 +230,51 @@ func GetFromConv(pdu gosnmp.SnmpPDU, conv string, log logger.ContextL) string {
 
 	bv, ok := pdu.Value.([]byte)
 	if !ok || len(bv) == 0 {
-		return ""
+		return 0, ""
 	}
 
-	// If there's an encoded mac addr here.
-	if conv == "hwaddr" {
-		return net.HardwareAddr(bv).String()
-	}
-
-	// Otherwise, try out some custom conversions.
-	split := strings.Split(conv, ":")
-	if split[0] == "hextoint" && len(split) == 3 {
-		endian := split[1]
-		bit := split[2]
-
-		if endian == "LittleEndian" {
-			switch bit {
-			case "uint64":
-				return fmt.Sprintf("%d", binary.LittleEndian.Uint64(bv))
-			case "uint32":
-				return fmt.Sprintf("%d", binary.LittleEndian.Uint32(bv))
-			case "uint16":
-				return fmt.Sprintf("%d", binary.LittleEndian.Uint16(bv))
-			default:
-				log.Errorf("invalid bit value (%s) for hex to int conversion", bit)
-				return ""
-			}
-		} else if endian == "BigEndian" {
-			switch bit {
-			case "uint64":
-				return fmt.Sprintf("%d", binary.BigEndian.Uint64(bv))
-			case "uint32":
-				return fmt.Sprintf("%d", binary.BigEndian.Uint32(bv))
-			case "uint16":
-				return fmt.Sprintf("%d", binary.BigEndian.Uint16(bv))
-			default:
-				log.Errorf("invalid bit value (%s) for hex to int conversion", bit)
-				return ""
+	switch conv {
+	case CONV_HWADDR: // If there's an encoded mac addr here.
+		return 0, net.HardwareAddr(bv).String()
+	case CONV_POWERSET:
+		return vendor.HandlePowersetStatus(bv)
+	default:
+		// Otherwise, try out some custom conversions.
+		split := strings.Split(conv, ":")
+		if split[0] == CONV_HEXTOINT && len(split) == 3 {
+			endian := split[1]
+			bit := split[2]
+
+			if endian == "LittleEndian" {
+				switch bit {
+				case "uint64":
+					return 0, fmt.Sprintf("%d", binary.LittleEndian.Uint64(bv))
+				case "uint32":
+					return 0, fmt.Sprintf("%d", binary.LittleEndian.Uint32(bv))
+				case "uint16":
+					return 0, fmt.Sprintf("%d", binary.LittleEndian.Uint16(bv))
+				default:
+					log.Errorf("invalid bit value (%s) for hex to int conversion", bit)
+					return 0, ""
+				}
+			} else if endian == "BigEndian" {
+				switch bit {
+				case "uint64":
+					return 0, fmt.Sprintf("%d", binary.BigEndian.Uint64(bv))
+				case "uint32":
+					return 0, fmt.Sprintf("%d", binary.BigEndian.Uint32(bv))
+				case "uint16":
+					return 0, fmt.Sprintf("%d", binary.BigEndian.Uint16(bv))
+				default:
+					log.Errorf("invalid bit value (%s) for hex to int conversion", bit)
+					return 0, ""
+				}
+			} else {
+				log.Errorf("invalid Endian value (%s) for hex to int conversion", endian)
+				return 0, ""
 			}
-		} else {
-			log.Errorf("invalid Endian value (%s) for hex to int conversion", endian)
-			return ""
 		}
 	}
 
-	return string(bv) // Default down to here.
+	return 0, string(bv) // Default down to here.
 }
diff --git a/pkg/inputs/snmp/util/vendor/powerset.go b/pkg/inputs/snmp/util/vendor/powerset.go
new file mode 100644
index 0000000..bce948e
--- /dev/null
+++ b/pkg/inputs/snmp/util/vendor/powerset.go
@@ -0,0 +1,135 @@
+package vendor
+
+import (
+	"strings"
+)
+
+func HandlePowersetStatus(bv []byte) (int64, string) {
+	actives := make([]*UpsCode, len(upsCodes))
+
+	for i, v := range bv {
+		switch string(v) {
+		case "0":
+		case "1":
+			actives[i] = &upsCodes[i]
+		}
+	}
+
+	// Track what is set in the system.
+	severity := map[int][]string{}
+
+	for _, active := range actives {
+		if active == nil {
+			continue
+		}
+		if active.Dependancy > 0 {
+			if actives[active.Dependancy] != nil {
+				// Ignore because this dependancy is set.
+			} else {
+				if severity[active.Severity] == nil {
+					severity[active.Severity] = []string{active.Status}
+				} else {
+					severity[active.Severity] = append(severity[active.Severity], active.Status)
+				}
+			}
+		} else { // No dependency so set right away.
+			if severity[active.Severity] == nil {
+				severity[active.Severity] = []string{active.Status}
+			} else {
+				severity[active.Severity] = append(severity[active.Severity], active.Status)
+			}
+		}
+	}
+
+	for i := len(severities); i > 0; i-- {
+		if len(severity[i]) > 0 { // Return the highest severity present which has flags set.
+			return int64(i), severities[i-1] + ": " + strings.Join(severity[i], ",")
+		}
+	}
+
+	// Generally assume you don't get here.
+	return 0, ""
+}
+
+type UpsCode struct {
+	Severity   int
+	Index      int
+	Dependancy int
+	Status     string
+}
+
+var (
+	severities = []string{
+		"Informational",
+		"Warning",
+		"High",
+		"Disaster",
+	}
+
+	upsCodes = []UpsCode{
+		UpsCode{Severity: 2, Index: 0, Dependancy: 0, Status: "Abnormal Condition Present"},
+		UpsCode{Severity: 3, Index: 1, Dependancy: 28, Status: "On Battery"},
+		UpsCode{Severity: 3, Index: 2, Dependancy: 0, Status: "Low Battery"},
+		UpsCode{Severity: 1, Index: 3, Dependancy: 0, Status: "On Line"},
+		UpsCode{Severity: 3, Index: 4, Dependancy: 0, Status: "Replace Battery"},
+		UpsCode{Severity: 1, Index: 5, Dependancy: 0, Status: "Serial Communication Established"},
+		UpsCode{Severity: 1, Index: 6, Dependancy: 0, Status: "AVR Boost Active"},
+		UpsCode{Severity: 1, Index: 7, Dependancy: 0, Status: "AVR Trim Active"},
+		UpsCode{Severity: 3, Index: 8, Dependancy: 28, Status: "Overload"},
+		UpsCode{Severity: 1, Index: 9, Dependancy: 0, Status: "Runtime Calibration "},
+		UpsCode{Severity: 3, Index: 10, Dependancy: 28, Status: "Batteries Discharged"},
+		UpsCode{Severity: 2, Index: 11, Dependancy: 0, Status: "Manual Bypass"},
+		UpsCode{Severity: 2, Index: 12, Dependancy: 0, Status: "Software Bypass"},
+		UpsCode{Severity: 3, Index: 13, Dependancy: 0, Status: "In Bypass due to Internal Fault"},
+		UpsCode{Severity: 3, Index: 14, Dependancy: 0, Status: "In Bypass due to Supply Failure"},
+		UpsCode{Severity: 3, Index: 15, Dependancy: 0, Status: "In Bypass due to Fan Failure"},
+		UpsCode{Severity: 1, Index: 16, Dependancy: 0, Status: "Sleeping on a Timer"},
+		UpsCode{Severity: 2, Index: 17, Dependancy: 0, Status: "Sleeping until Utility Power Returns"},
+		UpsCode{Severity: 1, Index: 18, Dependancy: 0, Status: "Powered On"},
+		UpsCode{Severity: 2, Index: 19, Dependancy: 0, Status: "Rebooting"},
+		UpsCode{Severity: 2, Index: 20, Dependancy: 0, Status: "Battery Communication Lost"},
+		UpsCode{Severity: 1, Index: 21, Dependancy: 0, Status: "Graceful Shutdown Initiated"},
+		UpsCode{Severity: 1, Index: 22, Dependancy: 0, Status: "Smart Boost or Smart Trim Fault"},
+		UpsCode{Severity: 3, Index: 23, Dependancy: 28, Status: "Bad Output Voltage"},
+		UpsCode{Severity: 3, Index: 24, Dependancy: 0, Status: "Battery Charger Failure"},
+		UpsCode{Severity: 2, Index: 25, Dependancy: 28, Status: "High Battery Temperature"},
+		UpsCode{Severity: 3, Index: 26, Dependancy: 0, Status: "Warning Battery Temperature"},
+		UpsCode{Severity: 4, Index: 27, Dependancy: 0, Status: "Critical Battery Temperature"},
+		UpsCode{Severity: 2, Index: 28, Dependancy: 0, Status: "Self Test In Progress"},
+		UpsCode{Severity: 3, Index: 29, Dependancy: 28, Status: "Low Battery / On Battery"},
+		UpsCode{Severity: 1, Index: 30, Dependancy: 0, Status: "Graceful Shutdown Issued by Upstream Device"},
+		UpsCode{Severity: 1, Index: 31, Dependancy: 0, Status: "Graceful Shutdown Issued by Downstream Device"},
+		UpsCode{Severity: 4, Index: 32, Dependancy: 0, Status: "No Batteries Attached"},
+		UpsCode{Severity: 1, Index: 33, Dependancy: 0, Status: "Synchronized Command is in Progress"},
+		UpsCode{Severity: 1, Index: 34, Dependancy: 0, Status: "Synchronized Sleeping Command is in Progress"},
+		UpsCode{Severity: 1, Index: 35, Dependancy: 0, Status: "Synchronized Rebooting Command is in Progress "},
+		UpsCode{Severity: 2, Index: 36, Dependancy: 0, Status: "Inverter DC Imbalance "},
+		UpsCode{Severity: 3, Index: 37, Dependancy: 0, Status: "Transfer Relay Failure"},
+		UpsCode{Severity: 3, Index: 38, Dependancy: 0, Status: "Shutdown or Unable to Transfer"},
+		UpsCode{Severity: 3, Index: 39, Dependancy: 0, Status: "Low Battery Shutdown"},
+		UpsCode{Severity: 3, Index: 40, Dependancy: 0, Status: "Electronic Unit Fan Failure"},
+		UpsCode{Severity: 3, Index: 41, Dependancy: 0, Status: "Main Relay Failure"},
+		UpsCode{Severity: 3, Index: 42, Dependancy: 0, Status: "Bypass Relay Failure"},
+		UpsCode{Severity: 2, Index: 43, Dependancy: 0, Status: "Temporary Bypass"},
+		UpsCode{Severity: 3, Index: 44, Dependancy: 0, Status: "High Internal Temperature"},
+		UpsCode{Severity: 3, Index: 45, Dependancy: 0, Status: "Battery Temperature Sensor Fault"},
+		UpsCode{Severity: 2, Index: 46, Dependancy: 0, Status: "Input Out of Range for Bypass"},
+		UpsCode{Severity: 3, Index: 47, Dependancy: 0, Status: "DC Bus Overvoltage"},
+		UpsCode{Severity: 2, Index: 48, Dependancy: 0, Status: "PFC Failure"},
+		UpsCode{Severity: 3, Index: 49, Dependancy: 0, Status: "Critical Hardware Fault"},
+		UpsCode{Severity: 1, Index: 50, Dependancy: 0, Status: "Green Mode/ECO Mode"},
+		UpsCode{Severity: 1, Index: 51, Dependancy: 0, Status: "Hot Standby"},
+		UpsCode{Severity: 3, Index: 52, Dependancy: 0, Status: "Emergency Power Off (EPO) Activated"},
+		UpsCode{Severity: 2, Index: 53, Dependancy: 0, Status: "Load Alarm Violation"},
+		UpsCode{Severity: 3, Index: 54, Dependancy: 0, Status: "Bypass Phase Fault"},
+		UpsCode{Severity: 2, Index: 55, Dependancy: 0, Status: "UPS Internal Communication Failure"},
+		UpsCode{Severity: 1, Index: 56, Dependancy: 0, Status: "Efficiency Booster Mode"},
+		UpsCode{Severity: 2, Index: 57, Dependancy: 0, Status: "Off"},
+		UpsCode{Severity: 2, Index: 58, Dependancy: 0, Status: "Standby"},
+		UpsCode{Severity: 1, Index: 59, Dependancy: 0, Status: "Not Used"},
+		UpsCode{Severity: 1, Index: 60, Dependancy: 0, Status: "Not Used"},
+		UpsCode{Severity: 1, Index: 61, Dependancy: 0, Status: "Not Used"},
+		UpsCode{Severity: 1, Index: 62, Dependancy: 0, Status: "Not Used"},
+		UpsCode{Severity: 1, Index: 63, Dependancy: 0, Status: "Not Used"},
+	}
+)


Test patch:
diff --git a/pkg/inputs/snmp/util/util_test.go b/pkg/inputs/snmp/util/util_test.go
index 4ddc8ee..80397c1 100644
--- a/pkg/inputs/snmp/util/util_test.go
+++ b/pkg/inputs/snmp/util/util_test.go
@@ -7,9 +7,9 @@ import (
 
 	"github.com/stretchr/testify/assert"
 
+	"github.com/gosnmp/gosnmp"
 	"github.com/kentik/ktranslate/pkg/eggs/logger"
 	lt "github.com/kentik/ktranslate/pkg/eggs/logger/testing"
-	"github.com/gosnmp/gosnmp"
 )
 
 type testWalker struct {
@@ -84,3 +84,61 @@ func TestGetIndex(t *testing.T) {
 	assert.Equal(".4.5.6", GetIndex("1.2.3.4.5.6", "1.2.3"))
 	assert.Equal(".4.5.6", GetIndex(".1.2.3.4.5.6", "1.2.3"))
 }
+
+func TestHandlePowerset(t *testing.T) {
+	assert := assert.New(t)
+	l := lt.NewTestContextL(logger.NilContext, t)
+
+	tests := map[string][]byte{
+		"Informational: On Line,Serial Communication Established,Powered On": []byte("0001010000000000001000000000000000000000000000000000000000000000"),
+		"Informational: On Line,Powered On":                                  []byte("0001000000000000001000000000000000000000000000000000000000000000"),
+		"Disaster: No Batteries Attached":                                    []byte("0001010000000000001000000000000010000000000000000000000000000000"),
+		"Warning: Self Test In Progress":                                     []byte("0101010000000000001000000000110000000000000000000000000000000000"),
+		"High: On Battery,Low Battery / On Battery":                          []byte("0101010000000000001000000000010000000000000000000000000000000000"), // No self test.
+	}
+
+	levels := map[string]int64{
+		"Informational": 1,
+		"Disaster":      4,
+		"Warning":       2,
+		"High":          3,
+	}
+
+	for expt, in := range tests {
+		pdu := gosnmp.SnmpPDU{Value: in}
+		val, str := GetFromConv(pdu, "powerset_status", l)
+		assert.Equal(expt, str, "%s", in)
+		pts := strings.SplitN(str, ":", 2)
+		assert.Equal(levels[pts[0]], val, "%s", in)
+	}
+}
+
+func TestHWAddr(t *testing.T) {
+	assert := assert.New(t)
+	l := lt.NewTestContextL(logger.NilContext, t)
+
+	tests := map[string][]byte{
+		"39:30:3a:36:31:3a:61:65:3a:66:62:3a:63:32:3a:31:39": []byte("90:61:ae:fb:c2:19"),
+	}
+
+	for expt, in := range tests {
+		pdu := gosnmp.SnmpPDU{Value: in}
+		_, str := GetFromConv(pdu, "hwaddr", l)
+		assert.Equal(expt, str, "%s", in)
+	}
+}
+
+func TestHexToInt(t *testing.T) {
+	assert := assert.New(t)
+	l := lt.NewTestContextL(logger.NilContext, t)
+
+	tests := map[string][]byte{
+		"7090466131453292601": []byte("9061aefbc219"),
+	}
+
+	for expt, in := range tests {
+		pdu := gosnmp.SnmpPDU{Value: in}
+		_, str := GetFromConv(pdu, "hextoint:LittleEndian:uint64", l)
+		assert.Equal(expt, str, "%s", in)
+	}
+}