294 lines
7.3 KiB
Go
294 lines
7.3 KiB
Go
|
//go:generate ../../../tools/readme_config_includer/generator
|
||
|
package proxmox
|
||
|
|
||
|
import (
|
||
|
_ "embed"
|
||
|
"encoding/json"
|
||
|
"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
|
||
|
hostname, _ := os.Hostname()
|
||
|
px.NodeName = hostname
|
||
|
}
|
||
|
|
||
|
tlsCfg, err := px.ClientConfig.TLSConfig()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
px.httpClient = &http.Client{
|
||
|
Transport: &http.Transport{
|
||
|
TLSClientConfig: tlsCfg,
|
||
|
},
|
||
|
Timeout: time.Duration(px.ResponseTimeout),
|
||
|
}
|
||
|
|
||
|
px.requestFunction = px.performRequest
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (px *Proxmox) Gather(acc telegraf.Accumulator) error {
|
||
|
if err := px.getNodeSearchDomain(); err != nil {
|
||
|
return fmt.Errorf("getting search domain failed: %w", err)
|
||
|
}
|
||
|
|
||
|
px.gatherVMData(acc, lxc)
|
||
|
px.gatherVMData(acc, qemu)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (px *Proxmox) getNodeSearchDomain() error {
|
||
|
apiURL := "/nodes/" + px.NodeName + "/dns"
|
||
|
jsonData, err := px.requestFunction(apiURL, http.MethodGet, nil)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("requesting data failed: %w", err)
|
||
|
}
|
||
|
|
||
|
var nodeDNS nodeDNS
|
||
|
if err := json.Unmarshal(jsonData, &nodeDNS); err != nil {
|
||
|
return fmt.Errorf("decoding message failed: %w", err)
|
||
|
}
|
||
|
px.nodeSearchDomain = nodeDNS.Data.Searchdomain
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
request.Header.Add("Authorization", "PVEAPIToken="+px.APIToken)
|
||
|
|
||
|
resp, err := px.httpClient.Do(request)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
responseBody, err := io.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return responseBody, nil
|
||
|
}
|
||
|
|
||
|
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 _, vmStat := range vmStats.Data {
|
||
|
vmConfig, err := px.getVMConfig(vmStat.ID, rt)
|
||
|
if err != nil {
|
||
|
px.Log.Errorf("Error getting VM config: %v", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if vmConfig.Data.Template == 1 {
|
||
|
px.Log.Debugf("Ignoring template VM %s (%s)", vmStat.ID, vmStat.Name)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
currentVMStatus, err := px.getCurrentVMStatus(rt, vmStat.ID)
|
||
|
if err != nil {
|
||
|
px.Log.Errorf("Error getting VM current VM status: %v", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
vmFQDN := vmConfig.Data.Hostname
|
||
|
if vmFQDN == "" {
|
||
|
vmFQDN = vmStat.Name
|
||
|
}
|
||
|
domain := vmConfig.Data.Searchdomain
|
||
|
if domain == "" {
|
||
|
domain = px.nodeSearchDomain
|
||
|
}
|
||
|
if domain != "" {
|
||
|
vmFQDN += "." + domain
|
||
|
}
|
||
|
|
||
|
nodeFQDN := px.NodeName
|
||
|
if px.nodeSearchDomain != "" {
|
||
|
nodeFQDN += "." + domain
|
||
|
}
|
||
|
|
||
|
tags := map[string]string{
|
||
|
"node_fqdn": nodeFQDN,
|
||
|
"vm_name": vmStat.Name,
|
||
|
"vm_fqdn": vmFQDN,
|
||
|
"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 (px *Proxmox) getCurrentVMStatus(rt resourceType, id json.Number) (vmStat, error) {
|
||
|
apiURL := "/nodes/" + px.NodeName + "/" + string(rt) + "/" + string(id) + "/status/current"
|
||
|
jsonData, err := px.requestFunction(apiURL, http.MethodGet, nil)
|
||
|
if err != nil {
|
||
|
return vmStat{}, err
|
||
|
}
|
||
|
|
||
|
var currentVMStatus vmCurrentStats
|
||
|
err = json.Unmarshal(jsonData, ¤tVMStatus)
|
||
|
if err != nil {
|
||
|
return vmStat{}, err
|
||
|
}
|
||
|
|
||
|
return currentVMStatus.Data, nil
|
||
|
}
|
||
|
|
||
|
func (px *Proxmox) getVMStats(rt resourceType) (vmStats, error) {
|
||
|
apiURL := "/nodes/" + px.NodeName + "/" + string(rt)
|
||
|
jsonData, err := px.requestFunction(apiURL, http.MethodGet, nil)
|
||
|
if err != nil {
|
||
|
return vmStats{}, err
|
||
|
}
|
||
|
|
||
|
var vmStatistics vmStats
|
||
|
err = json.Unmarshal(jsonData, &vmStatistics)
|
||
|
if err != nil {
|
||
|
return vmStats{}, err
|
||
|
}
|
||
|
|
||
|
return vmStatistics, nil
|
||
|
}
|
||
|
|
||
|
func (px *Proxmox) getVMConfig(vmID json.Number, rt resourceType) (vmConfig, error) {
|
||
|
apiURL := "/nodes/" + px.NodeName + "/" + string(rt) + "/" + string(vmID) + "/config"
|
||
|
jsonData, err := px.requestFunction(apiURL, http.MethodGet, nil)
|
||
|
if err != nil {
|
||
|
return vmConfig{}, err
|
||
|
}
|
||
|
|
||
|
var vmCfg vmConfig
|
||
|
err = json.Unmarshal(jsonData, &vmCfg)
|
||
|
if err != nil {
|
||
|
return vmConfig{}, err
|
||
|
}
|
||
|
|
||
|
return vmCfg, nil
|
||
|
}
|
||
|
|
||
|
func getByteMetrics(total, used json.Number) metrics {
|
||
|
int64Total := jsonNumberToInt64(total)
|
||
|
int64Used := jsonNumberToInt64(used)
|
||
|
int64Free := int64Total - int64Used
|
||
|
usedPercentage := 0.0
|
||
|
if int64Total != 0 {
|
||
|
usedPercentage = float64(int64Used) * 100 / float64(int64Total)
|
||
|
}
|
||
|
|
||
|
return metrics{
|
||
|
total: int64Total,
|
||
|
used: int64Used,
|
||
|
free: int64Free,
|
||
|
usedPercentage: usedPercentage,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func jsonNumberToInt64(value json.Number) int64 {
|
||
|
int64Value, err := value.Int64()
|
||
|
if err != nil {
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
return int64Value
|
||
|
}
|
||
|
|
||
|
func jsonNumberToFloat64(value json.Number) float64 {
|
||
|
float64Value, err := value.Float64()
|
||
|
if err != nil {
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
return float64Value
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
inputs.Add("proxmox", func() telegraf.Input {
|
||
|
return &Proxmox{}
|
||
|
})
|
||
|
}
|