diff --git a/config/config.go b/config/config.go index b6eed9446162f..e64d893bc05ea 100644 --- a/config/config.go +++ b/config/config.go @@ -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", diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 60a52903ef079..690df0d3b0e46 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -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" diff --git a/plugins/inputs/lvm/README.md b/plugins/inputs/lvm/README.md new file mode 100644 index 0000000000000..c0ce1a2e6008a --- /dev/null +++ b/plugins/inputs/lvm/README.md @@ -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 `` is the username of the user that +needs this access: + +```text +Cmnd_Alias LVM = /usr/sbin/pvs *, /usr/sbin/vgs *, /usr/sbin/lvs * + 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 diff --git a/plugins/inputs/lvm/lvm.go b/plugins/inputs/lvm/lvm.go new file mode 100644 index 0000000000000..ce46af8a31c04 --- /dev/null +++ b/plugins/inputs/lvm/lvm.go @@ -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{} + }) +} diff --git a/plugins/inputs/lvm/lvm_test.go b/plugins/inputs/lvm/lvm_test.go new file mode 100644 index 0000000000000..c48eff5c039b1 --- /dev/null +++ b/plugins/inputs/lvm/lvm_test.go @@ -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) +}