feat: add Linux Volume Manager input plugin (#9771)

This commit is contained in:
Joshua Powers 2021-09-21 15:51:43 -06:00 committed by GitHub
parent a9898f179b
commit 3eebfd2f0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 583 additions and 1 deletions

View File

@ -1571,7 +1571,7 @@ func (c *Config) missingTomlField(_ reflect.Type, key string) error {
"grok_timezone", "grok_unique_timestamp", "influx_max_line_bytes", "influx_sort_fields",
"influx_uint_support", "interval", "json_name_key", "json_query", "json_strict",
"json_string_fields", "json_time_format", "json_time_key", "json_timestamp_format", "json_timestamp_units", "json_timezone", "json_v2",
"metric_batch_size", "metric_buffer_limit", "name_override", "name_prefix",
"lvm", "metric_batch_size", "metric_buffer_limit", "name_override", "name_prefix",
"name_suffix", "namedrop", "namepass", "order", "pass", "period", "precision",
"prefix", "prometheus_export_timestamp", "prometheus_sort_metrics", "prometheus_string_as_label",
"separator", "splunkmetric_hec_routing", "splunkmetric_multimetric", "tag_keys",

View File

@ -100,6 +100,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/logparser"
_ "github.com/influxdata/telegraf/plugins/inputs/logstash"
_ "github.com/influxdata/telegraf/plugins/inputs/lustre2"
_ "github.com/influxdata/telegraf/plugins/inputs/lvm"
_ "github.com/influxdata/telegraf/plugins/inputs/mailchimp"
_ "github.com/influxdata/telegraf/plugins/inputs/marklogic"
_ "github.com/influxdata/telegraf/plugins/inputs/mcrouter"

View File

@ -0,0 +1,77 @@
# LVM Input Plugin
The Logical Volume Management (LVM) input plugin collects information about
physical volumes, volume groups, and logical volumes.
### Configuration
The `lvm` command requires elevated permissions. If the user has configured
sudo with the ability to run these commands, then set the `use_sudo` to true.
```toml
# Read metrics about LVM physical volumes, volume groups, logical volumes.
[[inputs.lvm]]
## Use sudo to run LVM commands
use_sudo = false
```
#### Using sudo
If your account does not already have the ability to run commands
with passwordless sudo then updates to the sudoers file are required. Below
is an example to allow the requires LVM commands:
First, use the `visudo` command to start editing the sudoers file. Then add
the following content, where `<username>` is the username of the user that
needs this access:
```text
Cmnd_Alias LVM = /usr/sbin/pvs *, /usr/sbin/vgs *, /usr/sbin/lvs *
<username> ALL=(root) NOPASSWD: LVM
Defaults!LVM !logfile, !syslog, !pam_session
```
### Metrics
Metrics are broken out by physical volume (pv), volume group (vg), and logical
volume (lv):
- lvm_physical_vol
- tags
- path
- vol_group
- fields
- size
- free
- used
- used_percent
- lvm_vol_group
- tags
- name
- fields
- size
- free
- used_percent
- physical_volume_count
- logical_volume_count
- snapshot_count
- lvm_logical_vol
- tags
- name
- vol_group
- fields
- size
- data_percent
- meta_percent
### Example Output
The following example shows a system with the root partition on an LVM group
as well as with a Docker thin-provisioned LVM group on a second drive:
> lvm_physical_vol,path=/dev/sda2,vol_group=vgroot free=0i,size=249510756352i,used=249510756352i,used_percent=100 1631823026000000000
> lvm_physical_vol,path=/dev/sdb,vol_group=docker free=3858759680i,size=128316342272i,used=124457582592i,used_percent=96.99277612525741 1631823026000000000
> lvm_vol_group,name=vgroot free=0i,logical_volume_count=1i,physical_volume_count=1i,size=249510756352i,snapshot_count=0i,used_percent=100 1631823026000000000
> lvm_vol_group,name=docker free=3858759680i,logical_volume_count=1i,physical_volume_count=1i,size=128316342272i,snapshot_count=0i,used_percent=96.99277612525741 1631823026000000000
> lvm_logical_vol,name=lvroot,vol_group=vgroot data_percent=0,metadata_percent=0,size=249510756352i 1631823026000000000
> lvm_logical_vol,name=thinpool,vol_group=docker data_percent=0.36000001430511475,metadata_percent=1.3300000429153442,size=121899057152i 1631823026000000000

293
plugins/inputs/lvm/lvm.go Normal file
View File

@ -0,0 +1,293 @@
package lvm
import (
"encoding/json"
"fmt"
"os/exec"
"strconv"
"strings"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
)
var (
execCommand = exec.Command
)
var sampleConfig = `
## Use sudo to run LVM commands
use_sudo = false
`
type LVM struct {
UseSudo bool `toml:"use_sudo"`
}
func (lvm *LVM) Description() string {
return "Read metrics about LVM physical volumes, volume groups, logical volumes."
}
func (lvm *LVM) SampleConfig() string {
return sampleConfig
}
func (lvm *LVM) Init() error {
return nil
}
func (lvm *LVM) Gather(acc telegraf.Accumulator) error {
if err := lvm.gatherPhysicalVolumes(acc); err != nil {
return err
} else if err := lvm.gatherVolumeGroups(acc); err != nil {
return err
} else if err := lvm.gatherLogicalVolumes(acc); err != nil {
return err
}
return nil
}
func (lvm *LVM) gatherPhysicalVolumes(acc telegraf.Accumulator) error {
pvsCmd := "/usr/sbin/pvs"
args := []string{
"--reportformat", "json", "--units", "b", "--nosuffix",
"-o", "pv_name,vg_name,pv_size,pv_free,pv_used",
}
out, err := lvm.runCmd(pvsCmd, args)
if err != nil {
return err
}
var report pvsReport
err = json.Unmarshal(out, &report)
if err != nil {
return fmt.Errorf("failed to unmarshal physical volume JSON: %s", err)
}
if len(report.Report) > 0 {
for _, pv := range report.Report[0].Pv {
tags := map[string]string{
"path": pv.Name,
"vol_group": pv.VolGroup,
}
size, err := strconv.ParseUint(pv.Size, 10, 64)
if err != nil {
return err
}
free, err := strconv.ParseUint(pv.Free, 10, 64)
if err != nil {
return err
}
used, err := strconv.ParseUint(pv.Used, 10, 64)
if err != nil {
return err
}
usedPercent := float64(used) / float64(size) * 100
fields := map[string]interface{}{
"size": size,
"free": free,
"used": used,
"used_percent": usedPercent,
}
acc.AddFields("lvm_physical_vol", fields, tags)
}
}
return nil
}
func (lvm *LVM) gatherVolumeGroups(acc telegraf.Accumulator) error {
cmd := "/usr/sbin/vgs"
args := []string{
"--reportformat", "json", "--units", "b", "--nosuffix",
"-o", "vg_name,pv_count,lv_count,snap_count,vg_size,vg_free",
}
out, err := lvm.runCmd(cmd, args)
if err != nil {
return err
}
var report vgsReport
err = json.Unmarshal(out, &report)
if err != nil {
return fmt.Errorf("failed to unmarshal vol group JSON: %s", err)
}
if len(report.Report) > 0 {
for _, vg := range report.Report[0].Vg {
tags := map[string]string{
"name": vg.Name,
}
size, err := strconv.ParseUint(vg.Size, 10, 64)
if err != nil {
return err
}
free, err := strconv.ParseUint(vg.Free, 10, 64)
if err != nil {
return err
}
pvCount, err := strconv.ParseUint(vg.PvCount, 10, 64)
if err != nil {
return err
}
lvCount, err := strconv.ParseUint(vg.LvCount, 10, 64)
if err != nil {
return err
}
snapCount, err := strconv.ParseUint(vg.SnapCount, 10, 64)
if err != nil {
return err
}
usedPercent := (float64(size) - float64(free)) / float64(size) * 100
fields := map[string]interface{}{
"size": size,
"free": free,
"used_percent": usedPercent,
"physical_volume_count": pvCount,
"logical_volume_count": lvCount,
"snapshot_count": snapCount,
}
acc.AddFields("lvm_vol_group", fields, tags)
}
}
return nil
}
func (lvm *LVM) gatherLogicalVolumes(acc telegraf.Accumulator) error {
cmd := "/usr/sbin/lvs"
args := []string{
"--reportformat", "json", "--units", "b", "--nosuffix",
"-o", "lv_name,vg_name,lv_size,data_percent,metadata_percent",
}
out, err := lvm.runCmd(cmd, args)
if err != nil {
return err
}
var report lvsReport
err = json.Unmarshal(out, &report)
if err != nil {
return fmt.Errorf("failed to unmarshal logical vol JSON: %s", err)
}
if len(report.Report) > 0 {
for _, lv := range report.Report[0].Lv {
tags := map[string]string{
"name": lv.Name,
"vol_group": lv.VolGroup,
}
size, err := strconv.ParseUint(lv.Size, 10, 64)
if err != nil {
return err
}
// Does not apply to all logical volumes, set default value
if lv.DataPercent == "" {
lv.DataPercent = "0.0"
}
dataPercent, err := strconv.ParseFloat(lv.DataPercent, 32)
if err != nil {
return err
}
// Does not apply to all logical volumes, set default value
if lv.MetadataPercent == "" {
lv.MetadataPercent = "0.0"
}
metadataPercent, err := strconv.ParseFloat(lv.MetadataPercent, 32)
if err != nil {
return err
}
fields := map[string]interface{}{
"size": size,
"data_percent": dataPercent,
"metadata_percent": metadataPercent,
}
acc.AddFields("lvm_logical_vol", fields, tags)
}
}
return nil
}
func (lvm *LVM) runCmd(cmd string, args []string) ([]byte, error) {
execCmd := execCommand(cmd, args...)
if lvm.UseSudo {
execCmd = execCommand("sudo", append([]string{"-n", cmd}, args...)...)
}
out, err := internal.StdOutputTimeout(execCmd, 5*time.Second)
if err != nil {
return nil, fmt.Errorf(
"failed to run command %s: %s - %s",
strings.Join(execCmd.Args, " "), err, string(out),
)
}
return out, nil
}
// Represents info about physical volume command, pvs, output
type pvsReport struct {
Report []struct {
Pv []struct {
Name string `json:"pv_name"`
VolGroup string `json:"vg_name"`
Size string `json:"pv_size"`
Free string `json:"pv_free"`
Used string `json:"pv_used"`
} `json:"pv"`
} `json:"report"`
}
// Represents info about volume group command, vgs, output
type vgsReport struct {
Report []struct {
Vg []struct {
Name string `json:"vg_name"`
Size string `json:"vg_size"`
Free string `json:"vg_free"`
LvCount string `json:"lv_count"`
PvCount string `json:"pv_count"`
SnapCount string `json:"snap_count"`
} `json:"vg"`
} `json:"report"`
}
// Represents info about logical volume command, lvs, output
type lvsReport struct {
Report []struct {
Lv []struct {
Name string `json:"lv_name"`
VolGroup string `json:"vg_name"`
Size string `json:"lv_size"`
DataPercent string `json:"data_percent"`
MetadataPercent string `json:"metadata_percent"`
} `json:"lv"`
} `json:"report"`
}
func init() {
inputs.Add("lvm", func() telegraf.Input {
return &LVM{}
})
}

View File

@ -0,0 +1,211 @@
package lvm
import (
"fmt"
"os"
"os/exec"
"testing"
"github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/require"
)
func TestGather(t *testing.T) {
var lvm LVM = LVM{UseSudo: false}
var acc testutil.Accumulator
// overwriting exec commands with mock commands
execCommand = fakeExecCommand
err := lvm.Gather(&acc)
require.NoError(t, err)
pvsTags := map[string]string{
"path": "/dev/sdb",
"vol_group": "docker",
}
pvsFields := map[string]interface{}{
"size": uint64(128316342272),
"free": uint64(3858759680),
"used": uint64(124457582592),
"used_percent": 96.99277612525741,
}
acc.AssertContainsTaggedFields(t, "lvm_physical_vol", pvsFields, pvsTags)
vgsTags := map[string]string{
"name": "docker",
}
vgsFields := map[string]interface{}{
"size": uint64(128316342272),
"free": uint64(3858759680),
"used_percent": 96.99277612525741,
"physical_volume_count": uint64(1),
"logical_volume_count": uint64(1),
"snapshot_count": uint64(0),
}
acc.AssertContainsTaggedFields(t, "lvm_vol_group", vgsFields, vgsTags)
lvsTags := map[string]string{
"name": "thinpool",
"vol_group": "docker",
}
lvsFields := map[string]interface{}{
"size": uint64(121899057152),
"data_percent": 0.36000001430511475,
"metadata_percent": 1.3300000429153442,
}
acc.AssertContainsTaggedFields(t, "lvm_logical_vol", lvsFields, lvsTags)
}
// Used as a helper function that mock the exec.Command call
func fakeExecCommand(command string, args ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, args...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
// Used to mock exec.Command output
func TestHelperProcess(_ *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
mockPVSData := `{
"report": [
{
"pv": [
{"pv_name":"/dev/sdb", "vg_name":"docker", "pv_size":"128316342272", "pv_free":"3858759680", "pv_used":"124457582592"}
]
}
]
}
`
mockVGSData := `{
"report": [
{
"vg": [
{"vg_name":"docker", "pv_count":"1", "lv_count":"1", "snap_count":"0", "vg_size":"128316342272", "vg_free":"3858759680"}
]
}
]
}
`
mockLVSData := `{
"report": [
{
"lv": [
{"lv_name":"thinpool", "vg_name":"docker", "lv_size":"121899057152", "data_percent":"0.36", "metadata_percent":"1.33"}
]
}
]
}
`
// Previous arguments are tests stuff, that looks like :
// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
args := os.Args
cmd := args[3]
if cmd == "/usr/sbin/pvs" {
//nolint:errcheck,revive // test will fail anyway
fmt.Fprint(os.Stdout, mockPVSData)
} else if cmd == "/usr/sbin/vgs" {
//nolint:errcheck,revive // test will fail anyway
fmt.Fprint(os.Stdout, mockVGSData)
} else if cmd == "/usr/sbin/lvs" {
//nolint:errcheck,revive // test will fail anyway
fmt.Fprint(os.Stdout, mockLVSData)
} else {
//nolint:errcheck,revive // test will fail anyway
fmt.Fprint(os.Stdout, "command not found")
//nolint:revive // error code is important for this "test"
os.Exit(1)
}
//nolint:revive // error code is important for this "test"
os.Exit(0)
}
// test when no lvm devices exist
func TestGatherNoLVM(t *testing.T) {
var noLVM LVM = LVM{UseSudo: false}
var acc testutil.Accumulator
// overwriting exec commands with mock commands
execCommand = fakeExecCommandNoLVM
err := noLVM.Gather(&acc)
require.NoError(t, err)
acc.AssertDoesNotContainMeasurement(t, "lvm_physical_vol")
acc.AssertDoesNotContainMeasurement(t, "lvm_vol_group")
acc.AssertDoesNotContainMeasurement(t, "lvm_logical_vol")
}
// Used as a helper function that mock the exec.Command call
func fakeExecCommandNoLVM(command string, args ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcessNoLVM", "--", command}
cs = append(cs, args...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
// Used to mock exec.Command output
func TestHelperProcessNoLVM(_ *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
mockPVSData := `{
"report": [
{
"pv": [
]
}
]
}
`
mockVGSData := `{
"report": [
{
"vg": [
]
}
]
}
`
mockLVSData := `{
"report": [
{
"lv": [
]
}
]
}
`
// Previous arguments are tests stuff, that looks like :
// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
args := os.Args
cmd := args[3]
if cmd == "/usr/sbin/pvs" {
//nolint:errcheck,revive // test will fail anyway
fmt.Fprint(os.Stdout, mockPVSData)
} else if cmd == "/usr/sbin/vgs" {
//nolint:errcheck,revive // test will fail anyway
fmt.Fprint(os.Stdout, mockVGSData)
} else if cmd == "/usr/sbin/lvs" {
//nolint:errcheck,revive // test will fail anyway
fmt.Fprint(os.Stdout, mockLVSData)
} else {
//nolint:errcheck,revive // test will fail anyway
fmt.Fprint(os.Stdout, "command not found")
//nolint:revive // error code is important for this "test"
os.Exit(1)
}
//nolint:revive // error code is important for this "test"
os.Exit(0)
}