1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,102 @@
# Network Interface Name Processor Plugin
The `ifname` plugin looks up network interface names using SNMP.
Telegraf minimum version: Telegraf 1.15.0
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Secret-store support
This plugin supports secrets from secret-stores for the `auth_password` and
`priv_password` option.
See the [secret-store documentation][SECRETSTORE] for more details on how
to use them.
[SECRETSTORE]: ../../../docs/CONFIGURATION.md#secret-store-secrets
## Configuration
```toml @sample.conf
# Add a tag of the network interface name looked up over SNMP by interface number
[[processors.ifname]]
## Name of tag holding the interface number
# tag = "ifIndex"
## Name of output tag where service name will be added
# dest = "ifName"
## Name of tag of the SNMP agent to request the interface name from
## example: agent = "source"
# agent = "agent"
## Timeout for each request.
# timeout = "5s"
## SNMP version; can be 1, 2, or 3.
# version = 2
## SNMP community string.
# community = "public"
## Number of retries to attempt.
# retries = 3
## The GETBULK max-repetitions parameter.
# max_repetitions = 10
## SNMPv3 authentication and encryption options.
##
## Security Name.
# sec_name = "myuser"
## Authentication protocol; one of "MD5", "SHA", or "".
# auth_protocol = "MD5"
## Authentication password.
# auth_password = "pass"
## Security Level; one of "noAuthNoPriv", "authNoPriv", or "authPriv".
# sec_level = "authNoPriv"
## Context Name.
# context_name = ""
## Privacy protocol used for encrypted messages; one of "DES", "AES" or "".
# priv_protocol = ""
## Privacy password used for encrypted messages.
# priv_password = ""
## max_parallel_lookups is the maximum number of SNMP requests to
## make at the same time.
# max_parallel_lookups = 100
## ordered controls whether or not the metrics need to stay in the
## same order this plugin received them in. If false, this plugin
## may change the order when data is cached. If you need metrics to
## stay in order set this to true. keeping the metrics ordered may
## be slightly slower
# ordered = false
## cache_ttl is the amount of time interface names are cached for a
## given agent. After this period elapses if names are needed they
## will be retrieved again.
# cache_ttl = "8h"
```
## Example
Example config:
```toml
[[processors.ifname]]
tag = "ifIndex"
dest = "ifName"
```
```diff
- foo,ifIndex=2,agent=127.0.0.1 field=123 1502489900000000000
+ foo,ifIndex=2,agent=127.0.0.1,ifName=eth0 field=123 1502489900000000000
```

View file

@ -0,0 +1,83 @@
package ifname
// See https://girai.dev/blog/lru-cache-implementation-in-go/
import (
"container/list"
)
type LRUValType = TTLValType
type hashType map[keyType]*list.Element
type LRUCache struct {
cap uint // capacity
l *list.List // doubly linked list
m hashType // hash table for checking if list node exists
}
// Pair is the value of a list node.
type Pair struct {
key keyType
value LRUValType
}
// initializes a new LRUCache.
func NewLRUCache(capacity uint) LRUCache {
return LRUCache{
cap: capacity,
l: new(list.List),
m: make(hashType, capacity),
}
}
// Get a list node from the hash map.
func (c *LRUCache) Get(key keyType) (LRUValType, bool) {
// check if list node exists
if node, ok := c.m[key]; ok {
val := node.Value.(*list.Element).Value.(Pair).value
// move node to front
c.l.MoveToFront(node)
return val, true
}
return LRUValType{}, false
}
// Put key and value in the LRUCache
func (c *LRUCache) Put(key keyType, value LRUValType) {
// check if list node exists
if node, ok := c.m[key]; ok {
// move the node to front
c.l.MoveToFront(node)
// update the value of a list node
node.Value.(*list.Element).Value = Pair{key: key, value: value}
} else {
// delete the last list node if the list is full
if uint(c.l.Len()) == c.cap {
// get the key that we want to delete
idx := c.l.Back().Value.(*list.Element).Value.(Pair).key
// delete the node pointer in the hash map by key
delete(c.m, idx)
// remove the last list node
c.l.Remove(c.l.Back())
}
// initialize a list node
node := &list.Element{
Value: Pair{
key: key,
value: value,
},
}
// push the new list node into the list
ptr := c.l.PushFront(node)
// save the node pointer in the hash map
c.m[key] = ptr
}
}
func (c *LRUCache) Delete(key keyType) {
if node, ok := c.m[key]; ok {
c.l.Remove(node)
delete(c.m, key)
}
}

