mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
But not using it as some linters are either plain incorrect (the one suggesting to not use nil for `c.t.Context()`) or just debatable (checking for err value is a good practice, but there are good reasons to opt out in some cases).
296 lines
7.7 KiB
Go
296 lines
7.7 KiB
Go
// SPDX-FileCopyrightText: 2022 Free Mobile
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package snmp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/gob"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/benbjohnson/clock"
|
|
|
|
"akvorado/common/reporter"
|
|
)
|
|
|
|
var (
|
|
// ErrCacheMiss is triggered on lookup cache miss
|
|
ErrCacheMiss = errors.New("SNMP cache miss")
|
|
// ErrCacheVersion is triggered when loading a cache from an incompatible version
|
|
ErrCacheVersion = errors.New("SNMP cache version mismatch")
|
|
// cacheCurrentVersionNumber is the current version of the on-disk cache format
|
|
cacheCurrentVersionNumber = 8
|
|
)
|
|
|
|
// snmpCache represents the SNMP cache.
|
|
type snmpCache struct {
|
|
r *reporter.Reporter
|
|
cache map[string]*cachedExporter
|
|
cacheLock sync.RWMutex
|
|
clock clock.Clock
|
|
|
|
metrics struct {
|
|
cacheHit reporter.Counter
|
|
cacheMiss reporter.Counter
|
|
cacheExpired reporter.Counter
|
|
cacheSize reporter.GaugeFunc
|
|
cacheExporters reporter.GaugeFunc
|
|
}
|
|
}
|
|
|
|
// cachedExporter represents information about a exporter. It includes
|
|
// the mapping from ifIndex to interfaces.
|
|
type cachedExporter struct {
|
|
Name string
|
|
Interfaces map[uint]*cachedInterface
|
|
}
|
|
|
|
// Interface contains the information about an interface.
|
|
type Interface struct {
|
|
Name string
|
|
Description string
|
|
Speed uint
|
|
}
|
|
|
|
// cachedInterface contains the information about a cached interface.
|
|
type cachedInterface struct {
|
|
LastUpdated int64
|
|
LastAccessed int64
|
|
Interface
|
|
}
|
|
|
|
func newSNMPCache(r *reporter.Reporter, clock clock.Clock) *snmpCache {
|
|
sc := &snmpCache{
|
|
r: r,
|
|
cache: make(map[string]*cachedExporter),
|
|
clock: clock,
|
|
}
|
|
sc.metrics.cacheHit = r.Counter(
|
|
reporter.CounterOpts{
|
|
Name: "cache_hit",
|
|
Help: "Number of lookups retrieved from cache.",
|
|
})
|
|
sc.metrics.cacheMiss = r.Counter(
|
|
reporter.CounterOpts{
|
|
Name: "cache_miss",
|
|
Help: "Number of lookup miss.",
|
|
})
|
|
sc.metrics.cacheExpired = r.Counter(
|
|
reporter.CounterOpts{
|
|
Name: "cache_expired",
|
|
Help: "Number of cache entries expired.",
|
|
})
|
|
sc.metrics.cacheSize = r.GaugeFunc(
|
|
reporter.GaugeOpts{
|
|
Name: "cache_size",
|
|
Help: "Number of entries in cache.",
|
|
}, func() (result float64) {
|
|
sc.cacheLock.RLock()
|
|
defer sc.cacheLock.RUnlock()
|
|
for _, exporter := range sc.cache {
|
|
result += float64(len(exporter.Interfaces))
|
|
}
|
|
return
|
|
})
|
|
sc.metrics.cacheExporters = r.GaugeFunc(
|
|
reporter.GaugeOpts{
|
|
Name: "cache_exporters",
|
|
Help: "Number of exporters in cache.",
|
|
}, func() float64 {
|
|
sc.cacheLock.RLock()
|
|
defer sc.cacheLock.RUnlock()
|
|
return float64(len(sc.cache))
|
|
})
|
|
return sc
|
|
}
|
|
|
|
// Lookup will perform a lookup of the cache. It returns the exporter
|
|
// name as well as the requested interface.
|
|
func (sc *snmpCache) Lookup(ip string, ifIndex uint) (string, Interface, error) {
|
|
return sc.lookup(ip, ifIndex, true)
|
|
}
|
|
|
|
func (sc *snmpCache) lookup(ip string, ifIndex uint, touchAccess bool) (string, Interface, error) {
|
|
sc.cacheLock.RLock()
|
|
defer sc.cacheLock.RUnlock()
|
|
exporter, ok := sc.cache[ip]
|
|
if !ok {
|
|
sc.metrics.cacheMiss.Inc()
|
|
return "", Interface{}, ErrCacheMiss
|
|
}
|
|
iface, ok := exporter.Interfaces[ifIndex]
|
|
if !ok {
|
|
sc.metrics.cacheMiss.Inc()
|
|
return "", Interface{}, ErrCacheMiss
|
|
}
|
|
sc.metrics.cacheHit.Inc()
|
|
if touchAccess {
|
|
atomic.StoreInt64(&iface.LastAccessed, sc.clock.Now().Unix())
|
|
}
|
|
return exporter.Name, iface.Interface, nil
|
|
}
|
|
|
|
// Put a new entry in the cache.
|
|
func (sc *snmpCache) Put(ip string, exporterName string, ifIndex uint, iface Interface) {
|
|
sc.cacheLock.Lock()
|
|
defer sc.cacheLock.Unlock()
|
|
|
|
now := sc.clock.Now().Unix()
|
|
ciface := cachedInterface{
|
|
LastUpdated: now,
|
|
LastAccessed: now,
|
|
Interface: iface,
|
|
}
|
|
exporter, ok := sc.cache[ip]
|
|
if !ok {
|
|
exporter = &cachedExporter{Interfaces: make(map[uint]*cachedInterface)}
|
|
sc.cache[ip] = exporter
|
|
}
|
|
exporter.Name = exporterName
|
|
exporter.Interfaces[ifIndex] = &ciface
|
|
}
|
|
|
|
// Expire expire entries older than the provided duration (rely on last access).
|
|
func (sc *snmpCache) Expire(older time.Duration) (count uint) {
|
|
threshold := sc.clock.Now().Add(-older).Unix()
|
|
|
|
sc.cacheLock.Lock()
|
|
defer sc.cacheLock.Unlock()
|
|
|
|
for ip, exporter := range sc.cache {
|
|
for ifindex, iface := range exporter.Interfaces {
|
|
if iface.LastAccessed < threshold {
|
|
delete(exporter.Interfaces, ifindex)
|
|
sc.metrics.cacheExpired.Inc()
|
|
count++
|
|
}
|
|
}
|
|
if len(exporter.Interfaces) == 0 {
|
|
delete(sc.cache, ip)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Return entries older than the provided duration. If LastAccessed is
|
|
// true, rely on last access, otherwise on last update.
|
|
func (sc *snmpCache) entriesOlderThan(older time.Duration, lastAccessed bool) map[string]map[uint]Interface {
|
|
threshold := sc.clock.Now().Add(-older).Unix()
|
|
result := make(map[string]map[uint]Interface)
|
|
|
|
sc.cacheLock.RLock()
|
|
defer sc.cacheLock.RUnlock()
|
|
|
|
for ip, exporter := range sc.cache {
|
|
for ifindex, iface := range exporter.Interfaces {
|
|
what := &iface.LastAccessed
|
|
if !lastAccessed {
|
|
what = &iface.LastUpdated
|
|
}
|
|
if atomic.LoadInt64(what) < threshold {
|
|
_, ok := result[ip]
|
|
if !ok {
|
|
rifaces := make(map[uint]Interface)
|
|
result[ip] = rifaces
|
|
}
|
|
result[ip][ifindex] = iface.Interface
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Need updates returns a map of interface entries that would need to
|
|
// be updated. It relies on last update.
|
|
func (sc *snmpCache) NeedUpdates(older time.Duration) map[string]map[uint]Interface {
|
|
return sc.entriesOlderThan(older, false)
|
|
}
|
|
|
|
// Need updates returns a map of interface entries that would have
|
|
// expired. It relies on last access.
|
|
func (sc *snmpCache) WouldExpire(older time.Duration) map[string]map[uint]Interface {
|
|
return sc.entriesOlderThan(older, true)
|
|
}
|
|
|
|
// Save stores the cache to the provided location.
|
|
func (sc *snmpCache) Save(cacheFile string) error {
|
|
tmpFile, err := ioutil.TempFile(
|
|
filepath.Dir(cacheFile),
|
|
fmt.Sprintf("%s-*", filepath.Base(cacheFile)))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create cache file %q: %w", cacheFile, err)
|
|
}
|
|
defer func() {
|
|
tmpFile.Close() // ignore errors
|
|
os.Remove(tmpFile.Name()) // ignore errors
|
|
}()
|
|
|
|
// Write cache
|
|
encoder := gob.NewEncoder(tmpFile)
|
|
if err := encoder.Encode(sc); err != nil {
|
|
return fmt.Errorf("unable to encode cache: %w", err)
|
|
}
|
|
|
|
// Move cache to new location
|
|
if err := os.Rename(tmpFile.Name(), cacheFile); err != nil {
|
|
return fmt.Errorf("unable to write cache file %q: %w", cacheFile, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Load loads the cache from the provided location.
|
|
func (sc *snmpCache) Load(cacheFile string) error {
|
|
f, err := os.Open(cacheFile)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to load cache %q: %w", cacheFile, err)
|
|
}
|
|
decoder := gob.NewDecoder(f)
|
|
if err := decoder.Decode(sc); err != nil {
|
|
return fmt.Errorf("unable to decode cache: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GobEncode encodes the SNMP cache.
|
|
func (sc *snmpCache) GobEncode() ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
encoder := gob.NewEncoder(&buf)
|
|
if err := encoder.Encode(&cacheCurrentVersionNumber); err != nil {
|
|
return nil, err
|
|
}
|
|
sc.cacheLock.Lock() // needed because we won't do atomic access
|
|
defer sc.cacheLock.Unlock()
|
|
if err := encoder.Encode(sc.cache); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// GobDecode decodes the SNMP cache.
|
|
func (sc *snmpCache) GobDecode(data []byte) error {
|
|
buf := bytes.NewBuffer(data)
|
|
decoder := gob.NewDecoder(buf)
|
|
version := cacheCurrentVersionNumber
|
|
if err := decoder.Decode(&version); err != nil {
|
|
return err
|
|
}
|
|
if version != cacheCurrentVersionNumber {
|
|
return ErrCacheVersion
|
|
}
|
|
cache := map[string]*cachedExporter{}
|
|
if err := decoder.Decode(&cache); err != nil {
|
|
return err
|
|
}
|
|
sc.cacheLock.Lock()
|
|
sc.cache = cache
|
|
sc.cacheLock.Unlock()
|
|
return nil
|
|
}
|