Skip to content

Commit

Permalink
Add start of proxmox API package
Browse files Browse the repository at this point in the history
  • Loading branch information
Starttoaster authored Mar 7, 2024
1 parent bfb0d06 commit c137bd5
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 6 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/starttoaster/proxmox-exporter
go 1.21

require (
github.com/google/go-querystring v1.1.0
github.com/gorilla/mux v1.8.1
github.com/luthermonson/go-proxmox v0.0.0-beta3
github.com/patrickmn/go-cache v2.1.0+incompatible
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
Expand Down Expand Up @@ -99,6 +102,7 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
Expand Down
54 changes: 48 additions & 6 deletions internal/prometheus/prometheus.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,72 @@
package prometheus

import (
"github.com/luthermonson/go-proxmox"
"github.com/prometheus/client_golang/prometheus"
"github.com/starttoaster/proxmox-exporter/internal/logger"
wrappedProxmox "github.com/starttoaster/proxmox-exporter/internal/proxmox"
)

// Collector contains all prometheus metric Descs
type Collector struct {
nodeUp *prometheus.Desc
up *prometheus.Desc
}

// NewCollector constructor function for Collector
func NewCollector() *Collector {
return &Collector{
nodeUp: prometheus.NewDesc(fqAddPrefix("node_up"),
"Shows whether nodes in a proxmox cluster are up.",
[]string{}, nil,
up: prometheus.NewDesc(fqAddPrefix("up"),
"Shows whether nodes and vms in a proxmox cluster are up. (0=down,1=up)",
[]string{"type", "name"},
nil,
),
}
}

// Describe contains all the prometheus descriptors for this metric collector
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.nodeUp
ch <- c.up
}

// Collect instructs the prometheus client how to collect the metrics for each descriptor
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(c.nodeUp, prometheus.GaugeValue, 1.0)
// Retrieve node statuses for the cluster
nodeStatuses, err := wrappedProxmox.Nodes()
if err != nil {
logger.Logger.Error(err.Error())
return
}

// Retrieve node info for each node from statuses
var nodes []*proxmox.Node
for _, nodeStatus := range nodeStatuses {
// Add node up metric
ch <- prometheus.MustNewConstMetric(c.up, prometheus.GaugeValue, float64(nodeStatus.Online), "node", nodeStatus.Name)

// Get node from node status's name
node, err := wrappedProxmox.Node(nodeStatus.Name)
if err != nil {
logger.Logger.Error(err.Error())
return
}
nodes = append(nodes, node)
}

for _, node := range nodes {
// Get VMs for node
vms, err := wrappedProxmox.VirtualMachinesOnNode(node)
if err != nil {
logger.Logger.Error(err.Error())
return
}

for _, vm := range vms {
// Add vm up metric
var vmUp float64 = 0.0
if vm.IsRunning() {
vmUp = 1.0
}
ch <- prometheus.MustNewConstMetric(c.up, prometheus.GaugeValue, vmUp, "qemu", vm.Name)
}
}
}
25 changes: 25 additions & 0 deletions internal/proxmox/vms.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,28 @@ func VirtualMachinesAllNodes() (proxmox.VirtualMachines, error) {

return vms, nil
}

// VirtualMachinesOnNode returns the virtual machines for a node
func VirtualMachinesOnNode(node *proxmox.Node) (proxmox.VirtualMachines, error) {
// Chech cache
var vms proxmox.VirtualMachines
if x, found := cash.Get(fmt.Sprintf("VirtualMachinesOnNode_%s", node.Name)); found {
var ok bool
vms, ok = x.(proxmox.VirtualMachines)
if ok {
log.Logger.Debug("proxmox request was found in cache for VirtualMachinesOnNode", "node", node.Name)
return vms, nil
}
}

// Get VMs on the node for this iteration
vms, err := node.VirtualMachines(context.Background())
if err != nil {
return nil, fmt.Errorf("encountered error making request to /nodes/%s/qemu: \n%v", node.Name, err)
}

// Update per-node cache since we have it
cash.Set(fmt.Sprintf("VirtualMachinesOnNode_%s", node.Name), vms, cache.DefaultExpiration)

return vms, nil
}
110 changes: 110 additions & 0 deletions pkg/proxmox/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package proxmox

import (
"fmt"
"net/http"
"net/url"
"strings"
)

const (
defaultBaseURL = "https://localhost:8006/"
apiPath = "api2/json/"
)

// Client for the Proxmox API
type Client struct {
// HTTP retryable client for the API
client *http.Client

// Base URL for API requests. Defaults to https://localhost:8006/,
// but can be changed to any remote endpoint.
baseURL *url.URL

// tokenID is the identifier given for a Proxmox API token
tokenID string

// token is the token secret
token string

// Services for each resource in the Proxmox API
Nodes *NodeService
}

