Skip to content

Commit

Permalink
feat(inputs.proxmox): Allow to add VM-id and status as tag (influxdat…
Browse files Browse the repository at this point in the history
  • Loading branch information
superdiego98 authored Jan 16, 2025
1 parent 237dd3d commit b613db3
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 182 deletions.
16 changes: 11 additions & 5 deletions plugins/inputs/proxmox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
```toml @sample.conf
# Provides metrics from Proxmox nodes (Proxmox Virtual Environment > 6.2).
[[inputs.proxmox]]
## API connection configuration. The API token was introduced in Proxmox v6.2. Required permissions for user and token: PVEAuditor role on /.
## API connection configuration. The API token was introduced in Proxmox v6.2.
## Required permissions for user and token: PVEAuditor role on /.
base_url = "https://localhost:8006/api2/json"
api_token = "USER@REALM!TOKENID=UUID"

Expand All @@ -29,15 +30,19 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## leaving this empty will often lead to a "search domain is not set" error.
# node_name = ""

## Additional tags of the VM stats data to add as a tag
## Supported values are "vmid" and "status"
# additional_vmstats_tags = []

## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Use TLS but skip chain & host verification
insecure_skip_verify = false
# insecure_skip_verify = false

# HTTP response timeout (default: 5s)
response_timeout = "5s"
## HTTP response timeout (default: 5s)
# response_timeout = "5s"
```

### Permissions
Expand Down Expand Up @@ -88,9 +93,10 @@ See this [Proxmox docs example][1] for further details.
- vm_name - Name of the VM/container
- vm_fqdn - FQDN of the VM/container
- vm_type - Type of the VM/container (lxc, qemu)
- vm_id - ID of the VM/container

## Example Output

```text
proxmox,host=pxnode,node_fqdn=pxnode.example.com,vm_fqdn=vm1.example.com,vm_name=vm1,vm_type=lxc cpuload=0.147998116735236,disk_free=4461129728i,disk_total=5217320960i,disk_used=756191232i,disk_used_percentage=14,mem_free=1046827008i,mem_total=1073741824i,mem_used=26914816i,mem_used_percentage=2,status="running",swap_free=536698880i,swap_total=536870912i,swap_used=172032i,swap_used_percentage=0,uptime=1643793i 1595457277000000000
proxmox,host=pxnode,node_fqdn=pxnode.example.com,vm_fqdn=vm1.example.com,vm_id=112,vm_name=vm1,vm_type=lxc cpuload=0.147998116735236,disk_free=4461129728i,disk_total=5217320960i,disk_used=756191232i,disk_used_percentage=14,mem_free=1046827008i,mem_total=1073741824i,mem_used=26914816i,mem_used_percentage=2,status="running",swap_free=536698880i,swap_total=536870912i,swap_used=172032i,swap_used_percentage=0,uptime=1643793i 1595457277000000000
```
167 changes: 92 additions & 75 deletions plugins/inputs/proxmox/proxmox.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,54 @@ import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"slices"
"strings"
"time"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/inputs"
)

//go:embed sample.conf
var sampleConfig string

type Proxmox struct {
BaseURL string `toml:"base_url"`
APIToken string `toml:"api_token"`
ResponseTimeout config.Duration `toml:"response_timeout"`
NodeName string `toml:"node_name"`
AdditionalVmstatsTags []string `toml:"additional_vmstats_tags"`
Log telegraf.Logger `toml:"-"`
tls.ClientConfig

httpClient *http.Client
nodeSearchDomain string

requestFunction func(apiUrl string, method string, data url.Values) ([]byte, error)
}

func (*Proxmox) SampleConfig() string {
return sampleConfig
}

