Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Linux Volume Manager input plugin #9771

Merged
merged 5 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1570,7 +1570,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_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",
Expand Down
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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"
Expand Down
77 changes: 77 additions & 0 deletions plugins/inputs/lvm/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 293 additions & 0 deletions plugins/inputs/lvm/lvm.go
Original file line number Diff line number Diff line change
@@ -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{}
})
}
Loading