1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,107 @@
# Logical Volume Manager Input Plugin
This plugin collects information about physical volumes, volume groups and
logical volumes from the Logical Volume Management (LVM) of the
[Linux kernel][kernel].
⭐ Telegraf v1.21.0
🏷️ system
💻 linux
[kernel]: https://www.kernel.org/
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Read metrics about LVM physical volumes, volume groups, logical volumes.
[[inputs.lvm]]
## Use sudo to run LVM commands
use_sudo = false
## The default location of the pvs binary can be overridden with:
#pvs_binary = "/usr/sbin/pvs"
## The default location of the vgs binary can be overridden with:
#vgs_binary = "/usr/sbin/vgs"
## The default location of the lvs binary can be overridden with:
#lvs_binary = "/usr/sbin/lvs"
```
The LVM commands requires elevated permissions. If the user has configured sudo
with the ability to run these commands, then set the `use_sudo` to true.
### 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
```
Path to binaries must match those from config file (pvs_binary, vgs_binary and
lvs_binary)
## 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:
```text
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
```

288
plugins/inputs/lvm/lvm.go Normal file
View file

@ -0,0 +1,288 @@
//go:generate ../../../tools/readme_config_includer/generator
package lvm
import (
_ "embed"
"encoding/json"
"fmt"
"os/exec"
"strconv"
"strings"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
var (
execCommand = exec.Command
)
type LVM struct {
UseSudo bool `toml:"use_sudo"`
PVSBinary string `toml:"pvs_binary"`
VGSBinary string `toml:"vgs_binary"`
LVSBinary string `toml:"lvs_binary"`
}
func (*LVM) SampleConfig() string {
return sampleConfig
}
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 {
args := []string{
"--reportformat", "json", "--units", "b", "--nosuffix",
"-o", "pv_name,vg_name,pv_size,pv_free,pv_used",
}
out, err := lvm.runCmd(lvm.PVSBinary, 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: %w", 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 {
args := []string{
"--reportformat", "json", "--units", "b", "--nosuffix",
"-o", "vg_name,pv_count,lv_count,snap_count,vg_size,vg_free",
}
out, err := lvm.runCmd(lvm.VGSBinary, 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: %w", 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 {
args := []string{
"--reportformat", "json", "--units", "b", "--nosuffix",
"-o", "lv_name,vg_name,lv_size,data_percent,metadata_percent",
}
out, err := lvm.runCmd(lvm.LVSBinary, 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: %w", 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: %w - %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{
PVSBinary: "/usr/sbin/pvs",
VGSBinary: "/usr/sbin/vgs",
LVSBinary: "/usr/sbin/lvs",
}
})
}

View file

@ -0,0 +1,214 @@
package lvm
import (
"fmt"
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/testutil"
)
func TestGather(t *testing.T) {
var acc testutil.Accumulator
lvm := LVM{
PVSBinary: "/usr/sbin/pvs",
VGSBinary: "/usr/sbin/vgs",
LVSBinary: "/usr/sbin/lvs",
}
// 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" {
fmt.Fprint(os.Stdout, mockPVSData)
} else if cmd == "/usr/sbin/vgs" {
fmt.Fprint(os.Stdout, mockVGSData)
} else if cmd == "/usr/sbin/lvs" {
fmt.Fprint(os.Stdout, mockLVSData)
} else {
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 acc testutil.Accumulator
noLVM := LVM{
PVSBinary: "/usr/sbin/pvs",
VGSBinary: "/usr/sbin/vgs",
LVSBinary: "/usr/sbin/lvs",
}
// 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" {
fmt.Fprint(os.Stdout, mockPVSData)
} else if cmd == "/usr/sbin/vgs" {
fmt.Fprint(os.Stdout, mockVGSData)
} else if cmd == "/usr/sbin/lvs" {
fmt.Fprint(os.Stdout, mockLVSData)
} else {
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)
}

View file

@ -0,0 +1,13 @@
# Read metrics about LVM physical volumes, volume groups, logical volumes.
[[inputs.lvm]]
## Use sudo to run LVM commands
use_sudo = false
## The default location of the pvs binary can be overridden with:
#pvs_binary = "/usr/sbin/pvs"
## The default location of the vgs binary can be overridden with:
#vgs_binary = "/usr/sbin/vgs"
## The default location of the lvs binary can be overridden with:
#lvs_binary = "/usr/sbin/lvs"