func (px *Proxmox) Init() error {
// Check parameters
for _, v := range px.AdditionalVmstatsTags {
switch v {
case "vmid", "status":
// Do nothing as those are valid values
default:
return fmt.Errorf("invalid additional vmstats tag %q", v)
}
}

// Set hostname as default node name for backwards compatibility
if px.NodeName == "" {
//nolint:errcheck // best attempt setting of NodeName
Expand All @@ -42,24 +71,25 @@ func (px *Proxmox) Init() error {
Timeout: time.Duration(px.ResponseTimeout),
}

px.requestFunction = px.performRequest

return nil
}

func (px *Proxmox) Gather(acc telegraf.Accumulator) error {
err := getNodeSearchDomain(px)
if err != nil {
if err := px.getNodeSearchDomain(); err != nil {
return err
}

gatherLxcData(px, acc)
gatherQemuData(px, acc)
px.gatherVMData(acc, lxc)
px.gatherVMData(acc, qemu)

return nil
}

func getNodeSearchDomain(px *Proxmox) error {
func (px *Proxmox) getNodeSearchDomain() error {
apiURL := "/nodes/" + px.NodeName + "/dns"
jsonData, err := px.requestFunction(px, apiURL, http.MethodGet, nil)
jsonData, err := px.requestFunction(apiURL, http.MethodGet, nil)
if err != nil {
return err
}
Expand All @@ -78,7 +108,7 @@ func getNodeSearchDomain(px *Proxmox) error {
return nil
}

func performRequest(px *Proxmox, apiURL, method string, data url.Values) ([]byte, error) {
func (px *Proxmox) performRequest(apiURL, method string, data url.Values) ([]byte, error) {
request, err := http.NewRequest(method, px.BaseURL+apiURL, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
Expand All @@ -99,24 +129,15 @@ func performRequest(px *Proxmox, apiURL, method string, data url.Values) ([]byte
return responseBody, nil
}

func gatherLxcData(px *Proxmox, acc telegraf.Accumulator) {
gatherVMData(px, acc, lxc)
}

func gatherQemuData(px *Proxmox, acc telegraf.Accumulator) {
gatherVMData(px, acc, qemu)
}

func gatherVMData(px *Proxmox, acc telegraf.Accumulator, rt resourceType) {
vmStats, err := getVMStats(px, rt)
func (px *Proxmox) gatherVMData(acc telegraf.Accumulator, rt resourceType) {
vmStats, err := px.getVMStats(rt)
if err != nil {
px.Log.Errorf("Error getting VM stats: %v", err)
return
}

// For each VM add metrics to Accumulator
for _, vmStat := range vmStats.Data {
vmConfig, err := getVMConfig(px, vmStat.ID, rt)
vmConfig, err := px.getVMConfig(vmStat.ID, rt)
if err != nil {
px.Log.Errorf("Error getting VM config: %v", err)
return
Expand All @@ -127,22 +148,64 @@ func gatherVMData(px *Proxmox, acc telegraf.Accumulator, rt resourceType) {
continue
}

tags := getTags(px, vmStat.Name, vmConfig, rt)
currentVMStatus, err := getCurrentVMStatus(px, rt, vmStat.ID)
currentVMStatus, err := px.getCurrentVMStatus(rt, vmStat.ID)
if err != nil {
px.Log.Errorf("Error getting VM current VM status: %v", err)
return
}

fields := getFields(currentVMStatus)
hostname := vmConfig.Data.Hostname
if hostname == "" {
hostname = vmStat.Name
}
domain := vmConfig.Data.Searchdomain
if domain == "" {
domain = px.nodeSearchDomain
}
fqdn := hostname + "." + domain

tags := map[string]string{
"node_fqdn": px.NodeName + "." + px.nodeSearchDomain,
"vm_name": vmStat.Name,
"vm_fqdn": fqdn,
"vm_type": string(rt),
}
if slices.Contains(px.AdditionalVmstatsTags, "vmid") {
tags["vm_id"] = vmStat.ID.String()
}
if slices.Contains(px.AdditionalVmstatsTags, "status") {
tags["status"] = currentVMStatus.Status
}

memMetrics := getByteMetrics(currentVMStatus.TotalMem, currentVMStatus.UsedMem)
swapMetrics := getByteMetrics(currentVMStatus.TotalSwap, currentVMStatus.UsedSwap)
diskMetrics := getByteMetrics(currentVMStatus.TotalDisk, currentVMStatus.UsedDisk)

fields := map[string]interface{}{
"status": currentVMStatus.Status,
"uptime": jsonNumberToInt64(currentVMStatus.Uptime),
"cpuload": jsonNumberToFloat64(currentVMStatus.CPULoad),
"mem_used": memMetrics.used,
"mem_total": memMetrics.total,
"mem_free": memMetrics.free,
"mem_used_percentage": memMetrics.usedPercentage,
"swap_used": swapMetrics.used,
"swap_total": swapMetrics.total,
"swap_free": swapMetrics.free,
"swap_used_percentage": swapMetrics.usedPercentage,
"disk_used": diskMetrics.used,
"disk_total": diskMetrics.total,
"disk_free": diskMetrics.free,
"disk_used_percentage": diskMetrics.usedPercentage,
}
acc.AddFields("proxmox", fields, tags)
}
}

func getCurrentVMStatus(px *Proxmox, rt resourceType, id json.Number) (vmStat, error) {
func (px *Proxmox) getCurrentVMStatus(rt resourceType, id json.Number) (vmStat, error) {
apiURL := "/nodes/" + px.NodeName + "/" + string(rt) + "/" + string(id) + "/status/current"

jsonData, err := px.requestFunction(px, apiURL, http.MethodGet, nil)
jsonData, err := px.requestFunction(apiURL, http.MethodGet, nil)
if err != nil {
return vmStat{}, err
}
Expand All @@ -156,9 +219,9 @@ func getCurrentVMStatus(px *Proxmox, rt resourceType, id json.Number) (vmStat, e
return currentVMStatus.Data, nil
}

func getVMStats(px *Proxmox, rt resourceType) (vmStats, error) {
func (px *Proxmox) getVMStats(rt resourceType) (vmStats, error) {
apiURL := "/nodes/" + px.NodeName + "/" + string(rt)
jsonData, err := px.requestFunction(px, apiURL, http.MethodGet, nil)
jsonData, err := px.requestFunction(apiURL, http.MethodGet, nil)
if err != nil {
return vmStats{}, err
}
Expand All @@ -172,9 +235,9 @@ func getVMStats(px *Proxmox, rt resourceType) (vmStats, error) {
return vmStatistics, nil
}

func getVMConfig(px *Proxmox, vmID json.Number, rt resourceType) (vmConfig, error) {
func (px *Proxmox) getVMConfig(vmID json.Number, rt resourceType) (vmConfig, error) {
apiURL := "/nodes/" + px.NodeName + "/" + string(rt) + "/" + string(vmID) + "/config"
jsonData, err := px.requestFunction(px, apiURL, http.MethodGet, nil)
jsonData, err := px.requestFunction(apiURL, http.MethodGet, nil)
if err != nil {
return vmConfig{}, err
}
Expand All @@ -188,30 +251,6 @@ func getVMConfig(px *Proxmox, vmID json.Number, rt resourceType) (vmConfig, erro
return vmCfg, nil
}

func getFields(vmStat vmStat) map[string]interface{} {
memMetrics := getByteMetrics(vmStat.TotalMem, vmStat.UsedMem)
swapMetrics := getByteMetrics(vmStat.TotalSwap, vmStat.UsedSwap)
diskMetrics := getByteMetrics(vmStat.TotalDisk, vmStat.UsedDisk)

return map[string]interface{}{
"status": vmStat.Status,
"uptime": jsonNumberToInt64(vmStat.Uptime),
"cpuload": jsonNumberToFloat64(vmStat.CPULoad),
"mem_used": memMetrics.used,
"mem_total": memMetrics.total,
"mem_free": memMetrics.free,
"mem_used_percentage": memMetrics.usedPercentage,
"swap_used": swapMetrics.used,
"swap_total": swapMetrics.total,
"swap_free": swapMetrics.free,
"swap_used_percentage": swapMetrics.usedPercentage,
"disk_used": diskMetrics.used,
"disk_total": diskMetrics.total,
"disk_free": diskMetrics.free,
"disk_used_percentage": diskMetrics.usedPercentage,
}
}

func getByteMetrics(total, used json.Number) metrics {
int64Total := jsonNumberToInt64(total)
int64Used := jsonNumberToInt64(used)
Expand Down Expand Up @@ -247,30 +286,8 @@ func jsonNumberToFloat64(value json.Number) float64 {
return float64Value
}

func getTags(px *Proxmox, name string, vmConfig vmConfig, rt resourceType) map[string]string {
domain := vmConfig.Data.Searchdomain
if len(domain) == 0 {
domain = px.nodeSearchDomain
}

hostname := vmConfig.Data.Hostname
if len(hostname) == 0 {
hostname = name
}
fqdn := hostname + "." + domain

return map[string]string{
"node_fqdn": px.NodeName + "." + px.nodeSearchDomain,
"vm_name": name,
"vm_fqdn": fqdn,
"vm_type": string(rt),
}
}

func init() {
inputs.Add("proxmox", func() telegraf.Input {
return &Proxmox{
requestFunction: performRequest,
}
return &Proxmox{}
})
}
Loading

0 comments on commit b613db3

Please sign in to comment.