// NewClient returns a new Proxmox API client
func NewClient(tokenID string, token string, options ...ClientOptionFunc) (*Client, error) {
if token == "" || tokenID == "" {
return nil, fmt.Errorf("can not create Proxmox API client without a token ID and token")
}

c := &Client{
tokenID: tokenID,
token: token,
}

// Set the client default fields
c.setBaseURL(defaultBaseURL)
c.setHttpClient(&http.Client{})

// Apply any given options
for _, fn := range options {
if fn == nil {
continue
}
if err := fn(c); err != nil {
return nil, err
}
}

// Create all the Proxmox API services
c.Nodes = &NodeService{client: c}

return c, nil
}

// ClientOptionFunc can be used to customize a new Proxmox API client
type ClientOptionFunc func(*Client) error

// WithBaseURL sets the URL for API requests to something other than localhost.
// API path is applied automatically if unspecified.
// Default: "https://localhost:8006/"
func WithBaseURL(urlStr string) ClientOptionFunc {
return func(c *Client) error {
return c.setBaseURL(urlStr)
}
}

// setBaseURL sets the URL for API requests
func (c *Client) setBaseURL(urlStr string) error {
// Make sure the given URL end with a slash
if !strings.HasSuffix(urlStr, "/") {
urlStr += "/"
}

baseURL, err := url.Parse(urlStr)
if err != nil {
return err
}

if !strings.HasSuffix(baseURL.Path, apiPath) {
baseURL.Path += apiPath
}

// Update the base URL of the client
c.baseURL = baseURL

return nil
}

// WithHttpClient sets the HTTP client for API requests to something other than the default Go http Client
func WithHttpClient(client *http.Client) ClientOptionFunc {
return func(c *Client) error {
return c.setHttpClient(client)
}
}

// setHttpClient sets the HTTP client for API requests
func (c *Client) setHttpClient(client *http.Client) error {
c.client = client
return nil
}
47 changes: 47 additions & 0 deletions pkg/proxmox/nodes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package proxmox

import (
"net/http"
)

// NodeService is the service that encapsulates node API methods
type NodeService struct {
client *Client
}

// Nodes is a list of Node types
type Nodes []Node

// Node contains data attributes for a node in the Proxmox nodes API
type Node struct {
CPU float64 `json:"cpu"`
Disk int64 `json:"disk"`
ID string `json:"id"`
Level string `json:"level"`
Maxcpu int `json:"maxcpu"`
Maxdisk int64 `json:"maxdisk"`
Maxmem int64 `json:"maxmem"`
Mem int64 `json:"mem"`
Node string `json:"node"`
SslFingerprint string `json:"ssl_fingerprint"`
Status string `json:"status"`
Type string `json:"type"`
Uptime int `json:"uptime"`
}

// Get makes a GET request to the /nodes endpoint
func (s *NodeService) Get() (*Nodes, *http.Response, error) {
u := "nodes"
req, err := s.client.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, nil, err
}

d := new(Nodes)
resp, err := s.client.Do(req, &d)
if err != nil {
return nil, resp, err
}

return d, resp, nil
}
86 changes: 86 additions & 0 deletions pkg/proxmox/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package proxmox

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"

"github.com/google/go-querystring/query"
)

// NewRequest creates a new request.
// Method should be a valid http request method.
// Path should be an API path relative to the client's base URL.
// Path should not have a preceding '/'
// If specified, the value pointed to by opt is encoded into the query string of the URL.
func (c *Client) NewRequest(method, path string, opt interface{}) (*http.Request, error) {
u := *c.baseURL
unescaped, err := url.PathUnescape(path)
if err != nil {
return nil, err
}

// Set the encoded path data
u.RawPath = c.baseURL.Path + path
u.Path = c.baseURL.Path + unescaped

// Set query parameters if any are provided
if opt != nil {
q, err := query.Values(opt)
if err != nil {
return nil, err
}
u.RawQuery = q.Encode()
}

// Create request
req, err := http.NewRequest(method, u.String(), nil)
if err != nil {
return nil, err
}

// Set request header if making a POST or PUT
if req.Method == http.MethodPost || req.Method == http.MethodPut {
req.Header.Set("Content-Type", "x-www-form-urlencoded")
}

return req, nil
}

// Do sends an API request. The response is stored in the value 'v' or returned as an error.
// If v implements the io.Writer interface, the raw response body will be written to v, without json decoding it.
func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
// Set auth header according to https://pve.proxmox.com/wiki/Proxmox_VE_API#Authentication
req.Header.Set("Authorization", fmt.Sprintf("PVEAPIToken=%s=%s", c.tokenID, c.token))

// Do request
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
defer io.Copy(io.Discard, resp.Body)

// Check for error API response and capture it as an error
if resp.StatusCode > 399 {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading Proxmox response body: %v", err)
}

return resp, fmt.Errorf(string(body))
}

// Copy body into v
if v != nil {
if w, ok := v.(io.Writer); ok {
_, err = io.Copy(w, resp.Body)
} else {
err = json.NewDecoder(resp.Body).Decode(v)
}
}

return resp, err
}

0 comments on commit c137bd5

Please sign in to comment.