//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{} }) }