View file

@ -0,0 +1,23 @@
package ifname
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCache(t *testing.T) {
c := NewLRUCache(2)
c.Put("ones", LRUValType{val: nameMap{1: "one"}})
twoMap := LRUValType{val: nameMap{2: "two"}}
c.Put("twos", twoMap)
c.Put("threes", LRUValType{val: nameMap{3: "three"}})
_, ok := c.Get("ones")
require.False(t, ok)
v, ok := c.Get("twos")
require.True(t, ok)
require.Equal(t, twoMap, v)
}

View file

@ -0,0 +1,330 @@
//go:generate ../../../tools/readme_config_includer/generator
package ifname
import (
_ "embed"
"errors"
"fmt"
"strconv"
"sync"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal/snmp"
"github.com/influxdata/telegraf/plugins/common/parallel"
"github.com/influxdata/telegraf/plugins/processors"
)
//go:embed sample.conf
var sampleConfig string
type nameMap map[uint64]string
type keyType = string
type valType = nameMap
type mapFunc func(agent string) (nameMap, error)
type sigMap map[string]chan struct{}
type IfName struct {
SourceTag string `toml:"tag"`
DestTag string `toml:"dest"`
AgentTag string `toml:"agent"`
snmp.ClientConfig
CacheSize uint `toml:"max_cache_entries"`
MaxParallelLookups int `toml:"max_parallel_lookups"`
Ordered bool `toml:"ordered"`
CacheTTL config.Duration `toml:"cache_ttl"`
Log telegraf.Logger `toml:"-"`
ifTable *snmp.Table
ifXTable *snmp.Table
cache *TTLCache
lock sync.Mutex
parallel parallel.Parallel
sigs sigMap
getMapRemote mapFunc
}
const minRetry = 5 * time.Minute
func (*IfName) SampleConfig() string {
return sampleConfig
}
func (d *IfName) Init() error {
d.getMapRemote = d.getMapRemoteNoMock
c := NewTTLCache(time.Duration(d.CacheTTL), d.CacheSize)
d.cache = &c
d.sigs = make(sigMap)
if _, err := snmp.NewWrapper(d.ClientConfig); err != nil {
return fmt.Errorf("parsing SNMP client config: %w", err)
}
return nil
}
func (d *IfName) addTag(metric telegraf.Metric) error {
agent, ok := metric.GetTag(d.AgentTag)
if !ok {
d.Log.Warn("Agent tag missing.")
return nil
}
numS, ok := metric.GetTag(d.SourceTag)
if !ok {
d.Log.Warn("Source tag missing.")
return nil
}
num, err := strconv.ParseUint(numS, 10, 64)
if err != nil {
return errors.New("couldn't parse source tag as uint")
}
firstTime := true
for {
m, age, err := d.getMap(agent)
if err != nil {
return fmt.Errorf("couldn't retrieve the table of interface names for %s: %w", agent, err)
}
name, found := m[num]
if found {
// success
metric.AddTag(d.DestTag, name)
return nil
}
// We have the agent's interface map but it doesn't contain
// the interface we're interested in. If the entry is old
// enough, retrieve it from the agent once more.
if age < minRetry {
return fmt.Errorf("interface number %d isn't in the table of interface names on %s", num, agent)
}
if firstTime {
d.invalidate(agent)
firstTime = false
continue
}
// not found, cache hit, retrying
return fmt.Errorf("missing interface but couldn't retrieve table for %v", agent)
}
}
func (d *IfName) invalidate(agent string) {
d.lock.Lock()
d.cache.Delete(agent)
d.lock.Unlock()
}
func (d *IfName) Start(acc telegraf.Accumulator) error {
var err error
d.ifTable, err = makeTable("1.3.6.1.2.1.2.2.1.2")
if err != nil {
return fmt.Errorf("preparing ifTable: %w", err)
}
d.ifXTable, err = makeTable("1.3.6.1.2.1.31.1.1.1.1")
if err != nil {
return fmt.Errorf("preparing ifXTable: %w", err)
}
fn := func(m telegraf.Metric) []telegraf.Metric {
err := d.addTag(m)
if err != nil {
d.Log.Debugf("Error adding tag: %v", err)
}
return []telegraf.Metric{m}
}
if d.Ordered {
d.parallel = parallel.NewOrdered(acc, fn, 10000, d.MaxParallelLookups)
} else {
d.parallel = parallel.NewUnordered(acc, fn, d.MaxParallelLookups)
}
return nil
}
func (d *IfName) Add(metric telegraf.Metric, _ telegraf.Accumulator) error {
d.parallel.Enqueue(metric)
return nil
}
func (d *IfName) Stop() {
d.parallel.Stop()
}
// getMap gets the interface names map either from cache or from the SNMP
// agent
func (d *IfName) getMap(agent string) (entry nameMap, age time.Duration, err error) {
var sig chan struct{}
d.lock.Lock()
// Check cache
m, ok, age := d.cache.Get(agent)
if ok {
d.lock.Unlock()
return m, age, nil
}
// cache miss. Is this the first request for this agent?
sig, found := d.sigs[agent]
if !found {
// This is the first request. Make signal for subsequent requests to wait on
s := make(chan struct{})
d.sigs[agent] = s
sig = s
}
d.lock.Unlock()
if found {
// This is not the first request. Wait for first to finish.
<-sig
// Check cache again
d.lock.Lock()
m, ok, age := d.cache.Get(agent)
d.lock.Unlock()
if ok {
return m, age, nil
}
return nil, 0, errors.New("getting remote table from cache")
}
// The cache missed and this is the first request for this
// agent. Make the SNMP request
m, err = d.getMapRemote(agent)
d.lock.Lock()
if err != nil {
// snmp failure. signal without saving to cache
close(sig)
delete(d.sigs, agent)
d.lock.Unlock()
return nil, 0, fmt.Errorf("getting remote table: %w", err)
}
// snmp success. Cache response, then signal any other waiting
// requests for this agent and clean up
d.cache.Put(agent, m)
close(sig)
delete(d.sigs, agent)
d.lock.Unlock()
return m, 0, nil
}
func (d *IfName) getMapRemoteNoMock(agent string) (nameMap, error) {
gs, err := snmp.NewWrapper(d.ClientConfig)
if err != nil {
return nil, fmt.Errorf("parsing SNMP client config: %w", err)
}
if err = gs.SetAgent(agent); err != nil {
return nil, fmt.Errorf("parsing agent tag: %w", err)
}
if err = gs.Connect(); err != nil {
return nil, fmt.Errorf("connecting when fetching interface names: %w", err)
}
// try ifXtable and ifName first. if that fails, fall back to
// ifTable and ifDescr
var m nameMap
if m, err = buildMap(gs, d.ifXTable); err == nil {
return m, nil
}
if m, err = buildMap(gs, d.ifTable); err == nil {
return m, nil
}
return nil, fmt.Errorf("fetching interface names: %w", err)
}
func init() {
processors.AddStreaming("ifname", func() telegraf.StreamingProcessor {
return &IfName{
SourceTag: "ifIndex",
DestTag: "ifName",
AgentTag: "agent",
CacheSize: 100,
MaxParallelLookups: 100,
ClientConfig: *snmp.DefaultClientConfig(),
CacheTTL: config.Duration(8 * time.Hour),
}
})
}
func makeTable(oid string) (*snmp.Table, error) {
var err error
tab := snmp.Table{
Name: "ifTable",
IndexAsTag: true,
Fields: []snmp.Field{
{Oid: oid, Name: "ifName"},
},
}
err = tab.Init(nil)
if err != nil {
// Init already wraps
return nil, err
}
return &tab, nil
}
func buildMap(gs snmp.GosnmpWrapper, tab *snmp.Table) (nameMap, error) {
var err error
rtab, err := tab.Build(gs, true)
if err != nil {
// Build already wraps
return nil, err
}
if len(rtab.Rows) == 0 {
return nil, errors.New("empty table")
}
t := make(nameMap)
for _, v := range rtab.Rows {
iStr, ok := v.Tags["index"]
if !ok {
// should always have an index tag because the table should
// always have IndexAsTag true
return nil, errors.New("no index tag")
}
i, err := strconv.ParseUint(iStr, 10, 64)
if err != nil {
return nil, errors.New("index tag isn't a uint")
}
nameIf, ok := v.Fields["ifName"]
if !ok {
return nil, errors.New("ifName field is missing")
}
name, ok := nameIf.(string)
if !ok {
return nil, errors.New("ifName field isn't a string")
}
t[i] = name
}
return t, nil
}

