diff --git a/.goreleaser.yml b/.goreleaser.yml index 160f3c6..d8e889e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -31,7 +31,7 @@ nfpms: - id: "thola" homepage: https://thola.io maintainer: team@thola.io - description: Network Monitoring and Provisioning Tool. + description: Network Monitoring Tool license: BSD-2-clause formats: - deb diff --git a/cmd/check_interface_metrics.go b/cmd/check_interface_metrics.go index f129d74..3eb5e82 100644 --- a/cmd/check_interface_metrics.go +++ b/cmd/check_interface_metrics.go @@ -12,6 +12,7 @@ func init() { checkCMD.AddCommand(checkInterfaceMetricsCMD) checkInterfaceMetricsCMD.Flags().Bool("print-interfaces", false, "Print interfaces to plugin output") + checkInterfaceMetricsCMD.Flags().Bool("snmp-gets-instead-of-walk", false, "Use SNMP Gets instead of Walks") checkInterfaceMetricsCMD.Flags().String("ifDescr-regex", "", "Apply a regex on the ifDescr of the interfaces. Use it together with the 'ifDescr-regex-replace' flag") checkInterfaceMetricsCMD.Flags().String("ifDescr-regex-replace", "", "Apply a regex on the ifDescr of the interfaces. Use it together with the 'ifDescr-regex' flag") checkInterfaceMetricsCMD.Flags().StringSlice("ifType-filter", []string{}, "Filter out interfaces which ifType equals the given types") @@ -28,6 +29,10 @@ var checkInterfaceMetricsCMD = &cobra.Command{ if err != nil { log.Fatal().Err(err).Msg("print-interfaces needs to be a boolean") } + snmpGetsInsteadOfWalk, err := cmd.Flags().GetBool("snmp-gets-instead-of-walk") + if err != nil { + log.Fatal().Err(err).Msg("snmp-gets-instead-of-walk needs to be a boolean") + } ifDescrRegex, err := cmd.Flags().GetString("ifDescr-regex") if err != nil { log.Fatal().Err(err).Msg("ifDescr-regex needs to be a string") @@ -51,13 +56,14 @@ var checkInterfaceMetricsCMD = &cobra.Command{ var nullString *string r := request.CheckInterfaceMetricsRequest{ - CheckDeviceRequest: getCheckDeviceRequest(args[0]), - PrintInterfaces: printInterfaces, - IfDescrRegex: utility.IfThenElse(cmd.Flags().Changed("ifDescr-regex"), &ifDescrRegex, nullString).(*string), - IfDescrRegexReplace: utility.IfThenElse(cmd.Flags().Changed("ifDescr-regex-replace"), &ifDescrRegexReplace, nullString).(*string), - IfTypeFilter: ifTypeFilter, - IfNameFilter: ifNameFilter, - IfDescrFilter: ifDescrFilter, + CheckDeviceRequest: getCheckDeviceRequest(args[0]), + PrintInterfaces: printInterfaces, + IfDescrRegex: utility.IfThenElse(cmd.Flags().Changed("ifDescr-regex"), &ifDescrRegex, nullString).(*string), + IfDescrRegexReplace: utility.IfThenElse(cmd.Flags().Changed("ifDescr-regex-replace"), &ifDescrRegexReplace, nullString).(*string), + IfTypeFilter: ifTypeFilter, + IfNameFilter: ifNameFilter, + IfDescrFilter: ifDescrFilter, + SNMPGetsInsteadOfWalk: snmpGetsInsteadOfWalk, } handleRequest(&r) diff --git a/config/codecommunicator/adva.go b/config/codecommunicator/adva.go index 60f94c4..00622ea 100644 --- a/config/codecommunicator/adva.go +++ b/config/codecommunicator/adva.go @@ -3,6 +3,7 @@ package codecommunicator import ( "context" "fmt" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/inexio/thola/internal/network" "github.com/pkg/errors" @@ -17,8 +18,8 @@ type advaCommunicator struct { } // GetInterfaces returns the interfaces of adva devices. -func (c *advaCommunicator) GetInterfaces(ctx context.Context) ([]device.Interface, error) { - interfaces, err := c.parent.GetInterfaces(ctx) +func (c *advaCommunicator) GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) { + interfaces, err := c.parent.GetInterfaces(ctx, filter...) if err != nil { return nil, err } diff --git a/config/codecommunicator/ceraos-ip10.go b/config/codecommunicator/ceraos-ip10.go index 92caee3..9dffcd2 100644 --- a/config/codecommunicator/ceraos-ip10.go +++ b/config/codecommunicator/ceraos-ip10.go @@ -2,6 +2,7 @@ package codecommunicator import ( "context" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/pkg/errors" "regexp" @@ -14,7 +15,7 @@ type ceraosIP10Communicator struct { // GetInterfaces returns the interfaces of ceraos/ip10 devices. // These devices need special behavior radio and ethernet interfaces. -func (c *ceraosIP10Communicator) GetInterfaces(ctx context.Context) ([]device.Interface, error) { +func (c *ceraosIP10Communicator) GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) { subInterfaces, err := c.parent.GetInterfaces(ctx) if err != nil { return nil, errors.Wrap(err, "an unexpected error occurred while trying to get ifTable of sub communicator") @@ -74,5 +75,5 @@ func (c *ceraosIP10Communicator) GetInterfaces(ctx context.Context) ([]device.In } } - return subInterfaces, nil + return filterInterfaces(subInterfaces, filter) } diff --git a/config/codecommunicator/code_communicator.go b/config/codecommunicator/code_communicator.go index 0267f10..2aa4d7e 100644 --- a/config/codecommunicator/code_communicator.go +++ b/config/codecommunicator/code_communicator.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "github.com/inexio/thola/internal/communicator/communicator" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/inexio/thola/internal/tholaerr" "github.com/pkg/errors" + "regexp" ) type codeCommunicator struct { @@ -69,7 +71,7 @@ func (c *codeCommunicator) GetOSVersion(_ context.Context) (string, error) { return "", tholaerr.NewNotImplementedError("function is not implemented for this communicator") } -func (c *codeCommunicator) GetInterfaces(_ context.Context) ([]device.Interface, error) { +func (c *codeCommunicator) GetInterfaces(_ context.Context, _ ...filter.PropertyFilter) ([]device.Interface, error) { return nil, tholaerr.NewNotImplementedError("function is not implemented for this communicator") } @@ -192,3 +194,68 @@ func (c *codeCommunicator) GetHardwareHealthComponentPowerSupply(_ context.Conte func (c *codeCommunicator) GetSBCComponentSystemHealthScore(_ context.Context) (int, error) { return 0, tholaerr.NewNotImplementedError("function is not implemented for this communicator") } + +func filterInterfaces(interfaces []device.Interface, filter []filter.PropertyFilter) ([]device.Interface, error) { + if len(filter) == 0 { + return interfaces, nil + } + + var ifDescrFilter, ifNameFilter, ifTypeFilter []*regexp.Regexp + + for _, fil := range filter { + switch key := fil.Key; key { + case "ifDescr": + regex, err := regexp.Compile(fil.Regex) + if err != nil { + return nil, errors.Wrap(err, "failed to compile ifDescr regex") + } + ifDescrFilter = append(ifDescrFilter, regex) + case "ifName": + regex, err := regexp.Compile(fil.Regex) + if err != nil { + return nil, errors.Wrap(err, "failed to compile ifName regex") + } + ifNameFilter = append(ifNameFilter, regex) + case "ifType": + regex, err := regexp.Compile(fil.Regex) + if err != nil { + return nil, errors.Wrap(err, "failed to compile ifType regex") + } + ifTypeFilter = append(ifTypeFilter, regex) + default: + return nil, errors.New("unknown filter key: " + key) + } + } + + var res []device.Interface +out: + for _, interf := range interfaces { + if interf.IfDescr != nil { + for _, fil := range ifDescrFilter { + if fil.MatchString(*interf.IfDescr) { + continue out + } + } + } + + if interf.IfName != nil { + for _, fil := range ifNameFilter { + if fil.MatchString(*interf.IfName) { + continue out + } + } + } + + if interf.IfType != nil { + for _, fil := range ifTypeFilter { + if fil.MatchString(*interf.IfType) { + continue out + } + } + } + + res = append(res, interf) + } + + return res, nil +} diff --git a/config/codecommunicator/ekinops.go b/config/codecommunicator/ekinops.go index 91e003b..6c3b5be 100644 --- a/config/codecommunicator/ekinops.go +++ b/config/codecommunicator/ekinops.go @@ -3,6 +3,7 @@ package codecommunicator import ( "context" "fmt" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/inexio/thola/internal/network" "github.com/pkg/errors" @@ -15,7 +16,7 @@ type ekinopsCommunicator struct { } // GetInterfaces returns the interfaces of ekinops devices. -func (c *ekinopsCommunicator) GetInterfaces(ctx context.Context) ([]device.Interface, error) { +func (c *ekinopsCommunicator) GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) { con, ok := network.DeviceConnectionFromContext(ctx) if !ok || con.SNMP == nil { return nil, errors.New("no device connection available") @@ -68,7 +69,12 @@ func (c *ekinopsCommunicator) GetInterfaces(ctx context.Context) ([]device.Inter } } - return normalizeEkinopsInterfaces(interfaces) + interfaces, err = c.normalizeInterfaces(interfaces) + if err != nil { + return nil, errors.Wrap(err, "failed to normalize interfaces") + } + + return filterInterfaces(interfaces, filter) } func ekinopsInterfacesIfIdentifierToSliceIndex(interfaces []device.Interface) (map[string]int, error) { @@ -88,7 +94,7 @@ func ekinopsInterfacesIfIdentifierToSliceIndex(interfaces []device.Interface) (m return m, nil } -func normalizeEkinopsInterfaces(interfaces []device.Interface) ([]device.Interface, error) { +func (c *ekinopsCommunicator) normalizeInterfaces(interfaces []device.Interface) ([]device.Interface, error) { var res []device.Interface for _, interf := range interfaces { diff --git a/config/codecommunicator/junos.go b/config/codecommunicator/junos.go index 0388c00..04c7eee 100644 --- a/config/codecommunicator/junos.go +++ b/config/codecommunicator/junos.go @@ -3,6 +3,7 @@ package codecommunicator import ( "context" "fmt" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/inexio/thola/internal/network" "github.com/pkg/errors" @@ -14,16 +15,16 @@ type junosCommunicator struct { codeCommunicator } -func (c *junosCommunicator) GetInterfaces(ctx context.Context) ([]device.Interface, error) { - interfaces, err := c.deviceClass.GetInterfaces(ctx) +func (c *junosCommunicator) GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) { + interfaces, err := c.deviceClass.GetInterfaces(ctx, filter...) if err != nil { return nil, err } - interfacesWithVLANs, err := juniperAddVLANsNonELS(ctx, interfaces) + interfacesWithVLANs, err := c.addVLANsNonELS(ctx, interfaces) if err != nil { log.Ctx(ctx).Debug().Err(err).Msg("getting juniper VLANs for non ELS devices failed, trying for ELS devices") - interfacesWithVLANs, err = juniperAddVLANsELS(ctx, interfaces) + interfacesWithVLANs, err = c.addVLANsELS(ctx, interfaces) if err != nil { log.Ctx(ctx).Debug().Err(err).Msg("getting juniper VLANs for ELS devices failed, skipping VLANs") interfacesWithVLANs = interfaces @@ -33,7 +34,7 @@ func (c *junosCommunicator) GetInterfaces(ctx context.Context) ([]device.Interfa return interfacesWithVLANs, nil } -func juniperAddVLANsELS(ctx context.Context, interfaces []device.Interface) ([]device.Interface, error) { +func (c *junosCommunicator) addVLANsELS(ctx context.Context, interfaces []device.Interface) ([]device.Interface, error) { con, ok := network.DeviceConnectionFromContext(ctx) if !ok || con.SNMP == nil { return nil, errors.New("snmp client is empty") @@ -80,7 +81,7 @@ func juniperAddVLANsELS(ctx context.Context, interfaces []device.Interface) ([]d } } - portIfIndex, err := juniperGetPortIfIndexMapping(ctx) + portIfIndex, err := c.getPortIfIndexMapping(ctx) if err != nil { return nil, err } @@ -130,7 +131,7 @@ out: return interfaces, nil } -func juniperAddVLANsNonELS(ctx context.Context, interfaces []device.Interface) ([]device.Interface, error) { +func (c *junosCommunicator) addVLANsNonELS(ctx context.Context, interfaces []device.Interface) ([]device.Interface, error) { con, ok := network.DeviceConnectionFromContext(ctx) if !ok || con.SNMP == nil { return nil, errors.New("snmp client is empty") @@ -142,7 +143,7 @@ func juniperAddVLANsNonELS(ctx context.Context, interfaces []device.Interface) ( return nil, errors.Wrap(err, "failed to get jnxExVlanPortStatus") } - portIfIndex, err := juniperGetPortIfIndexMapping(ctx) + portIfIndex, err := c.getPortIfIndexMapping(ctx) if err != nil { return nil, err } @@ -204,7 +205,7 @@ func juniperAddVLANsNonELS(ctx context.Context, interfaces []device.Interface) ( return interfaces, nil } -func juniperGetPortIfIndexMapping(ctx context.Context) (map[string]string, error) { +func (c *junosCommunicator) getPortIfIndexMapping(ctx context.Context) (map[string]string, error) { con, ok := network.DeviceConnectionFromContext(ctx) if !ok || con.SNMP == nil { return nil, errors.New("snmp client is empty") diff --git a/config/codecommunicator/timos-sas.go b/config/codecommunicator/timos-sas.go index 80c35fe..8f350f4 100644 --- a/config/codecommunicator/timos-sas.go +++ b/config/codecommunicator/timos-sas.go @@ -2,6 +2,7 @@ package codecommunicator import ( "context" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/inexio/thola/internal/network" "github.com/pkg/errors" @@ -14,7 +15,7 @@ type timosSASCommunicator struct { } // GetInterfaces returns the interfaces of a Nokia SAS-T device. -func (c *timosSASCommunicator) GetInterfaces(ctx context.Context) ([]device.Interface, error) { +func (c *timosSASCommunicator) GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) { interfaces, err := c.parent.GetInterfaces(ctx) if err != nil { return nil, err @@ -69,14 +70,14 @@ func (c *timosSASCommunicator) GetInterfaces(ctx context.Context) ([]device.Inte } } - return interfaces, nil + return filterInterfaces(interfaces, filter) } // getInterfaceBySubIndex returns the index of the interface that has the given index. // The returned index is the index of the array, not the IfIndex. func getInterfaceBySubIndex(subIndex uint64, interfaces []device.Interface) (int, error) { for index, iface := range interfaces { - if *iface.IfIndex == subIndex { + if iface.IfIndex != nil && *iface.IfIndex == subIndex { return index, nil } } diff --git a/config/codecommunicator/timos.go b/config/codecommunicator/timos.go index 3b84108..5206a9d 100644 --- a/config/codecommunicator/timos.go +++ b/config/codecommunicator/timos.go @@ -2,6 +2,7 @@ package codecommunicator import ( "context" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/inexio/thola/internal/network" "github.com/pkg/errors" @@ -15,7 +16,7 @@ type timosCommunicator struct { } // GetInterfaces returns the interfaces of Nokia devices. -func (c *timosCommunicator) GetInterfaces(ctx context.Context) ([]device.Interface, error) { +func (c *timosCommunicator) GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) { interfaces, err := c.parent.GetInterfaces(ctx) if err != nil { return nil, err @@ -89,7 +90,7 @@ func (c *timosCommunicator) GetInterfaces(ctx context.Context) ([]device.Interfa }) } - return interfaces, nil + return filterInterfaces(interfaces, filter) } // getPhysPortDescriptions returns a mapping from every ifIndex to a description. diff --git a/config/device-classes/generic.yaml b/config/device-classes/generic.yaml index 2e0772c..0e2dcfa 100644 --- a/config/device-classes/generic.yaml +++ b/config/device-classes/generic.yaml @@ -5,12 +5,14 @@ config: interfaces: true snmp: max_repetitions: 20 + max_oids: 60 components: interfaces: count: 1.3.6.1.2.1.2.1.0 properties: detection: snmpwalk + index: 1.3.6.1.2.1.2.2.1.1 values: ifIndex: oid: 1.3.6.1.2.1.2.2.1.1 diff --git a/config/device-classes/generic/ceraos.yaml b/config/device-classes/generic/ceraos.yaml index c10f919..5189d40 100644 --- a/config/device-classes/generic/ceraos.yaml +++ b/config/device-classes/generic/ceraos.yaml @@ -8,6 +8,10 @@ match: - .1.3.6.1.4.1.2281. logical_operator: OR +config: + snmp: + max_oids: 1 + identify: properties: vendor: diff --git a/internal/communicator/communicator/communicator.go b/internal/communicator/communicator/communicator.go index 1a5c55e..6b81b97 100644 --- a/internal/communicator/communicator/communicator.go +++ b/internal/communicator/communicator/communicator.go @@ -3,6 +3,7 @@ package communicator import ( "context" "github.com/inexio/thola/internal/communicator/component" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" ) @@ -67,7 +68,7 @@ type Functions interface { GetOSVersion(ctx context.Context) (string, error) // GetInterfaces returns the interfaces of a device. - GetInterfaces(ctx context.Context) ([]device.Interface, error) + GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) // GetCountInterfaces returns the count of interfaces of a device. GetCountInterfaces(ctx context.Context) (int, error) diff --git a/internal/communicator/communicator/network_device_communicator.go b/internal/communicator/communicator/network_device_communicator.go index d915937..a2bcd51 100644 --- a/internal/communicator/communicator/network_device_communicator.go +++ b/internal/communicator/communicator/network_device_communicator.go @@ -3,6 +3,7 @@ package communicator import ( "context" "github.com/inexio/thola/internal/communicator/component" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/inexio/thola/internal/tholaerr" "github.com/pkg/errors" @@ -548,9 +549,9 @@ func (c *networkDeviceCommunicator) GetOSVersion(ctx context.Context) (string, e return c.deviceClassCommunicator.GetOSVersion(ctx) } -func (c *networkDeviceCommunicator) GetInterfaces(ctx context.Context) ([]device.Interface, error) { +func (c *networkDeviceCommunicator) GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) { if c.codeCommunicator != nil { - res, err := c.codeCommunicator.GetInterfaces(ctx) + res, err := c.codeCommunicator.GetInterfaces(ctx, filter...) if err != nil { if !tholaerr.IsNotImplementedError(err) { return nil, errors.Wrap(err, "error in code communicator") @@ -560,7 +561,7 @@ func (c *networkDeviceCommunicator) GetInterfaces(ctx context.Context) ([]device } } - return c.deviceClassCommunicator.GetInterfaces(ctx) + return c.deviceClassCommunicator.GetInterfaces(ctx, filter...) } func (c *networkDeviceCommunicator) GetCountInterfaces(ctx context.Context) (int, error) { diff --git a/internal/communicator/create/create.go b/internal/communicator/create/create.go index 067547b..421ad42 100644 --- a/internal/communicator/create/create.go +++ b/internal/communicator/create/create.go @@ -76,10 +76,7 @@ func IdentifyNetworkDeviceCommunicator(ctx context.Context) (communicator.Commun return nil, err } - con, ok := network.DeviceConnectionFromContext(ctx) - if ok && con.SNMP != nil && (con.RawConnectionData.SNMP.MaxRepetitions == nil || *con.RawConnectionData.SNMP.MaxRepetitions == 0) { - con.SNMP.SnmpClient.SetMaxRepetitions(1) - } + setIdentifyConnectionSettings(ctx) comm, err := identifyDeviceRecursive(ctx, genericHierarchy.Children, true) if err != nil { @@ -157,5 +154,15 @@ func MatchDeviceClass(ctx context.Context, identifier string) (bool, error) { if err != nil { return false, errors.Wrap(err, "error during GetDeviceClasses") } + + setIdentifyConnectionSettings(ctx) + return comm.Match(ctx) } + +func setIdentifyConnectionSettings(ctx context.Context) { + con, ok := network.DeviceConnectionFromContext(ctx) + if ok && con.SNMP != nil && (con.RawConnectionData.SNMP.MaxRepetitions == nil || *con.RawConnectionData.SNMP.MaxRepetitions == 0) { + con.SNMP.SnmpClient.SetMaxRepetitions(1) + } +} diff --git a/internal/communicator/deviceclass/device_class.go b/internal/communicator/deviceclass/device_class.go index 1a2b55b..6abf242 100644 --- a/internal/communicator/deviceclass/device_class.go +++ b/internal/communicator/deviceclass/device_class.go @@ -13,6 +13,7 @@ import ( "github.com/inexio/thola/internal/mapping" "github.com/inexio/thola/internal/network" "github.com/inexio/thola/internal/tholaerr" + "github.com/inexio/thola/internal/utility" "github.com/inexio/thola/internal/value" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" @@ -133,6 +134,7 @@ type deviceClassComponentsInterfaces struct { // deviceClassSNMP represents the snmp config part of a device class. type deviceClassSNMP struct { MaxRepetitions uint32 `yaml:"max_repetitions"` + MaxOids int `yaml:"max_oids"` } // logicalOperator represents a logical operator (OR or AND). @@ -660,6 +662,10 @@ func (y *yamlDeviceClassIdentifyProperties) convert(parentProperties deviceClass } func (y *yamlDeviceClassConfig) convert(parentConfig deviceClassConfig) (deviceClassConfig, error) { + err := y.validate() + if err != nil { + return deviceClassConfig{}, errors.Wrap(err, "config is invalid") + } var cfg deviceClassConfig if y.SNMP.MaxRepetitions != 0 { @@ -667,6 +673,7 @@ func (y *yamlDeviceClassConfig) convert(parentConfig deviceClassConfig) (deviceC } else { cfg.snmp.MaxRepetitions = parentConfig.snmp.MaxRepetitions } + cfg.snmp.MaxOids = utility.IfThenElseInt(y.SNMP.MaxOids != 0, y.SNMP.MaxOids, parentConfig.snmp.MaxOids) components := make(map[component.Component]bool) for k, v := range parentConfig.components { @@ -686,6 +693,13 @@ func (y *yamlDeviceClassConfig) convert(parentConfig deviceClassConfig) (deviceC return cfg, nil } +func (y *yamlDeviceClassConfig) validate() error { + if y.SNMP.MaxOids < 0 { + return errors.New("invalid snmp max oids") + } + return nil +} + func (y *yamlComponentsUPSProperties) convert(parentComponent *deviceClassComponentsUPS) (deviceClassComponentsUPS, error) { var properties deviceClassComponentsUPS var err error @@ -1275,7 +1289,13 @@ func interfaceSlice2propertyOperators(i []interface{}, task relatedTask) (proper if !ok { return nil, errors.New("regex has to be a string") } - mod, err := newRegexSubmatchModifier(regexString, formatString) + var returnOnMismatch bool + if returnOnMismatchInt, ok := m["return_on_mismatch"]; ok { + if returnOnMismatch, ok = returnOnMismatchInt.(bool); !ok { + return nil, errors.New("return_on_mismatch needs to be a boolean") + } + } + mod, err := newRegexSubmatchModifier(regexString, formatString, returnOnMismatch) if err != nil { return nil, errors.Wrap(err, "failed to create new regex submatch modifier") } @@ -1537,6 +1557,24 @@ func interface2GroupPropertyReader(i interface{}, parentGroupPropertyReader grou } switch stringDetection { case "snmpwalk": + var index oidReader + if idx, ok := m["index"]; ok { + idxString, ok := idx.(string) + if !ok { + return nil, errors.New("index needs to be string (oid)") + } + oid := network.OID(idxString) + if err := oid.Validate(); err != nil { + return nil, errors.Wrap(err, "index needs to be an oid") + } + devClassOid := deviceClassOID{ + SNMPGetConfiguration: network.SNMPGetConfiguration{ + OID: oid, + }, + } + index = &devClassOid + } + if _, ok := m["values"]; !ok { return nil, errors.New("values are missing") } @@ -1568,9 +1606,13 @@ func interface2GroupPropertyReader(i interface{}, parentGroupPropertyReader grou devClassOIDsMerged := parentSNMPGroupPropertyReader.oids.merge(*devClassOIDs) devClassOIDs = &devClassOIDsMerged + + if index == nil { + index = parentSNMPGroupPropertyReader.index + } } - return &snmpGroupPropertyReader{*devClassOIDs}, nil + return &snmpGroupPropertyReader{index, *devClassOIDs}, nil default: return nil, fmt.Errorf("unknown detection type '%s'", stringDetection) } diff --git a/internal/communicator/deviceclass/device_class_communicator.go b/internal/communicator/deviceclass/device_class_communicator.go index 30cc97e..ada7a26 100644 --- a/internal/communicator/deviceclass/device_class_communicator.go +++ b/internal/communicator/deviceclass/device_class_communicator.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/inexio/thola/internal/communicator/component" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" "github.com/inexio/thola/internal/network" "github.com/inexio/thola/internal/tholaerr" @@ -51,10 +52,19 @@ func (o *deviceClassCommunicator) Match(ctx context.Context) (bool, error) { func (o *deviceClassCommunicator) UpdateConnection(ctx context.Context) error { if conn, ok := network.DeviceConnectionFromContext(ctx); ok { - if conn.SNMP != nil && conn.SNMP.SnmpClient != nil && - (conn.RawConnectionData.SNMP.MaxRepetitions == nil || *conn.RawConnectionData.SNMP.MaxRepetitions == 0) { - log.Ctx(ctx).Debug().Msg("set snmp max repetitions of device class") - conn.SNMP.SnmpClient.SetMaxRepetitions(o.deviceClass.config.snmp.MaxRepetitions) + if conn.SNMP != nil && conn.SNMP.SnmpClient != nil { + if conn.RawConnectionData.SNMP.MaxRepetitions == nil || *conn.RawConnectionData.SNMP.MaxRepetitions == 0 { + log.Ctx(ctx).Debug().Uint32("max_repetitions", o.deviceClass.config.snmp.MaxRepetitions).Msg("set snmp max repetitions of device class") + conn.SNMP.SnmpClient.SetMaxRepetitions(o.deviceClass.config.snmp.MaxRepetitions) + } + + if conn.SNMP.SnmpClient.GetVersion() != "1" { + log.Ctx(ctx).Debug().Int("max_oids", o.deviceClass.config.snmp.MaxOids).Msg("set snmp max oids of device class") + err := conn.SNMP.SnmpClient.SetMaxOIDs(o.deviceClass.config.snmp.MaxOids) + if err != nil { + return errors.Wrap(err, "failed to set max oids") + } + } } } return nil @@ -570,13 +580,13 @@ func (o *deviceClassCommunicator) GetOSVersion(ctx context.Context) (string, err return strings.TrimSpace(version.String()), nil } -func (o *deviceClassCommunicator) GetInterfaces(ctx context.Context) ([]device.Interface, error) { +func (o *deviceClassCommunicator) GetInterfaces(ctx context.Context, filter ...filter.PropertyFilter) ([]device.Interface, error) { if o.components.interfaces == nil || o.components.interfaces.Values == nil { log.Ctx(ctx).Debug().Str("property", "interfaces").Str("device_class", o.name).Msg("no interface information available") return nil, tholaerr.NewNotImplementedError("not implemented") } - interfacesRaw, indices, err := o.components.interfaces.Values.getProperty(ctx) + interfacesRaw, indices, err := o.components.interfaces.Values.getProperty(ctx, filter...) if err != nil { return nil, err } diff --git a/internal/communicator/deviceclass/group_properties.go b/internal/communicator/deviceclass/group_properties.go index 112cbfc..fada246 100644 --- a/internal/communicator/deviceclass/group_properties.go +++ b/internal/communicator/deviceclass/group_properties.go @@ -3,12 +3,14 @@ package deviceclass import ( "context" "fmt" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/network" "github.com/inexio/thola/internal/tholaerr" "github.com/inexio/thola/internal/value" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "regexp" "strconv" "strings" ) @@ -22,15 +24,30 @@ func (g *propertyGroups) Decode(destination interface{}) error { } type groupPropertyReader interface { - getProperty(ctx context.Context) (propertyGroups, []value.Value, error) + getProperty(ctx context.Context, filter ...filter.PropertyFilter) (propertyGroups, []value.Value, error) } type snmpGroupPropertyReader struct { - oids deviceClassOIDs + index oidReader + oids deviceClassOIDs } -func (s *snmpGroupPropertyReader) getProperty(ctx context.Context) (propertyGroups, []value.Value, error) { - groups, err := s.oids.readOID(ctx) +func (s *snmpGroupPropertyReader) getProperty(ctx context.Context, filter ...filter.PropertyFilter) (propertyGroups, []value.Value, error) { + wantedIndices, filteredIndices, err := s.getFilteredIndices(ctx, filter...) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to filter oid indices") + } + + useSNMPGetsInsteadOfWalk, ok := network.SNMPGetsInsteadOfWalkFromContext(ctx) + if !ok { + log.Ctx(ctx).Debug().Msg("SNMPGetsInsteadOfWalk not found in context, using walks") + } + + if !useSNMPGetsInsteadOfWalk { + wantedIndices = nil + } + + groups, err := s.oids.readOID(ctx, wantedIndices, true) if err != nil { return nil, nil, errors.Wrap(err, "failed to read oids") } @@ -58,25 +75,101 @@ func (s *snmpGroupPropertyReader) getProperty(ctx context.Context) (propertyGrou return nil, nil, fmt.Errorf("oidReader for index '%d' returned unexpected data type: %T", smallestIndex, groups[smallestIndex]) } + delete(groups, smallestIndex) + if !useSNMPGetsInsteadOfWalk { + if _, ok := filteredIndices[strconv.Itoa(smallestIndex)]; ok { + continue + } + } res = append(res, x) indices = append(indices, value.New(smallestIndex)) - delete(groups, smallestIndex) } return res, indices, nil } +func (s *snmpGroupPropertyReader) getFilteredIndices(ctx context.Context, filter ...filter.PropertyFilter) ([]value.Value, map[string]struct{}, error) { + indices := make(map[string]struct{}) + filteredIndices := make(map[string]struct{}) + + if s.index != nil { + res, err := s.index.readOID(ctx, nil, false) + if err == nil { + for idx := range res { + indices[strconv.Itoa(idx)] = struct{}{} + } + } + } + + for _, f := range filter { + // compile filter regex + regex, err := regexp.Compile(f.Regex) + if err != nil { + return nil, nil, errors.Wrap(err, "filter regex failed to compile") + } + + // find filter oid + attrs := strings.Split(f.Key, "/") + reader := oidReader(&s.oids) + for _, attr := range attrs { + // check if current oid reader contains multiple OIDs + multipleReader, ok := reader.(*deviceClassOIDs) + if !ok || multipleReader == nil { + return nil, nil, errors.New("filter attribute does not exist") + } + + // check if oid reader contains OID(s) for the current attribute name + if reader, ok = (*multipleReader)[attr]; !ok { + return nil, nil, errors.New("filter attribute does not exist") + } + } + + // check if the current oid reader contains only a single oid + singleReader, ok := reader.(*deviceClassOID) + if !ok || singleReader == nil { + return nil, nil, errors.New("filter attribute does not exist") + } + + results, err := singleReader.readOID(ctx, nil, false) + if err != nil { + log.Ctx(ctx).Debug().Err(err).Str("oid", string(singleReader.OID)).Msg("failed to read out filter oid, skipping filter") + continue + } + + for index, result := range results { + // add to indices map + indices[strconv.Itoa(index)] = struct{}{} + if regex.MatchString(result.(value.Value).String()) { + // if filter matches add to filtered indices map + filteredIndices[strconv.Itoa(index)] = struct{}{} + log.Ctx(ctx).Debug().Str("filter_key", f.Key).Str("filter_regex", f.Regex). + Str("received_value", result.(value.Value).String()). + Msgf("filter matched on index '%d'", index) + } + } + } + + var res []value.Value + for index := range indices { + if _, ok := filteredIndices[index]; !ok { + res = append(res, value.New(index)) + } + } + + return res, filteredIndices, nil +} + type oidReader interface { - readOID(context.Context) (map[int]interface{}, error) + readOID(context.Context, []value.Value, bool) (map[int]interface{}, error) } // deviceClassOIDs is a recursive data structure which maps labels to either a single OID (deviceClassOID) or another deviceClassOIDs type deviceClassOIDs map[string]oidReader -func (d *deviceClassOIDs) readOID(ctx context.Context) (map[int]interface{}, error) { +func (d *deviceClassOIDs) readOID(ctx context.Context, indices []value.Value, skipEmpty bool) (map[int]interface{}, error) { result := make(map[int]map[string]interface{}) for label, reader := range *d { - res, err := reader.readOID(ctx) + res, err := reader.readOID(ctx, indices, skipEmpty) if err != nil { if tholaerr.IsNotFoundError(err) || tholaerr.IsComponentNotFoundError(err) { log.Ctx(ctx).Debug().Err(err).Msgf("failed to get value '%s'", label) @@ -129,16 +222,60 @@ type deviceClassOID struct { indicesMapping *deviceClassOID } -func (d *deviceClassOID) readOID(ctx context.Context) (map[int]interface{}, error) { +func (d *deviceClassOID) readOID(ctx context.Context, indices []value.Value, skipEmpty bool) (map[int]interface{}, error) { result := make(map[int]interface{}) + logger := log.Ctx(ctx).With().Str("oid", string(d.OID)).Logger() + ctx = logger.WithContext(ctx) + con, ok := network.DeviceConnectionFromContext(ctx) if !ok || con.SNMP == nil { - log.Ctx(ctx).Debug().Str("property", "interface").Msg("snmp client is empty") + log.Ctx(ctx).Debug().Msg("snmp client is empty") return nil, errors.New("snmp client is empty") } - snmpResponse, err := con.SNMP.SnmpClient.SNMPWalk(ctx, string(d.OID)) + var snmpResponse []network.SNMPResponse + var err error + if len(indices) > 0 { + log.Ctx(ctx).Debug().Msg("indices given, using SNMP Gets instead of Walk") + + //change requested indices if necessary + if d.indicesMapping != nil { + mappingIndices, err := d.indicesMapping.readOID(ctx, nil, true) + if err != nil { + return nil, errors.Wrap(err, "failed to read indices") + } + + ifIndexRelIndex := make(map[string]value.Value) + for relIndex, ifIndex := range mappingIndices { + if idx, ok := ifIndexRelIndex[ifIndex.(value.Value).String()]; ok { + return nil, fmt.Errorf("index mapping resulted in duplicate ifIndex mapping on '%s'", idx.String()) + } + ifIndexRelIndex[ifIndex.(value.Value).String()] = value.New(relIndex) + } + + var newIndices []value.Value + for _, ifIndex := range indices { + if relIndex, ok := ifIndexRelIndex[ifIndex.String()]; ok { + newIndices = append(newIndices, relIndex) + } + } + + indices = newIndices + } + + oid := string(d.OID) + if !strings.HasSuffix(oid, ".") { + oid += "." + } + var oids []string + for _, index := range indices { + oids = append(oids, oid+index.String()) + } + snmpResponse, err = con.SNMP.SnmpClient.SNMPGet(ctx, oids...) + } else { + snmpResponse, err = con.SNMP.SnmpClient.SNMPWalk(ctx, string(d.OID)) + } if err != nil { if tholaerr.IsNotFoundError(err) { return nil, err @@ -148,19 +285,22 @@ func (d *deviceClassOID) readOID(ctx context.Context) (map[int]interface{}, erro } for _, response := range snmpResponse { + logger := log.Ctx(ctx).With().Str("oid", response.GetOID()).Logger() + ctx = logger.WithContext(ctx) + res, err := response.GetValueBySNMPGetConfiguration(d.SNMPGetConfiguration) if err != nil { - log.Ctx(ctx).Debug().Err(err).Msg("couldn't get value from response response") - return nil, errors.Wrap(err, "couldn't get value from response response") + log.Ctx(ctx).Debug().Err(err).Msg("couldn't get value from response") + continue } - if res != "" { + if res != "" || !skipEmpty { resNormalized, err := d.operators.apply(ctx, value.New(res)) if err != nil { if tholaerr.IsDidNotMatchError(err) { continue } - log.Ctx(ctx).Debug().Err(err).Msgf("response couldn't be normalized (oid: %s, response: %s)", response.GetOID(), res) - return nil, errors.Wrapf(err, "response couldn't be normalized (oid: %s, response: %s)", response.GetOID(), res) + log.Ctx(ctx).Debug().Err(err).Msgf("response couldn't be normalized (response: %s)", res) + return nil, errors.Wrapf(err, "response couldn't be normalized (response: %s)", res) } oid := strings.Split(response.GetOID(), ".") index, err := strconv.Atoi(oid[len(oid)-1]) @@ -174,16 +314,16 @@ func (d *deviceClassOID) readOID(ctx context.Context) (map[int]interface{}, erro //change indices if necessary if d.indicesMapping != nil { - indices, err := d.indicesMapping.readOID(ctx) + mappingIndices, err := d.indicesMapping.readOID(ctx, nil, true) if err != nil { - return nil, errors.Wrap(err, "failed to read indices") + return nil, errors.Wrap(err, "failed to read mapping indices") } mappedResult := make(map[int]interface{}) for k, v := range result { var idx int - if _, ok := indices[k]; ok { - idx, err = indices[k].(value.Value).Int() + if _, ok := mappingIndices[k]; ok { + idx, err = mappingIndices[k].(value.Value).Int() if err != nil { return nil, errors.Wrap(err, "failed to convert Value to int") } @@ -192,7 +332,7 @@ func (d *deviceClassOID) readOID(ctx context.Context) (map[int]interface{}, erro } if _, ok := mappedResult[idx]; ok { - return nil, fmt.Errorf("index mappings resulted in duplicated index '%d'", idx) + return nil, fmt.Errorf("index mapping resulted in duplicate index '%d'", idx) } mappedResult[idx] = v @@ -204,6 +344,6 @@ func (d *deviceClassOID) readOID(ctx context.Context) (map[int]interface{}, erro type emptyOIDReader struct{} -func (n *emptyOIDReader) readOID(context.Context) (map[int]interface{}, error) { +func (n *emptyOIDReader) readOID(context.Context, []value.Value, bool) (map[int]interface{}, error) { return nil, tholaerr.NewComponentNotFoundError("oid is ignored") } diff --git a/internal/communicator/deviceclass/operator.go b/internal/communicator/deviceclass/operator.go index 9e7d54e..425c6ce 100644 --- a/internal/communicator/deviceclass/operator.go +++ b/internal/communicator/deviceclass/operator.go @@ -201,11 +201,12 @@ func (a *addPrefixModifier) modify(_ context.Context, v value.Value) (value.Valu } type regexSubmatchModifier struct { - regex *regexp.Regexp - format string + regex *regexp.Regexp + format string + returnOnMismatch bool } -func newRegexSubmatchModifier(regex string, format string) (*regexSubmatchModifier, error) { +func newRegexSubmatchModifier(regex string, format string, returnOnMismatch bool) (*regexSubmatchModifier, error) { var r regexSubmatchModifier re, err := regexp.Compile(regex) if err != nil { @@ -213,6 +214,7 @@ func newRegexSubmatchModifier(regex string, format string) (*regexSubmatchModifi } r.regex = re r.format = format + r.returnOnMismatch = returnOnMismatch return &r, nil } @@ -224,6 +226,10 @@ func (o *regexSubmatchModifier) modify(_ context.Context, v value.Value) (value. return value.New(o.regex.ReplaceAllString(subMatches[0], o.format)), nil } +func (o *regexSubmatchModifier) returnOnError() bool { + return o.returnOnMismatch +} + type regexReplaceModifier struct { regex *regexp.Regexp replace string diff --git a/internal/communicator/filter/filter.go b/internal/communicator/filter/filter.go new file mode 100644 index 0000000..89171dc --- /dev/null +++ b/internal/communicator/filter/filter.go @@ -0,0 +1,6 @@ +package filter + +type PropertyFilter struct { + Key string + Regex string +} diff --git a/internal/network/context.go b/internal/network/context.go new file mode 100644 index 0000000..9234a31 --- /dev/null +++ b/internal/network/context.go @@ -0,0 +1,32 @@ +package network + +import "context" + +type ctxKey int + +const ( + requestDeviceConnectionKey ctxKey = iota + 1 + snmpGetsInsteadOfWalk +) + +// NewContextWithDeviceConnection returns a new context with the device connection +func NewContextWithDeviceConnection(ctx context.Context, con *RequestDeviceConnection) context.Context { + return context.WithValue(ctx, requestDeviceConnectionKey, con) +} + +// DeviceConnectionFromContext gets the device connection from the context +func DeviceConnectionFromContext(ctx context.Context) (*RequestDeviceConnection, bool) { + con, ok := ctx.Value(requestDeviceConnectionKey).(*RequestDeviceConnection) + return con, ok +} + +// NewContextWithSNMPGetsInsteadOfWalk returns a new context with the request +func NewContextWithSNMPGetsInsteadOfWalk(ctx context.Context, b bool) context.Context { + return context.WithValue(ctx, snmpGetsInsteadOfWalk, b) +} + +// SNMPGetsInsteadOfWalkFromContext gets the request from the context +func SNMPGetsInsteadOfWalkFromContext(ctx context.Context) (bool, bool) { + con, ok := ctx.Value(snmpGetsInsteadOfWalk).(bool) + return con, ok +} diff --git a/internal/network/request_device_connect.go b/internal/network/request_device_connect.go index fa6cca5..52747f2 100644 --- a/internal/network/request_device_connect.go +++ b/internal/network/request_device_connect.go @@ -31,21 +31,6 @@ type CommonOIDs struct { SysDescription *string } -type ctxKey int - -const requestDeviceConnectionKey ctxKey = iota + 1 - -// NewContextWithDeviceConnection returns a new context with the device connection -func NewContextWithDeviceConnection(ctx context.Context, con *RequestDeviceConnection) context.Context { - return context.WithValue(ctx, requestDeviceConnectionKey, con) -} - -// DeviceConnectionFromContext gets the device connection from the context -func DeviceConnectionFromContext(ctx context.Context) (*RequestDeviceConnection, bool) { - con, ok := ctx.Value(requestDeviceConnectionKey).(*RequestDeviceConnection) - return con, ok -} - // GetSysDescription returns the sysDescription. func (r *RequestDeviceConnectionSNMP) GetSysDescription(ctx context.Context) (string, error) { if r.CommonOIDs.SysDescription == nil { diff --git a/internal/network/snmp_client.go b/internal/network/snmp_client.go index 261e29c..dfb5f26 100644 --- a/internal/network/snmp_client.go +++ b/internal/network/snmp_client.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/gosnmp/gosnmp" "github.com/inexio/thola/internal/tholaerr" + "github.com/inexio/thola/internal/utility" "github.com/pkg/errors" "github.com/rs/zerolog/log" "golang.org/x/text/encoding/charmap" @@ -196,7 +197,7 @@ func NewSNMPClient(ctx context.Context, ipAddress, snmpVersion, community string Community: community, Version: version, Timeout: time.Duration(timeout) * time.Second, - MaxOids: 60, + MaxOids: utility.IfThenElseInt(version == gosnmp.Version1, 1, gosnmp.MaxOids), Retries: retries, } @@ -212,7 +213,7 @@ func NewSNMPv3Client(ctx context.Context, ipAddress string, port, timeout, retri Transport: "udp", Version: gosnmp.Version3, Timeout: time.Duration(timeout) * time.Second, - MaxOids: 60, + MaxOids: gosnmp.MaxOids, Retries: retries, SecurityModel: gosnmp.UserSecurityModel, } @@ -354,58 +355,56 @@ func getGoSNMPV3PrivProtocol(protocol string) (gosnmp.SnmpV3PrivProtocol, error) // SNMPGet sends one or more simple snmpget requests to the target host and returns the result. func (s *SNMPClient) SNMPGet(ctx context.Context, oid ...string) ([]SNMPResponse, error) { var snmpResponses []SNMPResponse - - m := make(map[int]SNMPResponse) + var successful bool var reqOIDs []string if s.useCache { - for a, o := range oid { - x, err := s.getCache.get(o) + for _, o := range oid { + cacheEntry, err := s.getCache.get(o) if err != nil { reqOIDs = append(reqOIDs, o) } else { - res, ok := x.res.(SNMPResponse) + res, ok := cacheEntry.res.(SNMPResponse) if !ok { - return nil, errors.New("cached snmp result is not a snmp response") + return nil, errors.New("cached SNMP Get result is not a SNMP response") + } + log.Ctx(ctx).Trace().Str("network_request", "snmpget").Str("oid", o).Msg("used cached SNMP Get result") + snmpResponses = append(snmpResponses, res) + if res.WasSuccessful() { + successful = true } - log.Ctx(ctx).Trace().Str("network_request", "snmpget").Str("oid", o).Msg("used cached snmp get result") - m[a] = res } } } else { reqOIDs = oid } - var response *gosnmp.SnmpPacket - var err error + var batch []string s.client.Context = ctx - if len(reqOIDs) != 0 { - response, err = s.client.Get(reqOIDs) + for len(reqOIDs) > 0 { + var batchSize int + if s.client.MaxOids >= len(reqOIDs) { + batchSize = len(reqOIDs) + } else { + batchSize = s.client.MaxOids + } + batch, reqOIDs = reqOIDs[:batchSize], reqOIDs[batchSize:] + + response, err := s.client.Get(batch) if err != nil { - log.Ctx(ctx).Trace().Str("network_request", "snmpget").Strs("oid", reqOIDs).Err(err).Msg("snmpget failed") + log.Ctx(ctx).Trace().Str("network_request", "snmpget").Strs("oid", batch).Err(err).Msg("SNMP Get failed") return nil, errors.Wrap(err, "error during snmpget") } - } - - successful := false - var currentResponse gosnmp.SnmpPDU - for i := 0; i < len(oid); i++ { - if x, ok := m[i]; ok { - snmpResponses = append(snmpResponses, x) - if x.WasSuccessful() { - successful = true - } - } else { - currentResponse, response.Variables = response.Variables[0], response.Variables[1:] + for _, currentResponse := range response.Variables { snmpResponse := SNMPResponse{} snmpResponse.oid = currentResponse.Name snmpResponse.value = currentResponse.Value snmpResponse.snmpType = currentResponse.Type if snmpResponse.WasSuccessful() { - log.Ctx(ctx).Trace().Str("network_request", "snmpget").Str("oid", snmpResponse.oid).Msg("snmpget was successful") + log.Ctx(ctx).Trace().Str("network_request", "snmpget").Str("oid", snmpResponse.oid).Msg("SNMP Get was successful") successful = true if s.useCache { s.getCache.add(snmpResponse.oid, snmpResponse, nil) @@ -431,13 +430,16 @@ func (s *SNMPClient) SNMPGet(ctx context.Context, oid ...string) ([]SNMPResponse // SNMPWalk sends a snmpwalk request to the specified oid. func (s *SNMPClient) SNMPWalk(ctx context.Context, oid string) ([]SNMPResponse, error) { if s.useCache { - x, err := s.walkCache.get(oid) + cacheEntry, err := s.walkCache.get(oid) if err == nil { - res, ok := x.res.([]SNMPResponse) + log.Ctx(ctx).Trace().Str("network_request", "snmpwalk").Str("oid", oid).Msg("used cached snmp walk result") + if cacheEntry.err != nil { + return nil, cacheEntry.err + } + res, ok := cacheEntry.res.([]SNMPResponse) if !ok { return nil, errors.New("cached snmp result is not a snmp response") } - log.Ctx(ctx).Trace().Str("network_request", "snmpwalk").Str("oid", oid).Msg("used cached snmp walk result") return res, nil } } @@ -553,6 +555,18 @@ func (s *SNMPClient) SetMaxRepetitions(maxRepetitions uint32) { s.client.MaxRepetitions = maxRepetitions } +// SetMaxOIDs sets the maximum OIDs. +func (s *SNMPClient) SetMaxOIDs(maxOIDs int) error { + if maxOIDs < 1 { + return errors.New("invalid max oids") + } + if s.client.Version == gosnmp.Version1 { + return errors.New("max oids cannot be changed for snmp v1") + } + s.client.MaxOids = maxOIDs + return nil +} + // GetV3Level returns the security level of the snmp v3 connection. // Return value is nil if no snmp v3 is being used. func (s *SNMPClient) GetV3Level() *string { diff --git a/internal/request/base_request.go b/internal/request/base_request.go index c5ccca7..265bf06 100644 --- a/internal/request/base_request.go +++ b/internal/request/base_request.go @@ -97,13 +97,21 @@ func (r *BaseRequest) validate(ctx context.Context) error { DiscoverParallelRequests: configData.SNMP.DiscoverParallelRequests, DiscoverTimeout: configData.SNMP.DiscoverTimeout, DiscoverRetries: configData.SNMP.DiscoverRetries, - V3Data: cacheData.SNMP.V3Data, + V3Data: network.SNMPv3ConnectionData{ + Level: utility.IfThenElse(cacheData.SNMP.V3Data.Level != nil, cacheData.SNMP.V3Data.Level, configData.SNMP.V3Data.Level).(*string), + ContextName: utility.IfThenElse(cacheData.SNMP.V3Data.ContextName != nil, cacheData.SNMP.V3Data.ContextName, configData.SNMP.V3Data.ContextName).(*string), + User: utility.IfThenElse(cacheData.SNMP.V3Data.User != nil, cacheData.SNMP.V3Data.User, configData.SNMP.V3Data.User).(*string), + AuthKey: utility.IfThenElse(cacheData.SNMP.V3Data.AuthKey != nil, cacheData.SNMP.V3Data.AuthKey, configData.SNMP.V3Data.AuthKey).(*string), + AuthProtocol: utility.IfThenElse(cacheData.SNMP.V3Data.AuthProtocol != nil, cacheData.SNMP.V3Data.AuthProtocol, configData.SNMP.V3Data.AuthProtocol).(*string), + PrivKey: utility.IfThenElse(cacheData.SNMP.V3Data.PrivKey != nil, cacheData.SNMP.V3Data.PrivKey, configData.SNMP.V3Data.PrivKey).(*string), + PrivProtocol: utility.IfThenElse(cacheData.SNMP.V3Data.PrivProtocol != nil, cacheData.SNMP.V3Data.PrivProtocol, configData.SNMP.V3Data.PrivProtocol).(*string), + }, }, HTTP: &network.HTTPConnectionData{ HTTPPorts: utility.SliceUniqueInt(append(cacheData.HTTP.HTTPPorts, configData.HTTP.HTTPPorts...)), HTTPSPorts: utility.SliceUniqueInt(append(cacheData.HTTP.HTTPSPorts, configData.HTTP.HTTPSPorts...)), - AuthUsername: utility.IfThenElse(cacheData.HTTP.AuthUsername == nil, configData.HTTP.AuthUsername, cacheData.HTTP.AuthUsername).(*string), - AuthPassword: utility.IfThenElse(cacheData.HTTP.AuthPassword == nil, configData.HTTP.AuthPassword, cacheData.HTTP.AuthPassword).(*string), + AuthUsername: utility.IfThenElse(cacheData.HTTP.AuthUsername != nil, cacheData.HTTP.AuthUsername, configData.HTTP.AuthUsername).(*string), + AuthPassword: utility.IfThenElse(cacheData.HTTP.AuthPassword != nil, cacheData.HTTP.AuthPassword, configData.HTTP.AuthPassword).(*string), }, } @@ -145,7 +153,8 @@ func (r *BaseRequest) validate(ctx context.Context) error { r.DeviceData.ConnectionData.SNMP.DiscoverRetries = mergedData.SNMP.DiscoverRetries } - if *r.DeviceData.ConnectionData.SNMP.DiscoverParallelRequests <= 0 || *r.DeviceData.ConnectionData.SNMP.DiscoverTimeout <= 0 { + if (r.DeviceData.ConnectionData.SNMP.DiscoverParallelRequests != nil && *r.DeviceData.ConnectionData.SNMP.DiscoverParallelRequests <= 0) || + (r.DeviceData.ConnectionData.SNMP.DiscoverTimeout != nil && *r.DeviceData.ConnectionData.SNMP.DiscoverTimeout <= 0) { return errors.New("invalid snmp connection discover preferences") } @@ -262,6 +271,13 @@ func getConfigConnectionData() network.ConnectionData { parallelRequests := viper.GetInt("device.snmp-discover-par-requests") timeout := viper.GetInt("device.snmp-discover-timeout") retries := viper.GetInt("device.snmp-discover-retries") + v3Level := viper.GetString("device.snmp-v3-level") + v3ContextName := viper.GetString("device.snmp-v3-context") + v3User := viper.GetString("device.snmp-v3-user") + v3AuthKey := viper.GetString("device.snmp-v3-auth-key") + v3AuthProto := viper.GetString("device.snmp-v3-auth-proto") + v3PrivKey := viper.GetString("device.snmp-v3-priv-key") + v3PrivProto := viper.GetString("device.snmp-v3-priv-proto") authUsername := viper.GetString("device.http-username") authPassword := viper.GetString("device.http-password") return network.ConnectionData{ @@ -272,6 +288,15 @@ func getConfigConnectionData() network.ConnectionData { DiscoverParallelRequests: ¶llelRequests, DiscoverTimeout: &timeout, DiscoverRetries: &retries, + V3Data: network.SNMPv3ConnectionData{ + Level: &v3Level, + ContextName: &v3ContextName, + User: &v3User, + AuthKey: &v3AuthKey, + AuthProtocol: &v3AuthProto, + PrivKey: &v3PrivKey, + PrivProtocol: &v3PrivProto, + }, }, HTTP: &network.HTTPConnectionData{ HTTPPorts: viper.GetIntSlice("device.http-ports"), diff --git a/internal/request/check_device_request.go b/internal/request/check_device_request.go index 565865d..942d3f9 100644 --- a/internal/request/check_device_request.go +++ b/internal/request/check_device_request.go @@ -2,7 +2,7 @@ package request // CheckDeviceRequest // -// CheckDeviceRequest is a the request struct for the check device request. +// CheckDeviceRequest is the request struct for the check device request. // // swagger:model type CheckDeviceRequest struct { diff --git a/internal/request/check_interface_metrics_request.go b/internal/request/check_interface_metrics_request.go index d9ecf96..8ddcf1d 100644 --- a/internal/request/check_interface_metrics_request.go +++ b/internal/request/check_interface_metrics_request.go @@ -12,13 +12,14 @@ import ( // // swagger:model type CheckInterfaceMetricsRequest struct { - PrintInterfaces bool `yaml:"print_interfaces" json:"print_interfaces" xml:"print_interfaces"` - IfDescrRegex *string `yaml:"ifDescr_regex" json:"ifDescr_regex" xml:"ifDescr_regex"` - ifDescrRegex *regexp.Regexp - IfDescrRegexReplace *string `yaml:"ifDescr_regex_replace" json:"ifDescr_regex_replace" xml:"ifDescr_regex_replace"` - IfTypeFilter []string `yaml:"ifType_filter" json:"ifType_filter" xml:"ifType_filter"` - IfNameFilter []string `yaml:"ifName_filter" json:"ifName_filter" xml:"ifName_filter"` - IfDescrFilter []string `yaml:"ifDescr_filter" json:"ifDescr_filter" xml:"ifDescr_filter"` + PrintInterfaces bool `yaml:"print_interfaces" json:"print_interfaces" xml:"print_interfaces"` + IfDescrRegex *string `yaml:"ifDescr_regex" json:"ifDescr_regex" xml:"ifDescr_regex"` + ifDescrRegex *regexp.Regexp + IfDescrRegexReplace *string `yaml:"ifDescr_regex_replace" json:"ifDescr_regex_replace" xml:"ifDescr_regex_replace"` + IfTypeFilter []string `yaml:"ifType_filter" json:"ifType_filter" xml:"ifType_filter"` + IfNameFilter []string `yaml:"ifName_filter" json:"ifName_filter" xml:"ifName_filter"` + IfDescrFilter []string `yaml:"ifDescr_filter" json:"ifDescr_filter" xml:"ifDescr_filter"` + SNMPGetsInsteadOfWalk bool `yaml:"snmp_gets_instead_of_walk" json:"snmp_gets_instead_of_walk" xml:"snmp_gets_instead_of_walk"` CheckDeviceRequest } diff --git a/internal/request/check_interface_metrics_request_process.go b/internal/request/check_interface_metrics_request_process.go index 1f547de..f160700 100644 --- a/internal/request/check_interface_metrics_request_process.go +++ b/internal/request/check_interface_metrics_request_process.go @@ -6,10 +6,11 @@ import ( "context" "fmt" "github.com/inexio/go-monitoringplugin" + "github.com/inexio/thola/internal/communicator/filter" "github.com/inexio/thola/internal/device" + "github.com/inexio/thola/internal/network" "github.com/inexio/thola/internal/parser" "github.com/pkg/errors" - "regexp" ) type interfaceCheckOutput struct { @@ -27,22 +28,22 @@ type interfaceCheckOutput struct { func (r *CheckInterfaceMetricsRequest) process(ctx context.Context) (Response, error) { r.init() - readInterfacesRequest := ReadInterfacesRequest{ReadRequest{r.BaseRequest}} - response, err := readInterfacesRequest.process(ctx) - if err != nil { - return nil, err - } + ctx = network.NewContextWithSNMPGetsInsteadOfWalk(ctx, r.SNMPGetsInsteadOfWalk) - readInterfacesResponse := response.(*ReadInterfacesResponse) + com, err := GetCommunicator(ctx, r.BaseRequest) + if r.mon.UpdateStatusOnError(err, monitoringplugin.UNKNOWN, "failed to get communicator", true) { + r.mon.PrintPerformanceData(false) + return &CheckResponse{r.mon.GetInfo()}, nil + } - err = r.normalizeInterfaces(readInterfacesResponse.Interfaces) - if r.mon.UpdateStatusOnError(err, monitoringplugin.UNKNOWN, "error while normalizing interfaces", true) { + interfaces, err := com.GetInterfaces(ctx, r.getFilter()...) + if r.mon.UpdateStatusOnError(err, monitoringplugin.UNKNOWN, "failed to read out interfaces", true) { r.mon.PrintPerformanceData(false) return &CheckResponse{r.mon.GetInfo()}, nil } - interfaces, err := r.filterInterfaces(readInterfacesResponse.Interfaces) - if r.mon.UpdateStatusOnError(err, monitoringplugin.UNKNOWN, "error while filtering interfaces", true) { + err = r.normalizeInterfaces(interfaces) + if r.mon.UpdateStatusOnError(err, monitoringplugin.UNKNOWN, "error while normalizing interfaces", true) { r.mon.PrintPerformanceData(false) return &CheckResponse{r.mon.GetInfo()}, nil } @@ -76,63 +77,42 @@ func (r *CheckInterfaceMetricsRequest) process(ctx context.Context) (Response, e interfaceOutput = append(interfaceOutput, x) } - output, err := parser.Parse(interfaceOutput, "json") - if r.mon.UpdateStatusOnError(err, monitoringplugin.UNKNOWN, "error while marshalling output", true) { - r.mon.PrintPerformanceData(false) - return &CheckResponse{r.mon.GetInfo()}, nil + if len(interfaceOutput) > 0 { + output, err := parser.Parse(interfaceOutput, "json") + if r.mon.UpdateStatusOnError(err, monitoringplugin.UNKNOWN, "error while marshalling output", true) { + r.mon.PrintPerformanceData(false) + return &CheckResponse{r.mon.GetInfo()}, nil + } + r.mon.UpdateStatus(monitoringplugin.OK, string(output)) } - r.mon.UpdateStatus(monitoringplugin.OK, string(output)) } return &CheckResponse{r.mon.GetInfo()}, nil } -func (r *CheckInterfaceMetricsRequest) filterInterfaces(interfaces []device.Interface) ([]device.Interface, error) { - var filterIndices []int -out: - for i, interf := range interfaces { - for _, filter := range r.IfTypeFilter { - if interf.IfType != nil && *interf.IfType == filter { - filterIndices = append(filterIndices, i) - continue out - } - } - for _, filter := range r.IfNameFilter { - if interf.IfName != nil { - matched, err := regexp.MatchString(filter, *interf.IfName) - if err != nil { - return nil, errors.Wrap(err, "ifName filter regex match failed") - } - if matched { - filterIndices = append(filterIndices, i) - continue out - } - } - } - for _, filter := range r.IfDescrFilter { - if interf.IfDescr != nil { - matched, err := regexp.MatchString(filter, *interf.IfDescr) - if err != nil { - return nil, errors.Wrap(err, "ifDescr filter regex match failed") - } - if matched { - filterIndices = append(filterIndices, i) - continue out - } - } - } - } +func (r *CheckInterfaceMetricsRequest) getFilter() []filter.PropertyFilter { + var res []filter.PropertyFilter - interfaces = filterInterfaces(interfaces, filterIndices, 0) - - return interfaces, nil -} - -func filterInterfaces(interfaces []device.Interface, toRemove []int, alreadyRemoved int) []device.Interface { - if len(toRemove) == 0 { - return interfaces + for _, f := range r.IfTypeFilter { + res = append(res, filter.PropertyFilter{ + Key: "ifType", + Regex: f, + }) } - return append(interfaces[:toRemove[0]-alreadyRemoved], filterInterfaces(interfaces[toRemove[0]+1-alreadyRemoved:], toRemove[1:], toRemove[0]+1)...) + for _, f := range r.IfNameFilter { + res = append(res, filter.PropertyFilter{ + Key: "ifName", + Regex: f, + }) + } + for _, f := range r.IfDescrFilter { + res = append(res, filter.PropertyFilter{ + Key: "ifDescr", + Regex: f, + }) + } + + return res } func (r *CheckInterfaceMetricsRequest) normalizeInterfaces(interfaces []device.Interface) error { diff --git a/internal/request/process_request.go b/internal/request/process_request.go index ab74ee2..d302ada 100644 --- a/internal/request/process_request.go +++ b/internal/request/process_request.go @@ -16,7 +16,7 @@ type response struct { err error } -// ProcessRequest is called by every request thola receives +// ProcessRequest is called by every request Thola receives func ProcessRequest(ctx context.Context, request Request) (Response, error) { err := request.validate(ctx) if err != nil { diff --git a/internal/request/request_test.go b/internal/request/request_test.go deleted file mode 100644 index 2ad256e..0000000 --- a/internal/request/request_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package request - -import ( - "github.com/inexio/thola/internal/device" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestRemoveInterfaces(t *testing.T) { - for i := 1; i < 1000; i++ { - var interfaces []device.Interface - var toRemove []int - for j := 0; j < i; j++ { - interfaces = append(interfaces, device.Interface{}) - if j%2 == 0 { - toRemove = append(toRemove, j) - } - } - interfaces = filterInterfaces(interfaces, toRemove, 0) - assert.Equal(t, i/2, len(interfaces), "expected removed length and actual length differs") - } -}