310 lines
7.8 KiB
Go
310 lines
7.8 KiB
Go
//go:generate ../../../tools/readme_config_includer/generator
|
|
package bond
|
|
|
|
import (
|
|
"bufio"
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/influxdata/telegraf"
|
|
"github.com/influxdata/telegraf/internal"
|
|
"github.com/influxdata/telegraf/plugins/inputs"
|
|
)
|
|
|
|
//go:embed sample.conf
|
|
var sampleConfig string
|
|
|
|
type Bond struct {
|
|
HostProc string `toml:"host_proc"`
|
|
HostSys string `toml:"host_sys"`
|
|
SysDetails bool `toml:"collect_sys_details"`
|
|
BondInterfaces []string `toml:"bond_interfaces"`
|
|
BondType string
|
|
}
|
|
|
|
type sysFiles struct {
|
|
ModeFile string
|
|
SlaveFile string
|
|
ADPortsFile string
|
|
}
|
|
|
|
func (*Bond) SampleConfig() string {
|
|
return sampleConfig
|
|
}
|
|
|
|
func (bond *Bond) Gather(acc telegraf.Accumulator) error {
|
|
// load proc path, get default value if config value and env variable are empty
|
|
bond.loadPaths()
|
|
// list bond interfaces from bonding directory or gather all interfaces.
|
|
bondNames, err := bond.listInterfaces()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, bondName := range bondNames {
|
|
bondAbsPath := bond.HostProc + "/net/bonding/" + bondName
|
|
file, err := os.ReadFile(bondAbsPath)
|
|
if err != nil {
|
|
acc.AddError(fmt.Errorf("error inspecting %q interface: %w", bondAbsPath, err))
|
|
continue
|
|
}
|
|
rawProcFile := strings.TrimSpace(string(file))
|
|
err = bond.gatherBondInterface(bondName, rawProcFile, acc)
|
|
if err != nil {
|
|
acc.AddError(fmt.Errorf("error inspecting %q interface: %w", bondName, err))
|
|
}
|
|
|
|
/*
|
|
Some details about bonds only exist in /sys/class/net/
|
|
In particular, LACP bonds track upstream port state here
|
|
*/
|
|
if bond.SysDetails {
|
|
files, err := bond.readSysFiles(bond.HostSys + "/class/net/" + bondName)
|
|
if err != nil {
|
|
acc.AddError(err)
|
|
}
|
|
gatherSysDetails(bondName, files, acc)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bond *Bond) gatherBondInterface(bondName, rawFile string, acc telegraf.Accumulator) error {
|
|
splitIndex := strings.Index(rawFile, "Slave Interface:")
|
|
if splitIndex == -1 {
|
|
splitIndex = len(rawFile)
|
|
}
|
|
bondPart := rawFile[:splitIndex]
|
|
slavePart := rawFile[splitIndex:]
|
|
|
|
err := bond.gatherBondPart(bondName, bondPart, acc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bond.gatherSlavePart(bondName, slavePart, acc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bond *Bond) gatherBondPart(bondName, rawFile string, acc telegraf.Accumulator) error {
|
|
fields := make(map[string]interface{})
|
|
tags := map[string]string{
|
|
"bond": bondName,
|
|
}
|
|
scanner := bufio.NewScanner(strings.NewReader(rawFile))
|
|
/*
|
|
/proc/bond/... files are formatted in a way that is difficult
|
|
to use regexes to parse. Because of that, we scan through
|
|
the file one line at a time and rely on specific lines to
|
|
mark "ends" of blocks. It's a hack that should be resolved,
|
|
but for now, it works.
|
|
*/
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
stats := strings.Split(line, ":")
|
|
if len(stats) < 2 {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(stats[0])
|
|
value := strings.TrimSpace(stats[1])
|
|
if name == "Bonding Mode" {
|
|
bond.BondType = value
|
|
}
|
|
if strings.Contains(name, "Currently Active Slave") {
|
|
fields["active_slave"] = value
|
|
}
|
|
if strings.Contains(name, "MII Status") {
|
|
fields["status"] = 0
|
|
if value == "up" {
|
|
fields["status"] = 1
|
|
}
|
|
acc.AddFields("bond", fields, tags)
|
|
return nil
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("couldn't find status info for %q", bondName)
|
|
}
|
|
|
|
func (bond *Bond) readSysFiles(bondDir string) (sysFiles, error) {
|
|
/*
|
|
Files we may need
|
|
bonding/mode
|
|
bonding/slaves
|
|
bonding/ad_num_ports
|
|
|
|
We load files here first to allow for easier testing
|
|
*/
|
|
var output sysFiles
|
|
|
|
file, err := os.ReadFile(bondDir + "/bonding/mode")
|
|
if err != nil {
|
|
return sysFiles{}, fmt.Errorf("error inspecting %q interface: %w", bondDir+"/bonding/mode", err)
|
|
}
|
|
output.ModeFile = strings.TrimSpace(string(file))
|
|
file, err = os.ReadFile(bondDir + "/bonding/slaves")
|
|
if err != nil {
|
|
return sysFiles{}, fmt.Errorf("error inspecting %q interface: %w", bondDir+"/bonding/slaves", err)
|
|
}
|
|
output.SlaveFile = strings.TrimSpace(string(file))
|
|
if bond.BondType == "IEEE 802.3ad Dynamic link aggregation" {
|
|
file, err = os.ReadFile(bondDir + "/bonding/ad_num_ports")
|
|
if err != nil {
|
|
return sysFiles{}, fmt.Errorf("error inspecting %q interface: %w", bondDir+"/bonding/ad_num_ports", err)
|
|
}
|
|
output.ADPortsFile = strings.TrimSpace(string(file))
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func gatherSysDetails(bondName string, files sysFiles, acc telegraf.Accumulator) {
|
|
var slaves []string
|
|
var adPortCount int
|
|
|
|
// To start with, we get the bond operating mode
|
|
mode := strings.TrimSpace(strings.Split(files.ModeFile, " ")[0])
|
|
|
|
tags := map[string]string{
|
|
"bond": bondName,
|
|
"mode": mode,
|
|
}
|
|
|
|
// Next we collect the number of bond slaves the system expects
|
|
slavesTmp := strings.Split(files.SlaveFile, " ")
|
|
for _, slave := range slavesTmp {
|
|
if slave != "" {
|
|
slaves = append(slaves, slave)
|
|
}
|
|
}
|
|
if mode == "802.3ad" {
|
|
/*
|
|
If we're in LACP mode, we should check on how the bond ports are
|
|
interacting with the upstream switch ports
|
|
a failed conversion can be treated as 0 ports
|
|
*/
|
|
if pc, err := strconv.Atoi(strings.TrimSpace(files.ADPortsFile)); err == nil {
|
|
adPortCount = pc
|
|
}
|
|
} else {
|
|
adPortCount = len(slaves)
|
|
}
|
|
|
|
fields := map[string]interface{}{
|
|
"slave_count": len(slaves),
|
|
"ad_port_count": adPortCount,
|
|
}
|
|
acc.AddFields("bond_sys", fields, tags)
|
|
}
|
|
|
|
func (bond *Bond) gatherSlavePart(bondName, rawFile string, acc telegraf.Accumulator) error {
|
|
var slaveCount int
|
|
tags := map[string]string{
|
|
"bond": bondName,
|
|
}
|
|
fields := map[string]interface{}{
|
|
"status": 0,
|
|
}
|
|
var scanPast bool
|
|
if bond.BondType == "IEEE 802.3ad Dynamic link aggregation" {
|
|
scanPast = true
|
|
}
|
|
|
|
scanner := bufio.NewScanner(strings.NewReader(rawFile))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
stats := strings.Split(line, ":")
|
|
if len(stats) < 2 {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(stats[0])
|
|
value := strings.TrimSpace(stats[1])
|
|
if strings.Contains(name, "Slave Interface") {
|
|
tags["interface"] = value
|
|
slaveCount++
|
|
}
|
|
if strings.Contains(name, "MII Status") && value == "up" {
|
|
fields["status"] = 1
|
|
}
|
|
if strings.Contains(name, "Link Failure Count") {
|
|
count, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fields["failures"] = count
|
|
if !scanPast {
|
|
acc.AddFields("bond_slave", fields, tags)
|
|
fields = map[string]interface{}{
|
|
"status": 0,
|
|
}
|
|
}
|
|
}
|
|
if strings.Contains(name, "Actor Churned Count") {
|
|
count, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fields["actor_churned"] = count
|
|
}
|
|
if strings.Contains(name, "Partner Churned Count") {
|
|
count, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fields["partner_churned"] = count
|
|
fields["total_churned"] = fields["actor_churned"].(int) + fields["partner_churned"].(int)
|
|
acc.AddFields("bond_slave", fields, tags)
|
|
fields = map[string]interface{}{
|
|
"status": 0,
|
|
}
|
|
}
|
|
}
|
|
tags = map[string]string{
|
|
"bond": bondName,
|
|
}
|
|
fields = map[string]interface{}{
|
|
"count": slaveCount,
|
|
}
|
|
acc.AddFields("bond_slave", fields, tags)
|
|
|
|
return scanner.Err()
|
|
}
|
|
|
|
// loadPaths can be used to read path firstly from config
|
|
// if it is empty then try read from env variable
|
|
func (bond *Bond) loadPaths() {
|
|
if bond.HostProc == "" {
|
|
bond.HostProc = internal.GetProcPath()
|
|
}
|
|
if bond.HostSys == "" {
|
|
bond.HostSys = internal.GetSysPath()
|
|
}
|
|
}
|
|
|
|
func (bond *Bond) listInterfaces() ([]string, error) {
|
|
var interfaces []string
|
|
if len(bond.BondInterfaces) > 0 {
|
|
interfaces = bond.BondInterfaces
|
|
} else {
|
|
paths, err := filepath.Glob(bond.HostProc + "/net/bonding/*")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, p := range paths {
|
|
interfaces = append(interfaces, filepath.Base(p))
|
|
}
|
|
}
|
|
return interfaces, nil
|
|
}
|
|
|
|
func init() {
|
|
inputs.Add("bond", func() telegraf.Input {
|
|
return &Bond{}
|
|
})
|
|
}
|