View file

@ -0,0 +1,232 @@
package ifname
import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal/snmp"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/testutil"
)
type item struct {
entry nameMap
age time.Duration
err error
}
func TestTableIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
t.Skip("Skipping test due to connect failures")
d := IfName{}
err := d.Init()
require.NoError(t, err)
tab, err := makeTable("1.3.6.1.2.1.2.2.1.2")
require.NoError(t, err)
gs, err := snmp.NewWrapper(*snmp.DefaultClientConfig())
require.NoError(t, err)
err = gs.SetAgent("127.0.0.1")
require.NoError(t, err)
err = gs.Connect()
require.NoError(t, err)
// Could use ifIndex but oid index is always the same
m, err := buildMap(gs, tab)
require.NoError(t, err)
require.NotEmpty(t, m)
}
func TestIfNameIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
t.Skip("Skipping test due to connect failures")
d := IfName{
SourceTag: "ifIndex",
DestTag: "ifName",
AgentTag: "agent",
CacheSize: 1000,
ClientConfig: *snmp.DefaultClientConfig(),
}
err := d.Init()
require.NoError(t, err)
acc := testutil.Accumulator{}
err = d.Start(&acc)
require.NoError(t, err)
m := testutil.MustMetric(
"cpu",
map[string]string{
"ifIndex": "1",
"agent": "127.0.0.1",
},
map[string]interface{}{},
time.Unix(0, 0),
)
expected := testutil.MustMetric(
"cpu",
map[string]string{
"ifIndex": "1",
"agent": "127.0.0.1",
"ifName": "lo",
},
map[string]interface{}{},
time.Unix(0, 0),
)
err = d.addTag(m)
require.NoError(t, err)
testutil.RequireMetricEqual(t, expected, m)
}
func TestGetMap(t *testing.T) {
d := IfName{
CacheSize: 1000,
CacheTTL: config.Duration(10 * time.Second),
}
require.NoError(t, d.Init())
expected := nameMap{
1: "ifname1",
2: "ifname2",
}
var remoteCalls int32
// Mock the snmp transaction
d.getMapRemote = func(string) (nameMap, error) {
atomic.AddInt32(&remoteCalls, 1)
return expected, nil
}
m, age, err := d.getMap("agent")
require.NoError(t, err)
require.Zero(t, age) // Age is zero when map comes from getMapRemote
require.Equal(t, expected, m)
// Remote call should happen the first time getMap runs
require.Equal(t, int32(1), remoteCalls)
const thMax = 3
ch := make(chan item, thMax)
var wg sync.WaitGroup
for th := 0; th < thMax; th++ {
wg.Add(1)
go func() {
defer wg.Done()
m, age, err := d.getMap("agent")
ch <- item{entry: m, age: age, err: err}
}()
}
wg.Wait()
close(ch)
for entry := range ch {
require.NoError(t, entry.err)
require.NotZero(t, entry.age) // Age is nonzero when map comes from cache
require.Equal(t, expected, entry.entry)
}
// Remote call should not happen subsequent times getMap runs
require.Equal(t, int32(1), remoteCalls)
}
func TestTracking(t *testing.T) {
// Setup raw input and expected output
inputRaw := []telegraf.Metric{
metric.New(
"test",
map[string]string{"ifIndex": "1", "agent": "127.0.0.1"},
map[string]interface{}{"value": 42},
time.Unix(0, 0),
),
}
expected := []telegraf.Metric{
metric.New(
"test",
map[string]string{
"ifIndex": "1",
"agent": "127.0.0.1",
"ifName": "lo",
},
map[string]interface{}{"value": 42},
time.Unix(0, 0),
),
}
// Create fake notification for testing
var mu sync.Mutex
delivered := make([]telegraf.DeliveryInfo, 0, len(inputRaw))
notify := func(di telegraf.DeliveryInfo) {
mu.Lock()
defer mu.Unlock()
delivered = append(delivered, di)
}
// Convert raw input to tracking metric
input := make([]telegraf.Metric, 0, len(inputRaw))
for _, m := range inputRaw {
tm, _ := metric.WithTracking(m, notify)
input = append(input, tm)
}
// Prepare and start the plugin
plugin := &IfName{
SourceTag: "ifIndex",
DestTag: "ifName",
AgentTag: "agent",
CacheSize: 1000,
CacheTTL: config.Duration(10 * time.Second),
MaxParallelLookups: 100,
}
require.NoError(t, plugin.Init())
plugin.cache.Put("127.0.0.1", nameMap{1: "lo"})
var acc testutil.Accumulator
require.NoError(t, plugin.Start(&acc))
defer plugin.Stop()
// Process expected metrics and compare with resulting metrics
for _, in := range input {
require.NoError(t, plugin.Add(in, &acc))
}
require.Eventually(t, func() bool {
return int(acc.NMetrics()) >= len(expected)
}, 3*time.Second, 100*time.Microsecond)
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual)
// Simulate output acknowledging delivery
for _, m := range actual {
m.Accept()
}
// Check delivery
require.Eventuallyf(t, func() bool {
mu.Lock()
defer mu.Unlock()
return len(input) == len(delivered)
}, time.Second, 100*time.Millisecond, "%d delivered but %d expected", len(delivered), len(expected))
}

