Initial commit
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
*/bin/
|
||||
*/pkg/
|
||||
*/vendor/
|
||||
|
||||
# Go executables
|
||||
*.exe
|
||||
*.exe~
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
.atom/
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# Node modules
|
||||
/node_modules/
|
||||
|
||||
# Build outputs
|
||||
/build/
|
||||
/dist/
|
||||
/release/
|
||||
/pkg/
|
||||
*.tar.gz
|
||||
*.zip
|
||||
|
||||
# Ignore config with potential secrets
|
||||
config.yaml
|
||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM golang:1.22-alpine AS build
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN go mod download
|
||||
RUN go build main.go
|
||||
|
||||
FROM alpine:3.18
|
||||
WORKDIR /opt
|
||||
|
||||
COPY --from=build /build/main ./pve-exporter
|
||||
COPY --from=build /build/config.yaml ./config.yaml
|
||||
|
||||
RUN chmod +x ./pve-exporter
|
||||
|
||||
EXPOSE 9090
|
||||
CMD [ "./pve-exporter" ]
|
||||
24
LICENSE.txt
Normal file
24
LICENSE.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
MIT License
|
||||
-----------
|
||||
|
||||
Copyright (c) 2024 Institute of Molecular and Translational Medicine, Palacký University (https://imtm.cz/)
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
104
README.md
Normal file
104
README.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# PVE Exporter
|
||||
|
||||
Proxmox Virtual Environment Prometheus metrics exporter.
|
||||
|
||||
## Overview
|
||||
|
||||
PVE Exporter is a tool that collects metrics from a Proxmox Virtual Environment cluster and exposes them for Prometheus to scrape. This exporter supports gathering metrics for cluster state, LXC containers, QEMU virtual machines, physical disks, node storage, node status, node subscription details, and software-defined networking (SDN).
|
||||
|
||||
## Features
|
||||
|
||||
- Collects various metrics from Proxmox VE to monitor the health and performance of your virtual environment.
|
||||
- Supports multi-node clusters for high availability.
|
||||
- Securely uses Proxmox API tokens with minimal permissions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed on your machine.
|
||||
- Access to a Proxmox VE instance with API tokens configured.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Proxmox API Token
|
||||
|
||||
When generating the token, make sure to assign the 'PVEAuditor' permission to both the user and the API token. For security reasons, assign only the 'PVEAuditor' role to limit permissions appropriately.
|
||||
|
||||
```yaml
|
||||
# Proxmox API token configuration.
|
||||
token:
|
||||
tokenId: "your-token-id"
|
||||
secret: "your-secret"
|
||||
```
|
||||
|
||||
### Proxmox API Hosts
|
||||
|
||||
If you are running a multi-node cluster, add multiple API hosts to ensure high availability of metrics. Note that this configuration is not intended for gathering metrics from multiple PVE clusters. For multiple PVE clusters, deploy a separate exporter instance for each cluster.
|
||||
|
||||
```yaml
|
||||
# Proxmox API hosts.
|
||||
hosts:
|
||||
- "https://host1.example.com"
|
||||
- "https://host2.example.com"
|
||||
```
|
||||
|
||||
### Metrics Configuration
|
||||
|
||||
Configure which metrics to collect by enabling or disabling specific metric types.
|
||||
|
||||
```yaml
|
||||
# Proxmox metrics configuration.
|
||||
metrics:
|
||||
clusterState: true # Enable collection of cluster state metrics.
|
||||
ltc: true # Enable collection of LXC container metrics.
|
||||
qemu: true # Enable collection of QEMU virtual machine metrics.
|
||||
disk: true # Enable collection of physical disk metrics.
|
||||
storage: true # Enable collection of node storage metrics.
|
||||
nodeStatus: true # Enable collection of node status metrics.
|
||||
subscription: true # Enable collection of node subscription details.
|
||||
sdn: true # Enable collection of software-defined network (SDN) metrics.
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
To build the Docker image for PVE Exporter, use the following command:
|
||||
|
||||
```sh
|
||||
docker build --rm -t harbor.imtm.cz/private/pve-exporter:version .
|
||||
```
|
||||
|
||||
Replace `version` with the appropriate version tag you want to use.
|
||||
|
||||
## Run
|
||||
|
||||
To run the Docker image, use the following command:
|
||||
|
||||
```sh
|
||||
docker run --rm -d -p 9090:9090 --name pve-exporter harbor.imtm.cz/private/pve-exporter:version
|
||||
```
|
||||
|
||||
Replace `version` with the appropriate version tag you used during the build.
|
||||
|
||||
## Usage
|
||||
|
||||
Once the Docker container is running, the exporter will be available on port `9090`. You can configure Prometheus to scrape metrics from the exporter by adding a new scrape configuration to your Prometheus configuration file.
|
||||
|
||||
Example Prometheus scrape configuration:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'pve-exporter'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the LICENSE.txt file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please open an issue or submit a pull request if you have any improvements or bug fixes.
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues or have questions, please open an issue on the project's GitHub repository.
|
||||
81
application/application.go
Normal file
81
application/application.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
"lostak.dev/pve-exporter/configuration"
|
||||
"lostak.dev/pve-exporter/metrics"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// Application context.
|
||||
type Application struct {
|
||||
Configuration *configuration.Configuration // Application configuration.
|
||||
Router *mux.Router // Gorilla MUX router instance.
|
||||
Server *http.Server // HTTP server instance.
|
||||
MetricsManager *metrics.PveMetricsManager // PVE metrics manager.
|
||||
PveApiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
}
|
||||
|
||||
// Create new instance of the PVE exporter.
|
||||
func NewApplication(configPath string) *Application {
|
||||
app := Application{}
|
||||
app.Router = mux.NewRouter()
|
||||
|
||||
// Load configuration file.
|
||||
file, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
log.Panicf("Can't open config.yaml. Error: %s.", err)
|
||||
}
|
||||
|
||||
// Unmarshal configuration file.
|
||||
if err := yaml.Unmarshal(file, &app.Configuration); err != nil {
|
||||
log.Panicf("Can't parse config.yaml. Error: %s.", err)
|
||||
}
|
||||
|
||||
// Validate configuration file.
|
||||
err = app.Configuration.Validate()
|
||||
if err != nil {
|
||||
log.Fatalf("Configuration error: %s", err)
|
||||
}
|
||||
|
||||
// Set log level.
|
||||
log.SetLevel(log.AllLevels[app.Configuration.LogLevel])
|
||||
|
||||
// Create proxmox API client instance.
|
||||
pvec := app.Configuration.PVE
|
||||
app.PveApiClient = proxmox.NewPveApiClient(pvec.Hosts, pvec.Token.TokenId, pvec.Token.Secret)
|
||||
|
||||
// Create metrics collector.
|
||||
app.MetricsManager = metrics.NewPveMetricsManager(app.PveApiClient, &app.Configuration.PVE)
|
||||
|
||||
return &app
|
||||
}
|
||||
|
||||
// Initialize application internals such as HTTP server, router, PVE metrics scraper.
|
||||
func (app *Application) Start() {
|
||||
// Prepare web server.
|
||||
hostaddr := fmt.Sprintf("%s:%d", app.Configuration.Host, app.Configuration.Port)
|
||||
app.Server = &http.Server{
|
||||
Handler: app.Router,
|
||||
Addr: hostaddr,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
// Prepare metrics handler
|
||||
app.Router.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
// Start the metrics manager.
|
||||
app.MetricsManager.Start()
|
||||
|
||||
log.Infof("Proxmox exporter started on %s.", hostaddr)
|
||||
log.Fatal(app.Server.ListenAndServe())
|
||||
}
|
||||
77
configuration/configuration.go
Normal file
77
configuration/configuration.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Application configuration which is parsed from config.yaml.
|
||||
type Configuration struct {
|
||||
LogLevel int `yaml:"logLevel"` // Application log level.
|
||||
Host string `yaml:"host"` // Host address that application will bind to.
|
||||
Port uint16 `yaml:"port"` // Host port which application will listen for HTTP requests.
|
||||
PVE PveConfiguration `yaml:"proxmox"` // PVE configuration.
|
||||
}
|
||||
|
||||
// PVE token configuration.
|
||||
type PveTokenConfiguration struct {
|
||||
TokenId string `yaml:"tokenId"` // Token ID from PVE API key management.
|
||||
Secret string `yaml:"secret"` // Secret from PVE API key management.
|
||||
}
|
||||
|
||||
// MetricsConfiguration represents the metrics configuration.
|
||||
type PveMetricsConfiguration struct {
|
||||
ClusterState bool `yaml:"clusterState"` // Enables cluster state metrics collection.
|
||||
LXC bool `yaml:"lxc"` // Enables LXC container metrics collection.
|
||||
QEMU bool `yaml:"qemu"` // Enable QEMU virtual machine metrics collection.
|
||||
Disk bool `yaml:"disk"` // Enable physical disk metrics collection.
|
||||
Storage bool `yaml:"storage"` // Enable node storage metrics collection.
|
||||
NodeStatus bool `yaml:"nodeStatus"` // Enable node status metrics collection.
|
||||
Subscription bool `yaml:"subscription"` // Enable node subscription detail collection.
|
||||
SDN bool `yaml:"sdn"` // Enable node software defined network state collection.
|
||||
}
|
||||
|
||||
// PVE configuration.
|
||||
type PveConfiguration struct {
|
||||
Token PveTokenConfiguration `yaml:"token"` // PVE token.
|
||||
Hosts []string `yaml:"hosts"` // PVE hosts.
|
||||
Interval int `yaml:"interval"` // PVE scrape interval.
|
||||
Metrics PveMetricsConfiguration `yaml:"metrics"` // PVE metrics.
|
||||
}
|
||||
|
||||
// Validate configuration and check for mandaotry field.
|
||||
func (c *Configuration) Validate() error {
|
||||
// Validate host IP
|
||||
if net.ParseIP(c.Host) == nil {
|
||||
return fmt.Errorf("Host is not a valid IP address: %s", c.Host)
|
||||
}
|
||||
|
||||
// Validate PVE hosts
|
||||
if len(c.PVE.Hosts) == 0 {
|
||||
return errors.New("PVE hosts cannot be empty.")
|
||||
}
|
||||
|
||||
for _, host := range c.PVE.Hosts {
|
||||
u, err := url.Parse(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("PVE host '%s' is not valid URL. Error: %s", host, err)
|
||||
}
|
||||
|
||||
if !(u.Scheme == "http" || u.Scheme == "https") {
|
||||
return fmt.Errorf("PVE host '%s' must be protocol type of HTTP or HTTPS.", host)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate PVE token
|
||||
if c.PVE.Token.TokenId == "" {
|
||||
return errors.New("PVE tokenId cannot be empty")
|
||||
}
|
||||
|
||||
if c.PVE.Token.Secret == "" {
|
||||
return errors.New("PVE secret cannot be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
22
go.mod
Normal file
22
go.mod
Normal file
@@ -0,0 +1,22 @@
|
||||
module lostak.dev/pve-exporter
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/prometheus/client_golang v1.19.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
)
|
||||
34
go.sum
Normal file
34
go.sum
Normal file
@@ -0,0 +1,34 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
13
main.go
Normal file
13
main.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"lostak.dev/pve-exporter/application"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var configPath string
|
||||
flag.StringVar(&configPath, "config", "config.yaml", "Path to config yaml file.")
|
||||
application.NewApplication(configPath).Start()
|
||||
}
|
||||
59
metrics/pve_cluster_state_collector.go
Normal file
59
metrics/pve_cluster_state_collector.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PVE cluster state collector.
|
||||
type PveClusterStateCollector struct {
|
||||
apiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
|
||||
nodes *prometheus.GaugeVec // Count of nodes prometheus gauge.
|
||||
quorate *prometheus.GaugeVec // Cluster quorum state prometheus gauge.
|
||||
}
|
||||
|
||||
// Create new instance of PVE cluster state collector.
|
||||
func NewPveClusterStateCollector(apiClient *proxmox.PveApiClient) *PveClusterStateCollector {
|
||||
c := PveClusterStateCollector{apiClient: apiClient}
|
||||
|
||||
// Cluster meta gauge.
|
||||
c.nodes = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_cluster_nodes",
|
||||
Help: "Cluster nodes count.",
|
||||
},
|
||||
[]string{"cluster"},
|
||||
)
|
||||
|
||||
// Cluster quorate gauge.
|
||||
c.quorate = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_cluster_quorate",
|
||||
Help: "Cluster quorum state.",
|
||||
},
|
||||
[]string{"cluster"},
|
||||
)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveClusterStateCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := prometheus.Labels{"cluster": cluster.Name}
|
||||
c.nodes.With(l).Set(float64(cluster.Nodes))
|
||||
c.quorate.With(l).Set(float64(cluster.Quorate))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveClusterStateCollector) GetName() string {
|
||||
return "Cluster State"
|
||||
}
|
||||
126
metrics/pve_metrics_manager.go
Normal file
126
metrics/pve_metrics_manager.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"lostak.dev/pve-exporter/configuration"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
"lostak.dev/pve-exporter/utils"
|
||||
)
|
||||
|
||||
// PVE metrics collector interface.
|
||||
type PveMetricsCollector interface {
|
||||
CollectMetrics() error // Called by manager on metrics colletion.
|
||||
GetName() string // Used for logging purposes if error happens in collector.
|
||||
}
|
||||
|
||||
// PVE metrics manager
|
||||
type PveMetricsManager struct {
|
||||
apiClient *proxmox.PveApiClient // Proxmox virtual environment API client instance.
|
||||
collectors []PveMetricsCollector // Metrics collector instances.
|
||||
|
||||
latencySummary *prometheus.SummaryVec // Collection latency summary.
|
||||
interval int // Collection interval.
|
||||
|
||||
stop chan struct{} // Stop channel which is used in ticker.
|
||||
}
|
||||
|
||||
// Create new instance of PVE metrics collector.
|
||||
func NewPveMetricsManager(apiClient *proxmox.PveApiClient, conf *configuration.PveConfiguration) *PveMetricsManager {
|
||||
c := PveMetricsManager{apiClient: apiClient, interval: conf.Interval}
|
||||
metricsCf := conf.Metrics
|
||||
|
||||
// Cluster state metrics collector.
|
||||
if metricsCf.ClusterState {
|
||||
c.RegisterCollector(NewPveClusterStateCollector(apiClient))
|
||||
}
|
||||
|
||||
// Node state metrics collector.
|
||||
if metricsCf.NodeStatus {
|
||||
c.RegisterCollector(NewPveNodeStatusCollector(apiClient))
|
||||
}
|
||||
|
||||
// Node subscription state collector.
|
||||
if metricsCf.Subscription {
|
||||
c.RegisterCollector(NewPveSubscriptionCollector(apiClient))
|
||||
}
|
||||
|
||||
// Node disk collector.
|
||||
if metricsCf.Disk {
|
||||
c.RegisterCollector(NewPveNodeDiskCollector(apiClient))
|
||||
}
|
||||
|
||||
// Node SDN collector.
|
||||
if metricsCf.SDN {
|
||||
c.RegisterCollector(NewPveSdnCollector(apiClient))
|
||||
}
|
||||
|
||||
// Node storage collector.
|
||||
if metricsCf.Storage {
|
||||
c.RegisterCollector(NewPveStorageCollector(apiClient))
|
||||
}
|
||||
|
||||
// Node container collector.
|
||||
if metricsCf.LXC {
|
||||
c.RegisterCollector(NewPveContainerCollector(apiClient))
|
||||
}
|
||||
|
||||
// Node virtual machine collector.
|
||||
if metricsCf.QEMU {
|
||||
c.RegisterCollector(NewPveVirtualMachineCollector(apiClient))
|
||||
}
|
||||
|
||||
// Metrics collection latency summary.
|
||||
c.latencySummary = promauto.NewSummaryVec(prometheus.SummaryOpts{
|
||||
Name: "pve_metrics_collection_latency_ms",
|
||||
Help: "Summary of metrics collection latency milliseconds from PVE API.",
|
||||
}, []string{"collector"})
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// Called periodically by ticker and invokes collection on all registered collectors.
|
||||
func (c *PveMetricsManager) collectMetrics() {
|
||||
for _, collector := range c.collectors {
|
||||
start := time.Now()
|
||||
log.Tracef("Collecting %s metrics...", collector.GetName())
|
||||
err := collector.CollectMetrics()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to collect '%s' metrics. Error: %s.", collector.GetName(), err)
|
||||
} else {
|
||||
latency := time.Since(start)
|
||||
log.Tracef("Finished collecting '%s' metrics after %s.", collector.GetName(), utils.HumanDuration(latency))
|
||||
c.latencySummary.With(prometheus.Labels{"collector": collector.GetName()}).Observe(float64(latency.Milliseconds()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registers collector instance for metrics collection.
|
||||
func (c *PveMetricsManager) RegisterCollector(collector PveMetricsCollector) {
|
||||
c.collectors = append(c.collectors, collector)
|
||||
log.Infof("Metrics collector '%s' registered successfully.", collector.GetName())
|
||||
}
|
||||
|
||||
// Start metrics collector
|
||||
func (c *PveMetricsManager) Start() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(c.interval))
|
||||
c.collectMetrics()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.collectMetrics()
|
||||
case <-c.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Close stop metrics collector.
|
||||
func (c *PveMetricsManager) Stop() {
|
||||
close(c.stop)
|
||||
}
|
||||
209
metrics/pve_node_container_collector.go
Normal file
209
metrics/pve_node_container_collector.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PVE container collector.
|
||||
type PveContainerCollector struct {
|
||||
apiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
|
||||
state *prometheus.GaugeVec // Container state prometheus gauge.
|
||||
uptime *prometheus.GaugeVec // Container uptime prometheus gauge.
|
||||
|
||||
cpu *prometheus.GaugeVec // Container count of CPUs prometheus gauge.
|
||||
cpuUsage *prometheus.GaugeVec // Container CPU usage % prometheus gauge.
|
||||
|
||||
memBytes *prometheus.GaugeVec // Container memory in bytes prometheus gauge.
|
||||
memBytesUsed *prometheus.GaugeVec // Container memory usage in bytes prometheus gauge.
|
||||
|
||||
netReceive *prometheus.GaugeVec // Container network RX in bytes prometheus gauge.
|
||||
netTransmit *prometheus.GaugeVec // Container network TX in bytes prometheus gauge.
|
||||
|
||||
diskWrite *prometheus.GaugeVec // Container disk written in bytes prometheus gauge.
|
||||
diskRead *prometheus.GaugeVec // Container disk read in bytes prometheus gauge.
|
||||
|
||||
disk *prometheus.GaugeVec // Container disk space usage in bytes prometheus gauge.
|
||||
diskMax *prometheus.GaugeVec // Container disk size in bytes prometheus gauge.
|
||||
swap *prometheus.GaugeVec // Container swap usage in bytes prometheus gauge.
|
||||
}
|
||||
|
||||
// Create new instance of PVE container collector.
|
||||
func NewPveContainerCollector(apiClient *proxmox.PveApiClient) *PveContainerCollector {
|
||||
c := PveContainerCollector{apiClient: apiClient}
|
||||
|
||||
// Container state.
|
||||
c.state = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_state",
|
||||
Help: "Container state.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container uptime.
|
||||
c.uptime = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_uptime",
|
||||
Help: "Container uptime.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container CPU count.
|
||||
c.cpu = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_cpu_count",
|
||||
Help: "Container CPU count.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container CPU usage.
|
||||
c.cpuUsage = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_cpu_usage",
|
||||
Help: "Container CPU usage.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container memory total.
|
||||
c.memBytes = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_mem_total_bytes",
|
||||
Help: "Container total memory in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container memory usage.
|
||||
c.memBytesUsed = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_mem_used_bytes",
|
||||
Help: "Container used memory in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container network RX.
|
||||
c.netReceive = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_network_receive_bytes",
|
||||
Help: "Container network RX bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container network TX.
|
||||
c.netTransmit = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_network_transmit_bytes",
|
||||
Help: "Container network TX bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container disk written.
|
||||
c.diskWrite = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_disk_written_bytes",
|
||||
Help: "Container disk written bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container disk read.
|
||||
c.diskRead = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_disk_read_bytes",
|
||||
Help: "Container disk read bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container disk size.
|
||||
c.disk = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_disk_usage_bytes",
|
||||
Help: "Container disk read bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container disk size.
|
||||
c.diskMax = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_disk_size_bytes",
|
||||
Help: "Container disk size bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Container swap usage.
|
||||
c.swap = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_ct_swap_used_bytes",
|
||||
Help: "Container swap usage bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveContainerCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range cluster.NodeStatuses {
|
||||
containers, err := c.apiClient.GetNodeContainerList(node.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, container := range *containers {
|
||||
// Skip templates because they are always offline.
|
||||
if container.Template == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"vmid": container.VMID,
|
||||
"name": container.Name,
|
||||
}
|
||||
|
||||
c.state.With(labels).Set(container.GetStatusNumeric())
|
||||
c.cpu.With(labels).Set(float64(container.CPUs))
|
||||
c.memBytes.With(labels).Set(float64(container.MaxMem))
|
||||
c.diskMax.With(labels).Set(float64(container.MaxDisk))
|
||||
|
||||
// Metrics only on running container.
|
||||
if container.IsRunning() {
|
||||
c.uptime.With(labels).Set(float64(container.Uptime))
|
||||
c.cpuUsage.With(labels).Set(float64(container.CPU))
|
||||
c.memBytesUsed.With(labels).Set(float64(container.Mem))
|
||||
c.netReceive.With(labels).Set(float64(container.NetIn))
|
||||
c.netTransmit.With(labels).Set(float64(container.NetOut))
|
||||
c.diskRead.With(labels).Set(float64(container.DiskRead))
|
||||
c.diskWrite.With(labels).Set(float64(container.DiskWrite))
|
||||
c.disk.With(labels).Set(float64(container.Disk))
|
||||
c.swap.With(labels).Set(float64(container.Swap))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveContainerCollector) GetName() string {
|
||||
return "Container"
|
||||
}
|
||||
101
metrics/pve_node_disk_collector.go
Normal file
101
metrics/pve_node_disk_collector.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PVE cluster state collector.
|
||||
type PveNodeDiskCollector struct {
|
||||
apiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
|
||||
healthy *prometheus.GaugeVec // Node disk SMART passed state prometheus gauge.
|
||||
wearout *prometheus.GaugeVec // Node disk wearout % prometheus gauge.
|
||||
sizeBytes *prometheus.GaugeVec // Node disk size in bytes prometheus gauge.
|
||||
}
|
||||
|
||||
// Create new instance of PVE cluster state collector.
|
||||
func NewPveNodeDiskCollector(apiClient *proxmox.PveApiClient) *PveNodeDiskCollector {
|
||||
c := PveNodeDiskCollector{apiClient: apiClient}
|
||||
|
||||
// Node disk healthy state.
|
||||
c.healthy = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_disk_healthy",
|
||||
Help: "Node disk healthy state.",
|
||||
},
|
||||
[]string{"cluster", "node", "wwn", "type", "model", "serial", "vendor", "used", "osd_id"},
|
||||
)
|
||||
|
||||
// Node disk wearout.
|
||||
c.wearout = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_disk_wearout",
|
||||
Help: "Node disk wearout percent.",
|
||||
},
|
||||
[]string{"cluster", "node", "wwn", "type", "model", "serial", "vendor", "used", "osd_id"},
|
||||
)
|
||||
|
||||
// Node disk size in bytes.
|
||||
c.sizeBytes = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_disk_size_bytes",
|
||||
Help: "Node disk size in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "wwn", "type", "model", "serial", "vendor", "used", "osd_id"},
|
||||
)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveNodeDiskCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range cluster.NodeStatuses {
|
||||
disks, err := c.apiClient.GetNodeDisksList(node.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, disk := range *disks {
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"wwn": disk.WWN,
|
||||
"type": disk.Type,
|
||||
"model": disk.Model,
|
||||
"serial": disk.Serial,
|
||||
"vendor": strings.TrimSpace(disk.Vendor),
|
||||
"used": disk.Used,
|
||||
"osd_id": strconv.Itoa(disk.OSDID),
|
||||
}
|
||||
|
||||
// Disk healthy state.
|
||||
c.healthy.With(labels).Set(disk.GetSmartPassedState())
|
||||
|
||||
// Disk wearout %.
|
||||
wearout, ok := disk.WearOut.(float64)
|
||||
if ok {
|
||||
c.wearout.With(labels).Set(wearout)
|
||||
}
|
||||
|
||||
// Disk size in bytes.
|
||||
c.sizeBytes.With(labels).Set(float64(disk.Size))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveNodeDiskCollector) GetName() string {
|
||||
return "Node Disks"
|
||||
}
|
||||
65
metrics/pve_node_sdn_collector.go
Normal file
65
metrics/pve_node_sdn_collector.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PVE SDN state collector.
|
||||
type PveSdnCollector struct {
|
||||
apiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
|
||||
state *prometheus.GaugeVec // SDN state prometheus gauge.
|
||||
}
|
||||
|
||||
// Create new instance of PVE SDN collector.
|
||||
func NewPveSdnCollector(apiClient *proxmox.PveApiClient) *PveSdnCollector {
|
||||
c := PveSdnCollector{apiClient: apiClient}
|
||||
|
||||
// SDN Up state.
|
||||
c.state = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_sdn_state",
|
||||
Help: "Node software defined network state.",
|
||||
},
|
||||
[]string{"cluster", "node", "sdn", "sdn_id"},
|
||||
)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveSdnCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resources, err := c.apiClient.GetClusterResources()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range cluster.NodeStatuses {
|
||||
sdns := resources.FindNodeSDN(node.Name)
|
||||
if len(*sdns) > 0 {
|
||||
for _, sdn := range *sdns {
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"sdn": sdn.SDN,
|
||||
"sdn_id": sdn.ID,
|
||||
}
|
||||
|
||||
c.state.With(labels).Set(sdn.GetStatusNumeric())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveSdnCollector) GetName() string {
|
||||
return "SDN"
|
||||
}
|
||||
325
metrics/pve_node_status_collector.go
Normal file
325
metrics/pve_node_status_collector.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PVE cluster state collector.
|
||||
type PveNodeStatusCollector struct {
|
||||
apiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
|
||||
state *prometheus.GaugeVec // Node state prometheus gauge.
|
||||
uptime *prometheus.GaugeVec // Node uptime in seconds prometheus gauge.
|
||||
cpus *prometheus.GaugeVec // Node CPU count prometheus gauge.
|
||||
cpuUsage *prometheus.GaugeVec // Node CPU usage in percent prometheus gauge.
|
||||
memBytes *prometheus.GaugeVec // Node total RAM capacity in bytes prometheus gauge.
|
||||
memBytesUsed *prometheus.GaugeVec // Node RAM usage in bytes prometheus gauge.
|
||||
memBytesFree *prometheus.GaugeVec // Node RAM free in bytes prometheus gauge.
|
||||
ksmShared *prometheus.GaugeVec // Node Kernel samepage shared in bytes prometheus gauge.
|
||||
cgroupMode *prometheus.GaugeVec // Node CGroups mode prometheus gauge.
|
||||
load1 *prometheus.GaugeVec // Node load1 unix like (CPU seconds) prometheus gauge.
|
||||
load5 *prometheus.GaugeVec // Node load5 unix like (CPU seconds) prometheus gauge.
|
||||
load15 *prometheus.GaugeVec // Node load15 unix like (CPU seconds) prometheus gauge.
|
||||
fSFree *prometheus.GaugeVec // Node filesystem free space in bytes prometheus gauge.
|
||||
fSUsed *prometheus.GaugeVec // Node filesystem used space in bytes prometheus gauge.
|
||||
fSTotal *prometheus.GaugeVec // Node filesystem total space in bytes prometheus gauge.
|
||||
fSAvail *prometheus.GaugeVec // Node filesystem available capacity in bytes prometheus gauge.
|
||||
cpuInfo *prometheus.GaugeVec // Node CPU info prometheus gauge.
|
||||
systemInfo *prometheus.GaugeVec // Node system info prometheus gauge.
|
||||
time *prometheus.GaugeVec // Node time prometheus gauge.
|
||||
localTime *prometheus.GaugeVec // Node localtime prometheus gauge.
|
||||
}
|
||||
|
||||
// Create new instance of PVE cluster state collector.
|
||||
func NewPveNodeStatusCollector(apiClient *proxmox.PveApiClient) *PveNodeStatusCollector {
|
||||
c := PveNodeStatusCollector{apiClient: apiClient}
|
||||
|
||||
// Node state.
|
||||
c.state = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_state",
|
||||
Help: "Node state.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node uptime.
|
||||
c.uptime = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_uptime",
|
||||
Help: "Node uptime.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node cpu count.
|
||||
c.cpus = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_cpu_count",
|
||||
Help: "Node CPU count.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node CPU usage.
|
||||
c.cpuUsage = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_cpu_usage",
|
||||
Help: "Cluster node CPU usage %.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node memory in bytes.
|
||||
c.memBytes = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_memory_total_bytes",
|
||||
Help: "Node total memory in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Cluster node memory used in bytes.
|
||||
c.memBytesUsed = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_memory_used_bytes",
|
||||
Help: "Node used memory in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node memory free in bytes.
|
||||
c.memBytesFree = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_memory_free_bytes",
|
||||
Help: "Node free memory in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Kernel samepage shared in bytes.
|
||||
c.ksmShared = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_ksm_bytes",
|
||||
Help: "Node kernel samepage shares in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node memory cgroup mode.
|
||||
c.cgroupMode = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_cgroup_mode",
|
||||
Help: "Node cgroup mode.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node load 1.
|
||||
c.load1 = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_load1",
|
||||
Help: "Node CPU load 1 minute average.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node load 5.
|
||||
c.load5 = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_load5",
|
||||
Help: "Node CPU load 5 minutes average.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Cluster node load 15.
|
||||
c.load15 = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_load15",
|
||||
Help: "Node CPU load 15 minutes average.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node root FS free bytes.
|
||||
c.fSFree = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_rootfs_free_bytes",
|
||||
Help: "Node RootFS free bytes.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node root filesystem used bytes.
|
||||
c.fSUsed = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_rootfs_used_bytes",
|
||||
Help: "Node root filesystem used bytes.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node root filesystem total bytes.
|
||||
c.fSTotal = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_rootfs_total_bytes",
|
||||
Help: "Node root filesystem total bytes.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node root filesystem avail bytes.
|
||||
c.fSAvail = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_rootfs_avail_bytes",
|
||||
Help: "Node root filesystem avail bytes.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node CPU info.
|
||||
c.cpuInfo = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_cpuinfo",
|
||||
Help: "Node CPU info.",
|
||||
},
|
||||
[]string{"cluster", "node", "flags", "cores", "model", "sockets", "cpus", "hvm"},
|
||||
)
|
||||
|
||||
// Node system info metrics.
|
||||
c.systemInfo = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_systeminfo",
|
||||
Help: "Node system info.",
|
||||
},
|
||||
[]string{"cluster", "node", "kversion", "pveversion", "machine", "sysname", "release"},
|
||||
)
|
||||
|
||||
// Node time info.
|
||||
c.time = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_time",
|
||||
Help: "Node time.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node localtime info.
|
||||
c.localTime = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_localtime",
|
||||
Help: "Node localtime.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveNodeStatusCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range cluster.NodeStatuses {
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
}
|
||||
|
||||
time, err := c.apiClient.GetNodeTime(node.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.time.With(labels).Set(float64(time.Time))
|
||||
c.localTime.With(labels).Set(float64(time.LocalTime))
|
||||
}
|
||||
|
||||
status, err := c.apiClient.GetNodeStatusDetail(node.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.state.With(labels).Set(float64(node.Online))
|
||||
c.uptime.With(labels).Set(float64(status.Uptime))
|
||||
c.memBytes.With(labels).Set(float64(status.Memory.Total))
|
||||
c.memBytesUsed.With(labels).Set(float64(status.Memory.Used))
|
||||
c.memBytesFree.With(labels).Set(float64(status.Memory.Free))
|
||||
c.ksmShared.With(labels).Set(float64(status.Ksm.Shared))
|
||||
c.fSFree.With(labels).Set(float64(status.Rootfs.Free))
|
||||
c.fSUsed.With(labels).Set(float64(status.Rootfs.Used))
|
||||
c.fSTotal.With(labels).Set(float64(status.Rootfs.Total))
|
||||
c.fSAvail.With(labels).Set(float64(status.Rootfs.Avail))
|
||||
|
||||
// CPU load avg.
|
||||
if len(status.LoadAvg) > 0 {
|
||||
// Node load 1 metrics.
|
||||
f, err := strconv.ParseFloat(status.LoadAvg[0], 64)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to parse load1. Error: %s.", err)
|
||||
} else {
|
||||
c.load1.With(labels).Set(f)
|
||||
}
|
||||
|
||||
// Node load 5 metrics.
|
||||
f, err = strconv.ParseFloat(status.LoadAvg[1], 64)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to parse load5. Error: %s.", err)
|
||||
} else {
|
||||
c.load5.With(labels).Set(f)
|
||||
}
|
||||
|
||||
// Node load 15 metrics.
|
||||
f, err = strconv.ParseFloat(status.LoadAvg[2], 64)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to parse load15. Error: %s.", err)
|
||||
} else {
|
||||
c.load15.With(labels).Set(f)
|
||||
}
|
||||
} else {
|
||||
log.Error("CPU load stats are empty.")
|
||||
}
|
||||
|
||||
// Node CPU info.
|
||||
cpuLabels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"flags": status.CPUInfo.Flags,
|
||||
"cores": strconv.Itoa(status.CPUInfo.Cores),
|
||||
"model": status.CPUInfo.Model,
|
||||
"sockets": strconv.Itoa(status.CPUInfo.Sockets),
|
||||
"cpus": strconv.Itoa(status.CPUInfo.CPUs),
|
||||
"hvm": status.CPUInfo.HVM,
|
||||
}
|
||||
|
||||
c.cpuInfo.With(cpuLabels).Set(1)
|
||||
|
||||
// Node system info.
|
||||
sysLabels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"kversion": status.Kversion,
|
||||
"pveversion": status.PveVersion,
|
||||
"machine": status.CurrentKernel.Machine,
|
||||
"sysname": status.CurrentKernel.Sysname,
|
||||
"release": status.CurrentKernel.Release,
|
||||
}
|
||||
|
||||
c.systemInfo.With(sysLabels).Set(1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveNodeStatusCollector) GetName() string {
|
||||
return "Node State"
|
||||
}
|
||||
104
metrics/pve_node_storage_collector.go
Normal file
104
metrics/pve_node_storage_collector.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PVE Storage state collector.
|
||||
type PveStorageCollector struct {
|
||||
apiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
|
||||
state *prometheus.GaugeVec // Storage state prometheus gauge.
|
||||
total *prometheus.GaugeVec // Storage total bytes prometheus gauge.
|
||||
avail *prometheus.GaugeVec // Storage available bytes prometheus gauge.
|
||||
used *prometheus.GaugeVec // Storage used bytes prometheus gauge.
|
||||
}
|
||||
|
||||
// Create new instance of PVE SDN collector.
|
||||
func NewPveStorageCollector(apiClient *proxmox.PveApiClient) *PveStorageCollector {
|
||||
c := PveStorageCollector{apiClient: apiClient}
|
||||
|
||||
// Storage state.
|
||||
c.state = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_storage_up",
|
||||
Help: "Node storage UP state.",
|
||||
},
|
||||
[]string{"cluster", "node", "storage", "type", "content", "shared"},
|
||||
)
|
||||
|
||||
// Storage total bytes.
|
||||
c.total = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_storage_total_bytes",
|
||||
Help: "Node storage total capacity in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "storage", "type", "content", "shared"},
|
||||
)
|
||||
|
||||
// Storage available bytes.
|
||||
c.avail = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_storage_avail_bytes",
|
||||
Help: "Node storage available capacity in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "storage", "type", "content", "shared"},
|
||||
)
|
||||
|
||||
// Storage used bytes.
|
||||
c.used = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_storage_used_bytes",
|
||||
Help: "Node storage used capacity in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "storage", "type", "content", "shared"},
|
||||
)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveStorageCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range cluster.NodeStatuses {
|
||||
storages, err := c.apiClient.GetNodeStorages(node.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, storage := range *storages {
|
||||
// Skip disabled storages.
|
||||
if storage.Enabled == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"storage": storage.Storage,
|
||||
"type": storage.Type,
|
||||
"content": storage.Content,
|
||||
"shared": strconv.Itoa(storage.Shared),
|
||||
}
|
||||
|
||||
c.state.With(labels).Set(float64(storage.Active))
|
||||
c.total.With(labels).Set(float64(storage.Total))
|
||||
c.avail.With(labels).Set(float64(storage.Avail))
|
||||
c.used.With(labels).Set(float64(storage.Used))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveStorageCollector) GetName() string {
|
||||
return "Storage"
|
||||
}
|
||||
135
metrics/pve_node_subscription_collector.go
Normal file
135
metrics/pve_node_subscription_collector.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PVE subscription state collector.
|
||||
type PveSubscriptionCollector struct {
|
||||
apiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
|
||||
info *prometheus.GaugeVec // Node subscription info prometheus gauge.
|
||||
status *prometheus.GaugeVec // Node subscription status prometheus gauge.
|
||||
nextDueDate *prometheus.GaugeVec // Node subscription next due date prometheus gauge.
|
||||
regDate *prometheus.GaugeVec // Node subscription registration date prometheus gauge.
|
||||
sockets *prometheus.GaugeVec // Node subscription sockets count prometheus gauge.
|
||||
}
|
||||
|
||||
// Create new instance of PVE cluster state collector.
|
||||
func NewPveSubscriptionCollector(apiClient *proxmox.PveApiClient) *PveSubscriptionCollector {
|
||||
c := PveSubscriptionCollector{apiClient: apiClient}
|
||||
|
||||
// Node subscription info.
|
||||
c.info = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_subscription_info",
|
||||
Help: "Node subscription info.",
|
||||
},
|
||||
[]string{"cluster", "node", "productname", "serverid"},
|
||||
)
|
||||
|
||||
// Node subscription status.
|
||||
c.status = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_subscription_status",
|
||||
Help: "Node subscription status.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node subscription registration date.
|
||||
c.regDate = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_subscription_regdate",
|
||||
Help: "Node subscription registration date.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node subscription next due date.
|
||||
c.nextDueDate = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_subscription_nextduedate",
|
||||
Help: "Node subscription next due date.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
// Node subscription count of sockets.
|
||||
c.sockets = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_subscription_sockets",
|
||||
Help: "Node subscription count of sockets.",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveSubscriptionCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range cluster.NodeStatuses {
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
}
|
||||
|
||||
// Node subscription.
|
||||
subscription, err := c.apiClient.GetNodeSubscription(node.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
// Subscription info.
|
||||
subsLabels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"productname": subscription.ProductName,
|
||||
"serverid": subscription.ServerID,
|
||||
}
|
||||
|
||||
c.info.With(subsLabels).Set(1)
|
||||
|
||||
// Subscription state.
|
||||
c.status.With(labels).Set(subscription.GetActiveNumeric())
|
||||
|
||||
// Subscription sockets count.
|
||||
c.sockets.With(labels).Set(float64(subscription.Sockets))
|
||||
|
||||
// Subscription registered date.
|
||||
if subscription.Registration != "" {
|
||||
registrationTime, err := time.Parse("2006-01-02 15:04:05", subscription.Registration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.regDate.With(labels).Set(float64(registrationTime.Unix()))
|
||||
}
|
||||
|
||||
// Subscription due date.
|
||||
if subscription.NextDueDate != "" {
|
||||
nextDueTime, err := time.Parse("2006-01-02", subscription.NextDueDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.nextDueDate.With(labels).Set(float64(nextDueTime.Unix()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveSubscriptionCollector) GetName() string {
|
||||
return "Node Subscription"
|
||||
}
|
||||
311
metrics/pve_node_virtual_machine_collector.go
Normal file
311
metrics/pve_node_virtual_machine_collector.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PVE virtual machine collector.
|
||||
type PveVirtualMachineCollector struct {
|
||||
apiClient *proxmox.PveApiClient // PVE API client instance.
|
||||
|
||||
state *prometheus.GaugeVec // Virtual machine state prometheus gauge.
|
||||
uptime *prometheus.GaugeVec // Virtual machine uptime prometheus gauge.
|
||||
|
||||
cpu *prometheus.GaugeVec // Virtual machine count of CPUs prometheus gauge.
|
||||
cpuUsage *prometheus.GaugeVec // Virtual machine CPU usage % prometheus gauge.
|
||||
|
||||
memBytes *prometheus.GaugeVec // Virtual machine memory in bytes prometheus gauge.
|
||||
memBytesUsed *prometheus.GaugeVec // Virtual machine memory usage in bytes prometheus gauge.
|
||||
|
||||
disk *prometheus.GaugeVec // Virtual machine disk space usage in bytes prometheus gauge.
|
||||
diskMax *prometheus.GaugeVec // Virtual machine disk size in bytes prometheus gauge.
|
||||
swap *prometheus.GaugeVec // Virtual machine swap usage in bytes prometheus gauge.
|
||||
|
||||
netReceive *prometheus.GaugeVec // Virtual machine network receive in bytes prometheus gauge.
|
||||
netTransmit *prometheus.GaugeVec // Virtual machine network transmit in bytes prometheus gauge.
|
||||
|
||||
diskReadOps *prometheus.GaugeVec // Virtual machine disk read ops prometheus gauge.
|
||||
diskWriteOps *prometheus.GaugeVec // Virtual machine disk write ops prometheus gauge.
|
||||
|
||||
diskReadBytes *prometheus.GaugeVec // Virtual machine disk read bytes prometheus gauge.
|
||||
diskWriteBytes *prometheus.GaugeVec // Virtual machine disk write bytes prometheus gauge.
|
||||
|
||||
diskReadTimeNs *prometheus.GaugeVec // Virtual machine disk read time total prometheus gauge.
|
||||
diskWriteTimeNs *prometheus.GaugeVec // Virtual machine disk write time total prometheus gauge.
|
||||
|
||||
diskFailedReadOps *prometheus.GaugeVec // Virtual machine disk failed read ops prometheus gauge.
|
||||
diskFailedWriteOps *prometheus.GaugeVec // Virtual machine disk failed write ops prometheus gauge.
|
||||
|
||||
agent *prometheus.GaugeVec // Virtual machine agent enabled prometheus gauge.
|
||||
}
|
||||
|
||||
// Create new instance of PVE virtual machine collector.
|
||||
func NewPveVirtualMachineCollector(apiClient *proxmox.PveApiClient) *PveVirtualMachineCollector {
|
||||
c := PveVirtualMachineCollector{apiClient: apiClient}
|
||||
|
||||
// Virtual machine state.
|
||||
c.state = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_state",
|
||||
Help: "Virtual machine state.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine uptime.
|
||||
c.uptime = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_uptime",
|
||||
Help: "Virtual machine uptime.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine agent state.
|
||||
c.agent = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_agent",
|
||||
Help: "Virtual machine agent state.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine CPU count.
|
||||
c.cpu = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_cpu_count",
|
||||
Help: "Virtual machine CPU count.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine CPU usage.
|
||||
c.cpuUsage = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_cpu_usage",
|
||||
Help: "Virtual machine CPU usage.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine memory total.
|
||||
c.memBytes = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_mem_total_bytes",
|
||||
Help: "Virtual machine total memory in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine memory usage.
|
||||
c.memBytesUsed = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_mem_used_bytes",
|
||||
Help: "Virtual machine used memory in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine disk size.
|
||||
c.disk = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_usage_bytes",
|
||||
Help: "Virtual machine disk read bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine disk size.
|
||||
c.diskMax = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_size_bytes",
|
||||
Help: "Virtual machine disk size bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name"},
|
||||
)
|
||||
|
||||
// Virtual machine network receive bytes.
|
||||
c.netReceive = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_network_in_bytes",
|
||||
Help: "Virtual machine network receive in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "interface"},
|
||||
)
|
||||
|
||||
// Virtual machine network transmit bytes.
|
||||
c.netTransmit = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_network_out_bytes",
|
||||
Help: "Virtual machine network transmit in bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "interface"},
|
||||
)
|
||||
|
||||
// Virtual machine disk read ops.
|
||||
c.diskReadOps = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_rd_operations",
|
||||
Help: "Virtual machine disk read ops.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "device"},
|
||||
)
|
||||
|
||||
// Virtual machine disk write ops.
|
||||
c.diskWriteOps = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_wr_operations",
|
||||
Help: "Virtual machine disk write ops.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "device"},
|
||||
)
|
||||
|
||||
// Virtual machine disk read bytes.
|
||||
c.diskReadBytes = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_rd_bytes",
|
||||
Help: "Virtual machine disk read bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "device"},
|
||||
)
|
||||
|
||||
// Virtual machine disk write bytes.
|
||||
c.diskWriteBytes = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_wr_bytes",
|
||||
Help: "Virtual machine disk write bytes.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "device"},
|
||||
)
|
||||
|
||||
// Virtual machine failed disk read ops.
|
||||
c.diskFailedReadOps = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_failed_rd_ops",
|
||||
Help: "Virtual machine failed disk read ops.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "device"},
|
||||
)
|
||||
|
||||
// Virtual machine failed disk write ops.
|
||||
c.diskFailedWriteOps = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_failed_wr_ops",
|
||||
Help: "Virtual machine failed disk write ops.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "device"},
|
||||
)
|
||||
|
||||
// Virtual machine disk read time total nanoseconds.
|
||||
c.diskReadTimeNs = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_rd_time_total_ns",
|
||||
Help: "Virtual machine disk read time total in nanoseconds.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "device"},
|
||||
)
|
||||
|
||||
// Virtual machine disk write time total nanoseconds.
|
||||
c.diskWriteTimeNs = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_vm_disk_wr_time_total_ns",
|
||||
Help: "Virtual machine disk write time total in nanoseconds.",
|
||||
},
|
||||
[]string{"cluster", "node", "vmid", "name", "device"},
|
||||
)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveVirtualMachineCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range cluster.NodeStatuses {
|
||||
qemus, err := c.apiClient.GetNodeQemuList(node.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, qemu := range *qemus {
|
||||
// Skip templates because they are always offline.
|
||||
if qemu.Template == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"vmid": strconv.Itoa(qemu.VMID),
|
||||
"name": qemu.Name,
|
||||
}
|
||||
|
||||
c.state.With(labels).Set(qemu.GetStatusNumeric())
|
||||
c.cpu.With(labels).Set(float64(qemu.CPUs))
|
||||
c.memBytes.With(labels).Set(float64(qemu.MaxMem))
|
||||
c.diskMax.With(labels).Set(float64(qemu.MaxDisk))
|
||||
|
||||
// Metrics only on running virtual machines.
|
||||
if qemu.IsRunning() {
|
||||
c.uptime.With(labels).Set(float64(qemu.Uptime))
|
||||
c.cpuUsage.With(labels).Set(float64(qemu.CPU))
|
||||
c.memBytesUsed.With(labels).Set(float64(qemu.Mem))
|
||||
|
||||
detail, err := c.apiClient.GetNodeQemu(node.Name, strconv.Itoa(qemu.VMID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.agent.With(labels).Set(float64(detail.Agent))
|
||||
|
||||
for iface, value := range detail.Nics {
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"vmid": strconv.Itoa(qemu.VMID),
|
||||
"name": qemu.Name,
|
||||
"interface": iface,
|
||||
}
|
||||
|
||||
c.netReceive.With(labels).Set(float64(value.NetIn))
|
||||
c.netTransmit.With(labels).Set(float64(value.NetOut))
|
||||
}
|
||||
|
||||
for device, value := range detail.BlockStat {
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"vmid": strconv.Itoa(qemu.VMID),
|
||||
"name": qemu.Name,
|
||||
"device": device,
|
||||
}
|
||||
|
||||
c.diskReadOps.With(labels).Set(float64(value.RdOperations))
|
||||
c.diskWriteOps.With(labels).Set(float64(value.WrOperations))
|
||||
|
||||
c.diskReadBytes.With(labels).Set(float64(value.RdBytes))
|
||||
c.diskWriteBytes.With(labels).Set(float64(value.WrBytes))
|
||||
|
||||
c.diskFailedReadOps.With(labels).Set(float64(value.FailedRdOperations))
|
||||
c.diskFailedWriteOps.With(labels).Set(float64(value.FailedWrOperations))
|
||||
|
||||
c.diskReadTimeNs.With(labels).Set(float64(value.RdTotalTimeNs))
|
||||
c.diskWriteTimeNs.With(labels).Set(float64(value.WrTotalTimeNs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PveMetricsCollector interface implementation.
|
||||
func (c *PveVirtualMachineCollector) GetName() string {
|
||||
return "Virtual Machine"
|
||||
}
|
||||
257
proxmox/api_client.go
Normal file
257
proxmox/api_client.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Multipoint API HTTP client will make requests in round-robin fashion on all endpoints which are reachable.
|
||||
// The client will periodically check endpoints for liveness.
|
||||
type ApiClient struct {
|
||||
endpoints []*ApiEndpoint // API endpoints array.
|
||||
tokenId string // API token ID.
|
||||
secret string // API token secret.
|
||||
httpClient *http.Client // Internal HTTP client.
|
||||
checkInterval time.Duration // Check interval how often will APi client look for endpoints liveness.
|
||||
stop chan struct{} // Stop channel which is used in ticker.
|
||||
mutex sync.Mutex // Stop mutex to prevent requests making requests on unitialized API client instance.
|
||||
stopped bool // state of API client.
|
||||
index int // Round robin next index.
|
||||
aliveCount int // Count of alive endpoints.
|
||||
cache *cache.Cache // Cache instance.
|
||||
}
|
||||
|
||||
// PVE API endpoint.
|
||||
type ApiEndpoint struct {
|
||||
host string // Endpoint address.
|
||||
alive bool // Endpoint liveness state.
|
||||
}
|
||||
|
||||
// PVE API response.
|
||||
type ApiResponse struct {
|
||||
Data json.RawMessage `json:"data"` // Data message from PVE API.
|
||||
}
|
||||
|
||||
// Create new instance of API HTTP client.
|
||||
func NewApiClient(endpoints []string, tokenId string, secret string, checkInterval time.Duration) *ApiClient {
|
||||
instance := ApiClient{
|
||||
checkInterval: checkInterval,
|
||||
tokenId: tokenId,
|
||||
secret: secret,
|
||||
}
|
||||
|
||||
// Cache initialization.
|
||||
instance.cache = cache.New(30*time.Second, 5*time.Second)
|
||||
|
||||
// Prepare API endpoints.
|
||||
for _, endpoint := range endpoints {
|
||||
apiEndpoint := ApiEndpoint{
|
||||
host: endpoint,
|
||||
alive: false,
|
||||
}
|
||||
instance.endpoints = append(instance.endpoints, &apiEndpoint)
|
||||
}
|
||||
|
||||
// Configure HTTP transport.
|
||||
transport := &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
DualStack: true,
|
||||
}).Dial,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DisableCompression: false,
|
||||
MaxIdleConns: 4,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
// Create instance of HTTP client with modified HTTP transport.
|
||||
instance.httpClient = &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
return &instance
|
||||
}
|
||||
|
||||
// Check endpoint liveness state.
|
||||
func (instance *ApiClient) checkEndpointsLiveness() {
|
||||
// We want to make sure other routines won't make any requests until we have checked for alive connections.
|
||||
instance.mutex.Lock()
|
||||
defer instance.mutex.Unlock()
|
||||
|
||||
prevAliveCount := instance.aliveCount
|
||||
instance.aliveCount = 0
|
||||
|
||||
for _, endpoint := range instance.endpoints {
|
||||
endpoint.alive = false
|
||||
|
||||
url := fmt.Sprintf("%s%s", endpoint.host, "api2/json/")
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
log.Errorf("Liveness check of host %s failed. Error: %s. ", endpoint.host, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Authentication token.
|
||||
req.Header.Set("Authorization", fmt.Sprintf("PVEAPIToken=%s=%s", instance.tokenId, instance.secret))
|
||||
|
||||
resp, err := instance.httpClient.Do(req)
|
||||
if err != nil {
|
||||
log.Debugf("Liveness check of host %s failed. Error: %s. ", endpoint.host, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode == 401 {
|
||||
log.Errorf("Liveness check of host %s failed. Error: Authentication failed.", endpoint.host)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Errorf("Liveness check of host %s failed. Status code: %d. ", endpoint.host, resp.StatusCode)
|
||||
continue
|
||||
}
|
||||
|
||||
instance.aliveCount++
|
||||
endpoint.alive = true
|
||||
}
|
||||
|
||||
if prevAliveCount != instance.aliveCount {
|
||||
log.Infof("Checked Proxmox API hosts. Status: (%d/%d) hosts are UP.", instance.aliveCount, len(instance.endpoints))
|
||||
}
|
||||
}
|
||||
|
||||
// Return one alive endpoint Round-Robin.
|
||||
func (instance *ApiClient) getAliveEndpoint() (*ApiEndpoint, error) {
|
||||
var alive []*ApiEndpoint
|
||||
|
||||
// Prepare only alive endpoints.
|
||||
for _, endpoint := range instance.endpoints {
|
||||
if endpoint.alive {
|
||||
alive = append(alive, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
length := len(alive)
|
||||
|
||||
// If there are no alive endpoints then return error.
|
||||
if length == 0 {
|
||||
return nil, errors.New("All API endpoints are unreachable.")
|
||||
}
|
||||
|
||||
next := alive[instance.index%length]
|
||||
instance.index = (instance.index + 1) % length
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// Check if API client is stopped.
|
||||
func (instance *ApiClient) PerformRequest(method string, path string, params map[string]string) (*ApiResponse, error) {
|
||||
instance.mutex.Lock()
|
||||
defer instance.mutex.Unlock()
|
||||
|
||||
response, found := instance.cache.Get(method + path)
|
||||
if found {
|
||||
r := response.(ApiResponse)
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
endpoint, err := instance.getAliveEndpoint()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%sapi2/json%s", endpoint.host, path)
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Authentication token.
|
||||
req.Header.Set("Authorization", fmt.Sprintf("PVEAPIToken=%s=%s", instance.tokenId, instance.secret))
|
||||
|
||||
q := req.URL.Query()
|
||||
for key, element := range params {
|
||||
q.Add(key, element)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req.Header.Set("Connection", "Keep-Alive")
|
||||
req.Header.Set("User-Agent", "Proxmox exporter")
|
||||
|
||||
var maxAttempts int = 3
|
||||
var attempts int = 0
|
||||
var resp *http.Response
|
||||
for attempts <= maxAttempts {
|
||||
resp, err = instance.httpClient.Do(req)
|
||||
// If request is sucessful, then break the loop.
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
// In case the request failed it will be attempted again in another loop iteration after second + attempts made.
|
||||
if err != nil && attempts < maxAttempts {
|
||||
log.Warnf("Request '%s' failed (%s). Attempting again in %d seconds.", url, err.Error(), attempts+1)
|
||||
time.Sleep(time.Duration(attempts+1) * time.Second)
|
||||
continue
|
||||
}
|
||||
// If all attempts fail then return the error.
|
||||
if err != nil && attempts == maxAttempts {
|
||||
return nil, err
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Decode JSON to ApiResponse struct.
|
||||
result := ApiResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
instance.cache.Set(method+path, result, time.Second*5)
|
||||
return &result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check if API client is stopped.
|
||||
func (instance *ApiClient) IsStopped() bool {
|
||||
instance.mutex.Lock()
|
||||
defer instance.mutex.Unlock()
|
||||
return instance.stopped
|
||||
}
|
||||
|
||||
// Start the API client.
|
||||
func (instance *ApiClient) Start() {
|
||||
ticker := time.NewTicker(instance.checkInterval)
|
||||
instance.checkEndpointsLiveness()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
instance.checkEndpointsLiveness()
|
||||
case <-instance.stop:
|
||||
instance.mutex.Lock()
|
||||
defer instance.mutex.Unlock()
|
||||
instance.stopped = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Close connections and stop the API client.
|
||||
func (instance *ApiClient) Stop() {
|
||||
instance.httpClient.CloseIdleConnections()
|
||||
close(instance.stop)
|
||||
}
|
||||
494
proxmox/model.go
Normal file
494
proxmox/model.go
Normal file
@@ -0,0 +1,494 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// PveVersion represents the version information of a PVE (Proxmox Virtual Environment).
|
||||
type PveVersion struct {
|
||||
Version string `json:"version"` // PVE version number.
|
||||
RepoID string `json:"repoid"` // Repository ID of the PVE.
|
||||
Release string `json:"release"` // Release version of the PVE.
|
||||
}
|
||||
|
||||
// PveNodeStatus represents the status of a PVE node.
|
||||
type PveNodeStatus struct {
|
||||
IP string `json:"ip"` // IP address of the node.
|
||||
Name string `json:"name"` // Name of the node.
|
||||
Online uint8 `json:"online"` // Online status of the node (1 = online, 0 = offline).
|
||||
Local uint8 `json:"local"` // Local status of the node (1 = local, 0 = not local).
|
||||
NodeID int `json:"nodeid"` // Unique identifier for the node.
|
||||
Type string `json:"type"` // Type of the resource (e.g., "node").
|
||||
Level string `json:"level"` // Subscription level of the node.
|
||||
ID string `json:"id"` // Unique identifier for the node resource.
|
||||
}
|
||||
|
||||
// PveClusterStatus represents the status of a PVE cluster.
|
||||
type PveClusterStatus struct {
|
||||
Name string `json:"name"` // Name of the cluster.
|
||||
ID string `json:"id"` // Unique identifier for the cluster resource.
|
||||
Type string `json:"type"` // Type of the resource (e.g., "cluster").
|
||||
Version uint16 `json:"version"` // Version of the cluster.
|
||||
Quorate uint8 `json:"quorate"` // Quorate status of the cluster (1 = quorate, 0 = not quorate).
|
||||
Nodes uint16 `json:"nodes"` // Number of nodes in the cluster.
|
||||
NodeStatuses []PveNodeStatus `json:"-"` // Cluster nodes statuses.
|
||||
}
|
||||
|
||||
// PveResource represents a generic PVE resource object.
|
||||
type PveResource struct {
|
||||
Type string `mapstructure:"type"` // Type of resource (e.g., "lxc", "qemu", "node", "storage", "sdn").
|
||||
Node string `mapstructure:"node"` // Node where the resource is located.
|
||||
Status string `mapstructure:"status"` // Status of the resource (e.g., "running", "stopped", "online", "available").
|
||||
ID string `mapstructure:"id"` // Unique identifier for the resource.
|
||||
}
|
||||
|
||||
// PveLxcResource represents a PVE LXC container resource.
|
||||
type PveLxcResource struct {
|
||||
PveResource
|
||||
DiskRead uint64 `mapstructure:"diskread"` // Disk read bytes.
|
||||
MaxDisk uint64 `mapstructure:"maxdisk"` // Maximum disk size in bytes.
|
||||
CPU float64 `mapstructure:"cpu"` // CPU usage.
|
||||
NetOut uint64 `mapstructure:"netout"` // Network output bytes.
|
||||
Uptime uint32 `mapstructure:"uptime"` // Uptime in seconds.
|
||||
NetIn uint64 `mapstructure:"netin"` // Network input bytes.
|
||||
MaxCPU uint16 `mapstructure:"maxcpu"` // Maximum number of CPUs.
|
||||
Disk uint64 `mapstructure:"disk"` // Disk usage in bytes.
|
||||
Tags string `mapstructure:"tags"` // Tags associated with the container.
|
||||
VMID uint16 `mapstructure:"vmid"` // Virtual Machine ID.
|
||||
DiskWrite uint64 `mapstructure:"diskwrite"` // Disk write bytes.
|
||||
Name string `mapstructure:"name"` // Name of the container.
|
||||
Template uint8 `mapstructure:"template"` // Template status (0 = not a template, 1 = template).
|
||||
MaxMem uint64 `mapstructure:"maxmem"` // Maximum memory in bytes.
|
||||
ID string `mapstructure:"id"` // Unique identifier for the resource.
|
||||
}
|
||||
|
||||
// PveQemuResource represents a PVE QEMU virtual machine resource.
|
||||
type PveQemuResource struct {
|
||||
PveResource
|
||||
DiskRead uint64 `mapstructure:"diskread"` // Disk read bytes.
|
||||
MaxDisk uint64 `mapstructure:"maxdisk"` // Maximum disk size in bytes.
|
||||
NetOut uint64 `mapstructure:"netout"` // Network output bytes.
|
||||
Uptime uint32 `mapstructure:"uptime"` // Uptime in seconds.
|
||||
NetIn uint64 `mapstructure:"netin"` // Network input bytes.
|
||||
MaxCPU uint16 `mapstructure:"maxcpu"` // Maximum number of CPUs.
|
||||
Disk uint64 `mapstructure:"disk"` // Disk usage in bytes.
|
||||
Mem uint64 `mapstructure:"mem"` // Memory usage in bytes.
|
||||
Name string `mapstructure:"name"` // Name of the virtual machine.
|
||||
Template uint8 `mapstructure:"template"` // Template status (0 = not a template, 1 = template).
|
||||
MaxMem uint64 `mapstructure:"maxmem"` // Maximum memory in bytes.
|
||||
}
|
||||
|
||||
// PveNodeResource represents a PVE node resource.
|
||||
type PveNodeResource struct {
|
||||
PveResource
|
||||
MaxMem uint64 `mapstructure:"maxmem"` // Maximum memory in bytes.
|
||||
MaxCPU uint16 `mapstructure:"maxcpu"` // Maximum number of CPUs.
|
||||
Level string `mapstructure:"level"` // Subscription level.
|
||||
CPU float64 `mapstructure:"cpu"` // CPU usage.
|
||||
Uptime uint32 `mapstructure:"uptime"` // Uptime in seconds.
|
||||
Disk uint64 `mapstructure:"disk"` // Disk usage in bytes.
|
||||
Mem uint64 `mapstructure:"mem"` // Memory usage in bytes.
|
||||
CGroupMode uint8 `mapstructure:"cgroup-mode"` // Control group mode.
|
||||
}
|
||||
|
||||
// PveStorageResource represents a PVE storage resource.
|
||||
type PveStorageResource struct {
|
||||
PveResource
|
||||
Storage string `mapstructure:"storage"` // Name of the storage.
|
||||
MaxDisk uint64 `mapstructure:"maxdisk"` // Maximum disk size in bytes.
|
||||
Shared uint64 `mapstructure:"shared"` // Shared storage status (0 = not shared, 1 = shared).
|
||||
PluginType string `mapstructure:"plugintype"` // Type of storage plugin.
|
||||
Status string `mapstructure:"status"` // Status of the storage (e.g., "available").
|
||||
Content string `mapstructure:"content"` // Types of content stored (e.g., "rootdir,images").
|
||||
}
|
||||
|
||||
// PveSdnResource represents a PVE software-defined network (SDN) resource.
|
||||
type PveSdnResource struct {
|
||||
PveResource
|
||||
SDN string `mapstructure:"sdn"` // Name of the SDN.
|
||||
}
|
||||
|
||||
// PVE resources.
|
||||
type PveResources struct {
|
||||
CTs []PveLxcResource // LXC container resources.
|
||||
VMs []PveQemuResource // Virtual machine container resources.
|
||||
Nodes []PveNodeResource // Node resources.
|
||||
Storages []PveStorageResource // Storage resources.
|
||||
SDNs []PveSdnResource // Software defiend network resources.
|
||||
}
|
||||
|
||||
// PveNodeRootfs represents root filesystem stats.
|
||||
type PveNodeRootfs struct {
|
||||
Free uint64 `json:"free"` // Free space in bytes.
|
||||
Total uint64 `json:"total"` // Total space in bytes.
|
||||
Used uint64 `json:"used"` // Used space in bytes.
|
||||
Avail uint64 `json:"avail"` // Available space in bytes.
|
||||
}
|
||||
|
||||
// PveNodeMemory represents memory stats.
|
||||
type PveNodeMemory struct {
|
||||
Total uint64 `json:"total"` // Total memory in bytes.
|
||||
Free uint64 `json:"free"` // Free memory in bytes.
|
||||
Used uint64 `json:"used"` // Used memory in bytes.
|
||||
}
|
||||
|
||||
// PveNodeBootInfo represents boot information.
|
||||
type PveNodeBootInfo struct {
|
||||
Mode string `json:"mode"` // Boot mode (e.g., "efi").
|
||||
Secureboot int `json:"secureboot"` // Secure boot status (0 = disabled, 1 = enabled).
|
||||
}
|
||||
|
||||
// PveNodeKsm represents Kernel Same-page Merging (KSM) info.
|
||||
type PveNodeKsm struct {
|
||||
Shared int64 `json:"shared"` // Amount of shared memory in bytes.
|
||||
}
|
||||
|
||||
// PveNodeCurrentKernel represents current kernel info.
|
||||
type PveNodeCurrentKernel struct {
|
||||
Machine string `json:"machine"` // Machine architecture (e.g., "x86_64").
|
||||
Sysname string `json:"sysname"` // Operating system name (e.g., "Linux").
|
||||
Release string `json:"release"` // Kernel release version.
|
||||
Version string `json:"version"` // Detailed kernel version info.
|
||||
}
|
||||
|
||||
// PveNodeSwap represents swap memory stats.
|
||||
type PveNodeSwap struct {
|
||||
Used uint64 `json:"used"` // Used swap space in bytes.
|
||||
Free uint64 `json:"free"` // Free swap space in bytes.
|
||||
Total uint64 `json:"total"` // Total swap space in bytes.
|
||||
}
|
||||
|
||||
// PveNodeCPUInfo represents CPU information.
|
||||
type PveNodeCPUInfo struct {
|
||||
Flags string `json:"flags"` // CPU flags.
|
||||
Cores int `json:"cores"` // Number of CPU cores.
|
||||
MHz string `json:"mhz"` // CPU frequency in MHz.
|
||||
Model string `json:"model"` // CPU model.
|
||||
Sockets int `json:"sockets"` // Number of CPU sockets.
|
||||
UserHZ float64 `json:"user_hz"` // User HZ value.
|
||||
CPUs int `json:"cpus"` // Number of logical CPUs.
|
||||
HVM string `json:"hvm"` // Hardware virtualization support.
|
||||
}
|
||||
|
||||
// PveNodeStatusDetail represents detailed status of a PVE node.
|
||||
type PveNodeStatusDetail struct {
|
||||
Rootfs PveNodeRootfs `json:"rootfs"` // Root filesystem stats.
|
||||
Wait float64 `json:"wait"` // CPU wait time.
|
||||
Memory PveNodeMemory `json:"memory"` // Memory stats.
|
||||
BootInfo PveNodeBootInfo `json:"boot-info"` // Boot information.
|
||||
Ksm PveNodeKsm `json:"ksm"` // Kernel Same-page Merging (KSM) info.
|
||||
Kversion string `json:"kversion"` // Kernel version.
|
||||
LoadAvg []string `json:"loadavg,string"` // Load average values.
|
||||
CPUInfo PveNodeCPUInfo `json:"cpuinfo"` // CPU information.
|
||||
PveVersion string `json:"pveversion"` // Proxmox VE version.
|
||||
Uptime uint64 `json:"uptime"` // System uptime in seconds.
|
||||
Idle uint64 `json:"idle"` // Idle time in seconds.
|
||||
CurrentKernel PveNodeCurrentKernel `json:"current-kernel"` // Current kernel information.
|
||||
CPU float64 `json:"cpu"` // CPU usage.
|
||||
Swap PveNodeSwap `json:"swap"` // Swap memory stats.
|
||||
}
|
||||
|
||||
// PveSubscription represents PVE node subscription info.
|
||||
type PveSubscription struct {
|
||||
NextDueDate string `json:"nextduedate"` // Next due date for subscription (format: YYYY-MM-DD).
|
||||
ProductName string `json:"productname"` // Name of product associated with subscription.
|
||||
ServerID string `json:"serverid"` // Unique identifier of server.
|
||||
Registration string `json:"regdate"` // Registration date of subscription (format: YYYY-MM-DD HH:MM:SS).
|
||||
Sockets uint8 `json:"sockets"` // Number of CPU sockets covered by subscription.
|
||||
CheckTime uint `json:"checktime"` // Unix timestamp of last check time.
|
||||
URL string `json:"url"` // URL with more information about subscription.
|
||||
Level string `json:"level"` // Subscription level (e.g., "c" for community).
|
||||
Key string `json:"key"` // Subscription key.
|
||||
Status string `json:"status"` // Status of subscription (e.g., "active").
|
||||
}
|
||||
|
||||
// PVE node disk.
|
||||
type PveDisk struct {
|
||||
WWN string `json:"wwn"` // WWN (World Wide Name) of the drive.
|
||||
Type string `json:"type"` // Type of drive (e.g., ssd, hdd).
|
||||
Model string `json:"model"` // Model of the drive.
|
||||
Serial string `json:"serial"` // Serial number of the drive.
|
||||
Size int64 `json:"size"` // Size of the drive in bytes.
|
||||
Vendor string `json:"vendor"` // Vendor of the drive.
|
||||
OSDID int `json:"osdid"` // OSD (Object Storage Device) ID of the drive.
|
||||
DevPath string `json:"devpath"` // Device path of the drive.
|
||||
OSDIDList []int `json:"osdid-list"` // List of OSD IDs associated with the drive.
|
||||
ByIDLink string `json:"by_id_link"` // Symbolic link to the drive in /dev/disk/by-id directory.
|
||||
GPT int `json:"gpt"` // Indicates whether the drive uses GPT partitioning (1 for true, 0 for false).
|
||||
Health string `json:"health"` // Health status of the drive (e.g., PASSED, FAILED, OK).
|
||||
WearOut interface{} `json:"wearout,omitempty"` // Wear out percentage of the drive.
|
||||
Used string `json:"used,omitempty"` // How the drive is used (optional field).
|
||||
}
|
||||
|
||||
// PVE node time.
|
||||
type PveNodeTime struct {
|
||||
Time uint64 `json:"time"` // Unix timestamp in UTC.
|
||||
LocalTime uint64 `json:"localtime"` // Unix timestamp in local time.
|
||||
Timezone string `json:"timezone"` // Timezone information.
|
||||
}
|
||||
|
||||
// PVE storage information.
|
||||
type PveStorage struct {
|
||||
Shared int `json:"shared"` // Indicates if the storage is shared between multiple nodes.
|
||||
Enabled int `json:"enabled"` // Indicates if the storage is enabled.
|
||||
Storage string `json:"storage"` // Name of the storage.
|
||||
Total int64 `json:"total"` // Total storage capacity in bytes.
|
||||
Content string `json:"content"` // Type of content stored (e.g., backup, ISO, vztmpl).
|
||||
Avail int64 `json:"avail"` // Available storage capacity in bytes.
|
||||
Active int `json:"active"` // Indicates if the storage is active.
|
||||
Used int64 `json:"used"` // Used storage capacity in bytes.
|
||||
UsedFraction float64 `json:"used_fraction"` // Fraction of used storage.
|
||||
Type string `json:"type"` // Type of storage (e.g., pbs, zfspool, rbd, lvm).
|
||||
}
|
||||
|
||||
// PveLXCStatus represents the status information of a PVE LXC container.
|
||||
type PveLxcStatus struct {
|
||||
Status string `json:"status"` // Status of the LXC container (e.g., "running", "stopped").
|
||||
DiskRead uint64 `json:"diskread"` // Amount of disk read by the LXC container.
|
||||
DiskWrite uint64 `json:"diskwrite"` // Amount of disk write by the LXC container.
|
||||
VMID string `json:"vmid"` // Virtual Machine ID of the LXC container.
|
||||
MaxSwap uint64 `json:"maxswap"` // Maximum swap space allocated for the LXC container.
|
||||
PID uint32 `json:"pid"` // Process ID of the LXC container.
|
||||
Disk uint64 `json:"disk"` // Disk usage of the LXC container.
|
||||
Type string `json:"type"` // Type of the container (e.g., "lxc").
|
||||
MaxMem uint64 `json:"maxmem"` // Maximum memory allocated for the LXC container.
|
||||
Name string `json:"name"` // Name of the LXC container.
|
||||
NetIn uint64 `json:"netin"` // Network incoming traffic of the LXC container.
|
||||
CPU float64 `json:"cpu"` // CPU usage of the LXC container.
|
||||
Uptime uint32 `json:"uptime"` // Uptime of the LXC container in seconds.
|
||||
NetOut uint64 `json:"netout"` // Network outgoing traffic of the LXC container.
|
||||
Ha PveHaStatus `json:"ha"` // High Availability configuration for the LXC container.
|
||||
CPUs int `json:"cpus"` // Number of CPUs allocated for the LXC container.
|
||||
Swap uint64 `json:"swap"` // Swap usage of the LXC container.
|
||||
Mem uint64 `json:"mem"` // Memory usage of the LXC container.
|
||||
MaxDisk uint64 `json:"maxdisk"` // Maximum disk space allocated for the LXC container.
|
||||
Template int `json:"template"` // Template status (0 = not a template, 1 = template).
|
||||
}
|
||||
|
||||
// PveQemuStatus represents the status of a Proxmox virtual machine.
|
||||
type PveQemuStatus struct {
|
||||
BalloonMin uint64 `json:"balloon_min"` // Minimum balloon memory in bytes.
|
||||
Pid int `json:"pid"` // Process ID.
|
||||
Disk uint64 `json:"disk"` // Disk usage in bytes.
|
||||
Status string `json:"status"` // Status of the virtual machine (e.g., "running").
|
||||
DiskRead uint64 `json:"diskread"` // Disk read bytes.
|
||||
Tags string `json:"tags"` // Tags for the container.
|
||||
VMID int `json:"vmid"` // VM ID.
|
||||
DiskWrite uint64 `json:"diskwrite"` // Disk write bytes.
|
||||
CPUs int `json:"cpus"` // Number of CPUs.
|
||||
Shares int `json:"shares"` // CPU shares.
|
||||
MaxDisk uint64 `json:"maxdisk"` // Maximum disk size in bytes.
|
||||
Mem uint64 `json:"mem"` // Memory usage in bytes.
|
||||
Uptime int `json:"uptime"` // Uptime in seconds.
|
||||
NetOut uint64 `json:"netout"` // Network out bytes.
|
||||
Name string `json:"name"` // Name of the virtual machine.
|
||||
NetIn uint64 `json:"netin"` // Network in bytes.
|
||||
CPU float64 `json:"cpu"` // CPU usage.
|
||||
MaxMem uint64 `json:"maxmem"` // Maximum memory in bytes.
|
||||
Template int `json:"template"` // Template status (0 = not a template, 1 = template).
|
||||
}
|
||||
|
||||
// Ha represents the High Availability configuration for a PVE resource.
|
||||
type PveHaStatus struct {
|
||||
Managed int `json:"managed"` // Whether HA is managed for the resource.
|
||||
}
|
||||
|
||||
// PveBalloonInfo represents the balloon memory information.
|
||||
type PveBalloonInfo struct {
|
||||
LastUpdate int64 `json:"last_update"` // Last update time in Unix timestamp.
|
||||
MemSwappedIn uint64 `json:"mem_swapped_in"` // Memory swapped in, in bytes.
|
||||
TotalMem uint64 `json:"total_mem"` // Total memory in bytes.
|
||||
FreeMem uint64 `json:"free_mem"` // Free memory in bytes.
|
||||
MemSwappedOut uint64 `json:"mem_swapped_out"` // Memory swapped out, in bytes.
|
||||
MajorPageFaults uint64 `json:"major_page_faults"` // Number of major page faults.
|
||||
Actual uint64 `json:"actual"` // Actual memory in bytes.
|
||||
MaxMem uint64 `json:"max_mem"` // Maximum memory in bytes.
|
||||
MinorPageFaults uint64 `json:"minor_page_faults"` // Number of minor page faults.
|
||||
}
|
||||
|
||||
// PveNic represents network interface controller statistics.
|
||||
type PveNic struct {
|
||||
NetOut uint64 `json:"netout"` // Network out bytes.
|
||||
NetIn uint64 `json:"netin"` // Network in bytes.
|
||||
}
|
||||
|
||||
// PveBlockStat represents block device statistics.
|
||||
type PveBlockStat struct {
|
||||
ZoneAppendMerged uint64 `json:"zone_append_merged"` // Number of zone append merged operations.
|
||||
RdTotalTimeNs uint64 `json:"rd_total_time_ns"` // Total read time in nanoseconds.
|
||||
UnmapTotalTimeNs uint64 `json:"unmap_total_time_ns"` // Total unmap time in nanoseconds.
|
||||
RdMerged uint64 `json:"rd_merged"` // Number of read merged operations.
|
||||
TimedStats []interface{} `json:"timed_stats"` // Timed statistics (not detailed in sample).
|
||||
FlushOperations uint64 `json:"flush_operations"` // Number of flush operations.
|
||||
ZoneAppendOperations uint64 `json:"zone_append_operations"` // Number of zone append operations.
|
||||
RdOperations uint64 `json:"rd_operations"` // Number of read operations.
|
||||
FailedWrOperations uint64 `json:"failed_wr_operations"` // Number of failed write operations.
|
||||
FailedUnmapOperations uint64 `json:"failed_unmap_operations"` // Number of failed unmap operations.
|
||||
AccountInvalid bool `json:"account_invalid"` // Indicates if the account is invalid.
|
||||
WrTotalTimeNs uint64 `json:"wr_total_time_ns"` // Total write time in nanoseconds.
|
||||
InvalidUnmapOperations uint64 `json:"invalid_unmap_operations"` // Number of invalid unmap operations.
|
||||
WrMerged uint64 `json:"wr_merged"` // Number of write merged operations.
|
||||
AccountFailed bool `json:"account_failed"` // Indicates if the account failed.
|
||||
InvalidZoneAppendOperations uint64 `json:"invalid_zone_append_operations"` // Number of invalid zone append operations.
|
||||
WrHighestOffset uint64 `json:"wr_highest_offset"` // Highest write offset.
|
||||
WrOperations uint64 `json:"wr_operations"` // Number of write operations.
|
||||
UnmapMerged uint64 `json:"unmap_merged"` // Number of unmap merged operations.
|
||||
InvalidFlushOperations uint64 `json:"invalid_flush_operations"` // Number of invalid flush operations.
|
||||
WrBytes uint64 `json:"wr_bytes"` // Write bytes.
|
||||
UnmapBytes uint64 `json:"unmap_bytes"` // Unmap bytes.
|
||||
InvalidRdOperations uint64 `json:"invalid_rd_operations"` // Number of invalid read operations.
|
||||
ZoneAppendTotalTimeNs uint64 `json:"zone_append_total_time_ns"` // Total zone append time in nanoseconds.
|
||||
IdleTimeNs uint64 `json:"idle_time_ns"` // Idle time in nanoseconds.
|
||||
FailedZoneAppendOperations uint64 `json:"failed_zone_append_operations"` // Number of failed zone append operations.
|
||||
UnmapOperations uint64 `json:"unmap_operations"` // Number of unmap operations.
|
||||
FailedFlushOperations uint64 `json:"failed_flush_operations"` // Number of failed flush operations.
|
||||
ZoneAppendBytes uint64 `json:"zone_append_bytes"` // Zone append bytes.
|
||||
RdBytes uint64 `json:"rd_bytes"` // Read bytes.
|
||||
FailedRdOperations uint64 `json:"failed_rd_operations"` // Number of failed read operations.
|
||||
FlushTotalTimeNs uint64 `json:"flush_total_time_ns"` // Total flush time in nanoseconds.
|
||||
InvalidWrOperations uint64 `json:"invalid_wr_operations"` // Number of invalid write operations.
|
||||
}
|
||||
|
||||
// PveProxmoxSupport represents Proxmox support features and versions.
|
||||
type PveProxmoxSupport struct {
|
||||
BackupMaxWorkers bool `json:"backup-max-workers"` // Indicates if backup max workers is supported.
|
||||
PbsMasterkey bool `json:"pbs-masterkey"` // Indicates if PBS master key is supported.
|
||||
PbsDirtyBitmapSavevm bool `json:"pbs-dirty-bitmap-savevm"` // Indicates if PBS dirty bitmap save VM is supported.
|
||||
PbsDirtyBitmapMigration bool `json:"pbs-dirty-bitmap-migration"` // Indicates if PBS dirty bitmap migration is supported.
|
||||
PbsDirtyBitmap bool `json:"pbs-dirty-bitmap"` // Indicates if PBS dirty bitmap is supported.
|
||||
QueryBitmapInfo bool `json:"query-bitmap-info"` // Indicates if querying bitmap info is supported.
|
||||
PbsLibraryVersion string `json:"pbs-library-version"` // PBS library version.
|
||||
}
|
||||
|
||||
// PveQemuStatusDetail represents the detailed status of a Proxmox QEMU virtual machine.
|
||||
type PveQemuStatusDetail struct {
|
||||
VMID int `json:"vmid"` // VM ID.
|
||||
DiskWrite uint64 `json:"diskwrite"` // Disk write bytes.
|
||||
Status string `json:"status"` // Status of the VM (e.g., "running").
|
||||
RunningQemu string `json:"running-qemu"` // Running QEMU version.
|
||||
RunningMachine string `json:"running-machine"` // Running machine type.
|
||||
ProxmoxSupport PveProxmoxSupport `json:"proxmox-support"` // Proxmox support features and versions.
|
||||
Clipboard interface{} `json:"clipboard"` // Clipboard status (could be null).
|
||||
BalloonMin uint64 `json:"balloon_min"` // Minimum balloon memory in bytes.
|
||||
Agent int `json:"agent"` // Agent status.
|
||||
Name string `json:"name"` // Name of the VM.
|
||||
NetIn uint64 `json:"netin"` // Network in bytes.
|
||||
Ha PveHaStatus `json:"ha"` // High availability configuration.
|
||||
BalloonInfo PveBalloonInfo `json:"ballooninfo"` // Balloon memory information.
|
||||
MaxDisk uint64 `json:"maxdisk"` // Maximum disk size in bytes.
|
||||
Tags string `json:"tags"` // Tags for the VM.
|
||||
DiskRead uint64 `json:"diskread"` // Disk read bytes.
|
||||
Nics map[string]PveNic `json:"nics"` // Network interface controllers statistics.
|
||||
BlockStat map[string]PveBlockStat `json:"blockstat"` // Block device statistics.
|
||||
QmpStatus string `json:"qmpstatus"` // QMP status of the VM.
|
||||
Disk uint64 `json:"disk"` // Disk usage in bytes.
|
||||
Pid int `json:"pid"` // Process ID.
|
||||
Balloon uint64 `json:"balloon"` // Balloon memory in bytes.
|
||||
MaxMem uint64 `json:"maxmem"` // Maximum memory in bytes.
|
||||
CPU float64 `json:"cpu"` // CPU usage.
|
||||
NetOut uint64 `json:"netout"` // Network out bytes.
|
||||
Uptime int `json:"uptime"` // Uptime in seconds.
|
||||
FreeMem uint64 `json:"freemem"` // Free memory in bytes.
|
||||
Shares int `json:"shares"` // CPU shares.
|
||||
CPUs int `json:"cpus"` // Number of CPUs.
|
||||
Mem uint64 `json:"mem"` // Memory usage in bytes.
|
||||
Template int `json:"template"` // Template status (0 = not a template, 1 = template).
|
||||
}
|
||||
|
||||
// FindNode searches for a node by its name in the PVE resources.
|
||||
// Returns a pointer to the PveNodeResource if found, otherwise returns an error.
|
||||
func (r *PveResources) FindNode(nodeName string) (*PveNodeResource, error) {
|
||||
for i := range r.Nodes {
|
||||
if r.Nodes[i].Node == nodeName {
|
||||
return &r.Nodes[i], nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("Unable to find node '" + nodeName + "' in node resources.")
|
||||
}
|
||||
|
||||
// FindNodeSDN searches for SDN resources associated with a specific node name in the PVE resources.
|
||||
// Returns a slice of PveSdnResource pointers containing all matching SDN resources.
|
||||
func (r *PveResources) FindNodeSDN(nodeName string) *[]PveSdnResource {
|
||||
var sdns []PveSdnResource
|
||||
for _, sdn := range r.SDNs {
|
||||
if sdn.Node == nodeName {
|
||||
sdns = append(sdns, sdn)
|
||||
}
|
||||
}
|
||||
return &sdns
|
||||
}
|
||||
|
||||
// GetStatusNumeric returns the numeric status of an SDN resource.
|
||||
// Returns 1 if the status is "ok", otherwise returns 0.
|
||||
func (r *PveSdnResource) GetStatusNumeric() float64 {
|
||||
if r.Status == "ok" {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetActiveNumeric returns the numeric state of a subscription.
|
||||
// Returns 1 if the subscription status is "active", otherwise returns 0.
|
||||
func (r *PveSubscription) GetActiveNumeric() float64 {
|
||||
if r.Status == "active" {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetSmartPassedState returns the numeric health state of a disk.
|
||||
// Returns 1 if the disk health status is "OK" or "PASSED", otherwise returns 0.
|
||||
func (r *PveDisk) GetSmartPassedState() float64 {
|
||||
switch r.Health {
|
||||
case "OK", "PASSED":
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// GetStatusNumeric returns a numeric representation of the LXC container's status.
|
||||
// If the container status is "running", it returns 1. Otherwise, it returns 0.
|
||||
func (r *PveLxcStatus) GetStatusNumeric() float64 {
|
||||
if r.Status == "running" {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetStatusNumeric returns a numeric representation of the virtual machine status.
|
||||
// If the virtual machine status is "running", it returns 1. Otherwise, it returns 0.
|
||||
func (r *PveQemuStatus) GetStatusNumeric() float64 {
|
||||
if r.Status == "running" {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// IsRunning checks if LXC container is running.
|
||||
// If the LXC container status is "running", it returns true. Otherwise, it returns false.
|
||||
func (r *PveLxcStatus) IsRunning() bool {
|
||||
if r.Status == "running" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsRunning checks if virtual machine is running.
|
||||
// If the virtual machine status is "running", it returns true. Otherwise, it returns false.
|
||||
func (r *PveQemuStatus) IsRunning() bool {
|
||||
if r.Status == "running" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetClusterName returns name of cluster for labeling purposes.
|
||||
// If there is no cluster then return standalone node with node name. Otherwise, it returns cluster name.
|
||||
func (r *PveClusterStatus) GetClusterName() string {
|
||||
if r.Name == "" {
|
||||
return fmt.Sprintf("Standalone node - %s", r.NodeStatuses[0].Name)
|
||||
}
|
||||
return r.Name
|
||||
}
|
||||
303
proxmox/pve_api_client.go
Normal file
303
proxmox/pve_api_client.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// PVE API client.
|
||||
type PveApiClient struct {
|
||||
apiClient *ApiClient // Instance of API client.
|
||||
}
|
||||
|
||||
// Create new instance of PVE API client.
|
||||
func NewPveApiClient(endpoints []string, tokenId string, secret string) *PveApiClient {
|
||||
client := PveApiClient{}
|
||||
client.apiClient = NewApiClient(endpoints, tokenId, secret, 5*time.Second)
|
||||
client.apiClient.Start()
|
||||
return &client
|
||||
}
|
||||
|
||||
// Get PVE version.
|
||||
func (instance *PveApiClient) GetVersion() (*PveVersion, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/version", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version := PveVersion{}
|
||||
err = json.Unmarshal(res.Data, &version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &version, nil
|
||||
}
|
||||
|
||||
// Get PVE cluster status.
|
||||
func (instance *PveApiClient) GetClusterStatus() (*PveClusterStatus, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/cluster/status", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var objects []map[string]interface{}
|
||||
err = json.Unmarshal(res.Data, &objects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If data returned is empty array then there is possible issue with permissions.
|
||||
if len(objects) == 0 {
|
||||
return nil, errors.New("Unable to fetch cluster status from API. Please make sure you have valid token and 'PVEAuditor' permission on token and user.")
|
||||
}
|
||||
|
||||
var cluster PveClusterStatus
|
||||
|
||||
for _, obj := range objects {
|
||||
switch obj["type"] {
|
||||
case "cluster":
|
||||
if err := mapstructure.Decode(obj, &cluster); err != nil {
|
||||
log.Errorf("Unable to decode cluster status object. Error:", err)
|
||||
continue
|
||||
}
|
||||
case "node":
|
||||
var node PveNodeStatus
|
||||
if err := mapstructure.Decode(obj, &node); err != nil {
|
||||
log.Errorf("Unable to decode node status object. Error:", err)
|
||||
continue
|
||||
}
|
||||
cluster.NodeStatuses = append(cluster.NodeStatuses, node)
|
||||
default:
|
||||
log.Errorf("Unable to decode cluster status object. Unknown type:", obj["type"])
|
||||
}
|
||||
}
|
||||
|
||||
return &cluster, nil
|
||||
}
|
||||
|
||||
// Get PVE cluster resources.
|
||||
func (instance *PveApiClient) GetClusterResources() (*PveResources, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/cluster/resources", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var objects []map[string]interface{}
|
||||
err = json.Unmarshal(res.Data, &objects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If data returned is empty array then there is possible issue with permissions.
|
||||
if len(objects) == 0 {
|
||||
return nil, errors.New("Unable to fetch cluster status from API. Please make sure you have valid token and 'PVEAuditor' permission on token and user.")
|
||||
}
|
||||
|
||||
resources := PveResources{}
|
||||
for _, obj := range objects {
|
||||
switch obj["type"] {
|
||||
case "lxc":
|
||||
var resource PveLxcResource
|
||||
if err := mapstructure.Decode(obj, &resource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
if err := mapstructure.Decode(obj, &resource.PveResource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
resources.CTs = append(resources.CTs, resource)
|
||||
case "qemu":
|
||||
var resource PveQemuResource
|
||||
if err := mapstructure.Decode(obj, &resource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
if err := mapstructure.Decode(obj, &resource.PveResource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
resources.VMs = append(resources.VMs, resource)
|
||||
case "node":
|
||||
var resource PveNodeResource
|
||||
if err := mapstructure.Decode(obj, &resource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
if err := mapstructure.Decode(obj, &resource.PveResource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
resources.Nodes = append(resources.Nodes, resource)
|
||||
case "storage":
|
||||
var resource PveStorageResource
|
||||
if err := mapstructure.Decode(obj, &resource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
if err := mapstructure.Decode(obj, &resource.PveResource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
resources.Storages = append(resources.Storages, resource)
|
||||
case "sdn":
|
||||
var resource PveSdnResource
|
||||
if err := mapstructure.Decode(obj, &resource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
if err := mapstructure.Decode(obj, &resource.PveResource); err != nil {
|
||||
log.Errorf("Unable to decode cluster resource object. Error:", err)
|
||||
continue
|
||||
}
|
||||
resources.SDNs = append(resources.SDNs, resource)
|
||||
default:
|
||||
log.Errorf("Unable to decode cluster resource object. Unknown type:", obj["type"])
|
||||
}
|
||||
}
|
||||
|
||||
return &resources, nil
|
||||
}
|
||||
|
||||
// Get PVE cluster node stats.
|
||||
func (instance *PveApiClient) GetNodeStatusDetail(node string) (*PveNodeStatusDetail, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/nodes/"+node+"/status", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var status PveNodeStatusDetail
|
||||
err = json.Unmarshal(res.Data, &status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// Get PVE cluster node subscription info.
|
||||
func (instance *PveApiClient) GetNodeSubscription(node string) (*PveSubscription, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/nodes/"+node+"/subscription", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var subscription PveSubscription
|
||||
err = json.Unmarshal(res.Data, &subscription)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &subscription, nil
|
||||
}
|
||||
|
||||
// Get PVE cluster node disks info.
|
||||
func (instance *PveApiClient) GetNodeDisksList(node string) (*[]PveDisk, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/nodes/"+node+"/disks/list", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var disks []PveDisk
|
||||
err = json.Unmarshal(res.Data, &disks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &disks, nil
|
||||
}
|
||||
|
||||
// Get PVE node time info.
|
||||
func (instance *PveApiClient) GetNodeTime(node string) (*PveNodeTime, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/nodes/"+node+"/time", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var time PveNodeTime
|
||||
err = json.Unmarshal(res.Data, &time)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &time, nil
|
||||
}
|
||||
|
||||
// Get PVE node storage time info.
|
||||
func (instance *PveApiClient) GetNodeStorages(node string) (*[]PveStorage, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/nodes/"+node+"/storage", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var storages []PveStorage
|
||||
err = json.Unmarshal(res.Data, &storages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &storages, nil
|
||||
}
|
||||
|
||||
// Get PVE node LXC containers.
|
||||
func (instance *PveApiClient) GetNodeContainerList(node string) (*[]PveLxcStatus, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", fmt.Sprintf("/nodes/%s/lxc/", node), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var status []PveLxcStatus
|
||||
err = json.Unmarshal(res.Data, &status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// Get PVE LXC container status.
|
||||
func (instance *PveApiClient) GetContainerStatus(node string, vmid int) (*PveLxcStatus, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", fmt.Sprintf("/nodes/%s/lxc/%d/status/current", node, vmid), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var status PveLxcStatus
|
||||
err = json.Unmarshal(res.Data, &status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// Get PVE node VM list.
|
||||
func (instance *PveApiClient) GetNodeQemuList(node string) (*[]PveQemuStatus, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", fmt.Sprintf("/nodes/%s/qemu/", node), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var status []PveQemuStatus
|
||||
err = json.Unmarshal(res.Data, &status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// Get PVE node VM detail.
|
||||
func (instance *PveApiClient) GetNodeQemu(node string, vmid string) (*PveQemuStatusDetail, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", fmt.Sprintf("/nodes/%s/qemu/%s/status/current", node, vmid), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var status PveQemuStatusDetail
|
||||
err = json.Unmarshal(res.Data, &status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
24
utils/time_utils.go
Normal file
24
utils/time_utils.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HumanDuration converts a time.Duration into a human-readable format.
|
||||
// It returns the duration formatted as seconds (with two decimal places) if the duration is one second or more,
|
||||
// milliseconds if the duration is one millisecond or more,
|
||||
// microseconds if the duration is one microsecond or more,
|
||||
// otherwise nanoseconds.
|
||||
func HumanDuration(duration time.Duration) string {
|
||||
switch {
|
||||
case duration.Seconds() >= 1:
|
||||
return fmt.Sprintf("%.2fs", duration.Seconds())
|
||||
case duration.Milliseconds() >= 1:
|
||||
return fmt.Sprintf("%dms", duration.Milliseconds())
|
||||
case duration.Microseconds() >= 1:
|
||||
return fmt.Sprintf("%dμs", duration.Microseconds())
|
||||
default:
|
||||
return fmt.Sprintf("%dns", duration.Nanoseconds())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user