View file

@ -0,0 +1,59 @@
# Add a tag of the network interface name looked up over SNMP by interface number
[[processors.ifname]]
## Name of tag holding the interface number
# tag = "ifIndex"
## Name of output tag where service name will be added
# dest = "ifName"
## Name of tag of the SNMP agent to request the interface name from
## example: agent = "source"
# agent = "agent"
## Timeout for each request.
# timeout = "5s"
## SNMP version; can be 1, 2, or 3.
# version = 2
## SNMP community string.
# community = "public"
## Number of retries to attempt.
# retries = 3
## The GETBULK max-repetitions parameter.
# max_repetitions = 10
## SNMPv3 authentication and encryption options.
##
## Security Name.
# sec_name = "myuser"
## Authentication protocol; one of "MD5", "SHA", or "".
# auth_protocol = "MD5"
## Authentication password.
# auth_password = "pass"
## Security Level; one of "noAuthNoPriv", "authNoPriv", or "authPriv".
# sec_level = "authNoPriv"
## Context Name.
# context_name = ""
## Privacy protocol used for encrypted messages; one of "DES", "AES" or "".
# priv_protocol = ""
## Privacy password used for encrypted messages.
# priv_password = ""
## max_parallel_lookups is the maximum number of SNMP requests to
## make at the same time.
# max_parallel_lookups = 100
## ordered controls whether or not the metrics need to stay in the
## same order this plugin received them in. If false, this plugin
## may change the order when data is cached. If you need metrics to
## stay in order set this to true. keeping the metrics ordered may
## be slightly slower
# ordered = false
## cache_ttl is the amount of time interface names are cached for a
## given agent. After this period elapses if names are needed they
## will be retrieved again.
# cache_ttl = "8h"

View file

@ -0,0 +1,62 @@
package ifname
import (
"runtime"
"time"
)
type TTLValType struct {
time time.Time // when entry was added
val valType
}
type timeFunc func() time.Time
type TTLCache struct {
validDuration time.Duration
lru LRUCache
now timeFunc
}
func NewTTLCache(valid time.Duration, capacity uint) TTLCache {
return TTLCache{
lru: NewLRUCache(capacity),
validDuration: valid,
now: time.Now,
}
}
func (c *TTLCache) Get(key keyType) (valType, bool, time.Duration) {
v, ok := c.lru.Get(key)
if !ok {
return valType{}, false, 0
}
if runtime.GOOS == "windows" {
// Sometimes on Windows `c.now().Sub(v.time) == 0` due to clock resolution issues:
// https://github.com/golang/go/issues/17696
// https://github.com/golang/go/issues/29485
// Force clock to refresh:
time.Sleep(time.Nanosecond)
}
age := c.now().Sub(v.time)
if age < c.validDuration {
return v.val, ok, age
}
c.lru.Delete(key)
return valType{}, false, 0
}
func (c *TTLCache) Put(key keyType, value valType) {
v := TTLValType{
val: value,
time: c.now(),
}
c.lru.Put(key, v)
}
func (c *TTLCache) Delete(key keyType) {
c.lru.Delete(key)
}

View file

@ -0,0 +1,43 @@
package ifname
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestTTLCacheExpire(t *testing.T) {
c := NewTTLCache(1*time.Second, 100)
c.now = func() time.Time {
return time.Unix(0, 0)
}
c.Put("ones", nameMap{1: "one"})
require.Len(t, c.lru.m, 1)
c.now = func() time.Time {
return time.Unix(1, 0)
}
_, ok, _ := c.Get("ones")
require.False(t, ok)
require.Empty(t, c.lru.m)
require.Equal(t, 0, c.lru.l.Len())
}
func TestTTLCache(t *testing.T) {
c := NewTTLCache(1*time.Second, 100)
c.now = func() time.Time {
return time.Unix(0, 0)
}
expected := nameMap{1: "one"}
c.Put("ones", expected)
actual, ok, _ := c.Get("ones")
require.True(t, ok)
require.Equal(t, expected, actual)
}