Merge d2bed6b658
into 3b79c8e09f
This commit is contained in:
commit
e88a9fdba6
3 changed files with 347 additions and 18 deletions
|
@ -4,12 +4,14 @@ import (
|
|||
"context"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/glanceapp/glance/pkg/proxmox"
|
||||
"github.com/glanceapp/glance/pkg/sysinfo"
|
||||
)
|
||||
|
||||
|
@ -20,6 +22,44 @@ type serverStatsWidget struct {
|
|||
Servers []serverStatsRequest `yaml:"servers"`
|
||||
}
|
||||
|
||||
type serverStats struct {
|
||||
HostInfoIsAvailable bool `json:"host_info_is_available"`
|
||||
BootTime time.Time `json:"boot_time"`
|
||||
Hostname string `json:"hostname"`
|
||||
Platform string `json:"platform"`
|
||||
|
||||
CPU struct {
|
||||
LoadIsAvailable bool `json:"load_is_available"`
|
||||
Load1Percent uint8 `json:"load1_percent"`
|
||||
Load15Percent uint8 `json:"load15_percent"`
|
||||
|
||||
TemperatureIsAvailable bool `json:"temperature_is_available"`
|
||||
TemperatureC uint8 `json:"temperature_c"`
|
||||
} `json:"cpu"`
|
||||
|
||||
Memory struct {
|
||||
IsAvailable bool `json:"memory_is_available"`
|
||||
TotalMB uint64 `json:"total_mb"`
|
||||
UsedMB uint64 `json:"used_mb"`
|
||||
UsedPercent uint8 `json:"used_percent"`
|
||||
|
||||
SwapIsAvailable bool `json:"swap_is_available"`
|
||||
SwapTotalMB uint64 `json:"swap_total_mb"`
|
||||
SwapUsedMB uint64 `json:"swap_used_mb"`
|
||||
SwapUsedPercent uint8 `json:"swap_used_percent"`
|
||||
} `json:"memory"`
|
||||
|
||||
Mountpoints []serverStorageInfo `json:"mountpoints"`
|
||||
}
|
||||
|
||||
type serverStorageInfo struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
TotalMB uint64 `json:"total_mb"`
|
||||
UsedMB uint64 `json:"used_mb"`
|
||||
UsedPercent uint8 `json:"used_percent"`
|
||||
}
|
||||
|
||||
func (widget *serverStatsWidget) initialize() error {
|
||||
widget.withTitle("Server Stats").withCacheDuration(15 * time.Second)
|
||||
widget.widgetBase.WIP = true
|
||||
|
@ -56,21 +96,70 @@ func (widget *serverStatsWidget) update(context.Context) {
|
|||
}
|
||||
|
||||
serv.IsReachable = true
|
||||
serv.Info = info
|
||||
serv.Info = &serverStats{
|
||||
HostInfoIsAvailable: info.HostInfoIsAvailable,
|
||||
BootTime: info.BootTime.Time,
|
||||
Hostname: info.Hostname,
|
||||
Platform: info.Platform,
|
||||
CPU: info.CPU,
|
||||
Memory: info.Memory,
|
||||
}
|
||||
|
||||
for _, mountPoint := range info.Mountpoints {
|
||||
serv.Info.Mountpoints = append(serv.Info.Mountpoints, serverStorageInfo{
|
||||
Path: mountPoint.Path,
|
||||
Name: mountPoint.Name,
|
||||
TotalMB: mountPoint.TotalMB,
|
||||
UsedMB: mountPoint.UsedMB,
|
||||
UsedPercent: mountPoint.UsedPercent,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
info, err := fetchRemoteServerInfo(serv)
|
||||
if err != nil {
|
||||
slog.Warn("Getting remote system info: " + err.Error())
|
||||
serv.IsReachable = false
|
||||
serv.Info = &sysinfo.SystemInfo{
|
||||
Hostname: "Unnamed server #" + strconv.Itoa(i+1),
|
||||
|
||||
if serv.Type == "proxmox" {
|
||||
info, err := fetchProxmoxServerInfo(serv)
|
||||
if err != nil {
|
||||
slog.Warn("Getting remote system info: " + err.Error())
|
||||
serv.IsReachable = false
|
||||
serv.Info = &serverStats{
|
||||
Hostname: "Unnamed server #" + strconv.Itoa(i+1),
|
||||
}
|
||||
} else {
|
||||
serv.IsReachable = true
|
||||
serv.Info = info
|
||||
}
|
||||
} else {
|
||||
serv.IsReachable = true
|
||||
serv.Info = info
|
||||
info, err := fetchRemoteServerInfo(serv)
|
||||
if err != nil {
|
||||
slog.Warn("Getting remote system info: " + err.Error())
|
||||
serv.IsReachable = false
|
||||
serv.Info = &serverStats{
|
||||
Hostname: "Unnamed server #" + strconv.Itoa(i+1),
|
||||
}
|
||||
} else {
|
||||
serv.IsReachable = true
|
||||
serv.Info = &serverStats{
|
||||
HostInfoIsAvailable: info.HostInfoIsAvailable,
|
||||
BootTime: info.BootTime.Time,
|
||||
Hostname: info.Hostname,
|
||||
Platform: info.Platform,
|
||||
CPU: info.CPU,
|
||||
Memory: info.Memory,
|
||||
}
|
||||
|
||||
for _, mountPoint := range info.Mountpoints {
|
||||
serv.Info.Mountpoints = append(serv.Info.Mountpoints, serverStorageInfo{
|
||||
Path: mountPoint.Path,
|
||||
Name: mountPoint.Name,
|
||||
TotalMB: mountPoint.TotalMB,
|
||||
UsedMB: mountPoint.UsedMB,
|
||||
UsedPercent: mountPoint.UsedPercent,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -86,15 +175,17 @@ func (widget *serverStatsWidget) Render() template.HTML {
|
|||
|
||||
type serverStatsRequest struct {
|
||||
*sysinfo.SystemInfoRequest `yaml:",inline"`
|
||||
Info *sysinfo.SystemInfo `yaml:"-"`
|
||||
IsReachable bool `yaml:"-"`
|
||||
StatusText string `yaml:"-"`
|
||||
Name string `yaml:"name"`
|
||||
HideSwap bool `yaml:"hide-swap"`
|
||||
Type string `yaml:"type"`
|
||||
URL string `yaml:"url"`
|
||||
Token string `yaml:"token"`
|
||||
Timeout durationField `yaml:"timeout"`
|
||||
Info *serverStats `yaml:"-"`
|
||||
IsReachable bool `yaml:"-"`
|
||||
StatusText string `yaml:"-"`
|
||||
Name string `yaml:"name"`
|
||||
HideSwap bool `yaml:"hide-swap"`
|
||||
Type string `yaml:"type"`
|
||||
URL string `yaml:"url"`
|
||||
Token string `yaml:"token"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Timeout durationField `yaml:"timeout"`
|
||||
// Support for other agents
|
||||
// Provider string `yaml:"provider"`
|
||||
}
|
||||
|
@ -115,3 +206,81 @@ func fetchRemoteServerInfo(infoReq *serverStatsRequest) (*sysinfo.SystemInfo, er
|
|||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func fetchProxmoxServerInfo(infoReq *serverStatsRequest) (*serverStats, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(infoReq.Timeout))
|
||||
defer cancel()
|
||||
|
||||
cli := proxmox.New(infoReq.URL, infoReq.Username, infoReq.Token, infoReq.Password)
|
||||
resources, err := cli.GetClusterResources(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: Add support for multiple nodes
|
||||
for _, node := range resources {
|
||||
if node.Type != "node" {
|
||||
continue
|
||||
}
|
||||
|
||||
status, err := cli.GetNodeStatus(ctx, node.Node)
|
||||
if err != nil {
|
||||
// TODO: Log me!
|
||||
continue
|
||||
}
|
||||
|
||||
var info serverStats
|
||||
info.Platform = status.PveVersion
|
||||
info.BootTime = time.Unix(time.Now().Unix()-node.Uptime, 0)
|
||||
info.HostInfoIsAvailable = true
|
||||
|
||||
if len(status.LoadAvg) == 3 {
|
||||
info.CPU.LoadIsAvailable = true
|
||||
|
||||
load1, _ := strconv.ParseFloat(status.LoadAvg[0], 64)
|
||||
info.CPU.Load1Percent = uint8(math.Min(load1*100/float64(status.CpuInfo.Cores), 100))
|
||||
|
||||
load15, _ := strconv.ParseFloat(status.LoadAvg[2], 64)
|
||||
info.CPU.Load15Percent = uint8(math.Min(load15*100/float64(status.CpuInfo.Cores), 100))
|
||||
}
|
||||
|
||||
info.Memory.IsAvailable = true
|
||||
info.Memory.TotalMB = status.Memory.Total / 1024 / 1024
|
||||
info.Memory.UsedMB = status.Memory.Used / 1024 / 1024
|
||||
|
||||
if info.Memory.TotalMB > 0 {
|
||||
info.Memory.UsedPercent = uint8(math.Min(float64(info.Memory.UsedMB)/float64(info.Memory.TotalMB)*100, 100))
|
||||
}
|
||||
|
||||
info.Memory.SwapIsAvailable = true
|
||||
info.Memory.SwapTotalMB = status.Swap.Total / 1024 / 1024
|
||||
info.Memory.SwapUsedMB = status.Swap.Used / 1024 / 1024
|
||||
|
||||
if info.Memory.SwapTotalMB > 0 {
|
||||
info.Memory.SwapUsedPercent = uint8(math.Min(float64(info.Memory.SwapUsedMB)/float64(info.Memory.SwapTotalMB)*100, 100))
|
||||
}
|
||||
|
||||
for _, storage := range resources {
|
||||
if storage.Type != "storage" || storage.Node != node.Node {
|
||||
continue
|
||||
}
|
||||
|
||||
storageInfo := serverStorageInfo{
|
||||
Path: storage.ID,
|
||||
Name: storage.Storage,
|
||||
TotalMB: storage.MaxDisk / 1024 / 1024,
|
||||
UsedMB: storage.Disk / 1024 / 1024,
|
||||
}
|
||||
|
||||
if storageInfo.TotalMB > 0 {
|
||||
storageInfo.UsedPercent = uint8(math.Min(float64(storageInfo.UsedMB)/float64(storageInfo.TotalMB)*100, 100))
|
||||
}
|
||||
|
||||
info.Mountpoints = append(info.Mountpoints, storageInfo)
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
83
pkg/proxmox/proxmox.go
Normal file
83
pkg/proxmox/proxmox.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Proxmox struct {
|
||||
URL string
|
||||
Username string
|
||||
TokenID string
|
||||
Secret string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func New(URL string, userName string, tokenID string, secret string) *Proxmox {
|
||||
return &Proxmox{
|
||||
URL: URL,
|
||||
Username: userName,
|
||||
TokenID: tokenID,
|
||||
Secret: secret,
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxmox) setAuthorizationHeader(req *http.Request) {
|
||||
req.Header.Set("Authorization", "PVEAPIToken="+p.Username+"@pam!"+p.TokenID+"="+p.Secret)
|
||||
}
|
||||
|
||||
func (p *Proxmox) GetClusterResources(ctx context.Context) ([]ClusterResource, error) {
|
||||
client := &http.Client{
|
||||
Timeout: p.Timeout,
|
||||
}
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, "GET", p.URL+"/api2/json/cluster/resources", nil)
|
||||
p.setAuthorizationHeader(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending request to cluster resources: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("non-200 cluster resources response status: %s", response.Status)
|
||||
}
|
||||
|
||||
var result multipleResponse[ClusterResource]
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decoding cluster resources response: %w", err)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
func (p *Proxmox) GetNodeStatus(ctx context.Context, node string) (*NodeStatus, error) {
|
||||
client := &http.Client{
|
||||
Timeout: p.Timeout,
|
||||
}
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, "GET", p.URL+"/api2/json/nodes/"+node+"/status", nil)
|
||||
p.setAuthorizationHeader(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending request to node status: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("non-200 node status response status: %s", response.Status)
|
||||
}
|
||||
|
||||
var result singleResponse[NodeStatus]
|
||||
if err = json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decoding node status response: %w", err)
|
||||
}
|
||||
|
||||
return &result.Data, nil
|
||||
}
|
77
pkg/proxmox/types.go
Normal file
77
pkg/proxmox/types.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package proxmox
|
||||
|
||||
type singleResponse[T any] struct {
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
type multipleResponse[T any] struct {
|
||||
Data []T `json:"data"`
|
||||
}
|
||||
|
||||
type ClusterResource struct {
|
||||
NetOut int64 `json:"netout"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
NetIn int64 `json:"netin"`
|
||||
MaxCPU int `json:"maxcpu"`
|
||||
DiskWrite int64 `json:"diskwrite"`
|
||||
Template int `json:"template"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
Mem int64 `json:"mem"`
|
||||
DiskRead int64 `json:"diskread"`
|
||||
MaxMem int64 `json:"maxmem"`
|
||||
MaxDisk uint64 `json:"maxdisk"`
|
||||
ID string `json:"id"`
|
||||
VMID int `json:"vmid"`
|
||||
Type string `json:"type"`
|
||||
Node string `json:"node"`
|
||||
Disk uint64 `json:"disk"`
|
||||
Level string `json:"level"`
|
||||
CgroupMode int `json:"cgroup-mode"`
|
||||
PluginType string `json:"plugintype"`
|
||||
Content string `json:"content"`
|
||||
Shared int `json:"shared"`
|
||||
Storage string `json:"storage"`
|
||||
SDN string `json:"sdn"`
|
||||
}
|
||||
|
||||
type NodeStatus struct {
|
||||
PveVersion string `json:"pveversion"`
|
||||
Kversion string `json:"kversion"`
|
||||
Wait float64 `json:"wait"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
LoadAvg []string `json:"loadavg"`
|
||||
Cpu float64 `json:"cpu"`
|
||||
Idle int `json:"idle"`
|
||||
|
||||
Swap struct {
|
||||
Free uint64 `json:"free"`
|
||||
Used uint64 `json:"used"`
|
||||
Total uint64 `json:"total"`
|
||||
} `json:"swap"`
|
||||
|
||||
CpuInfo struct {
|
||||
Model string `json:"model"`
|
||||
Cpus int `json:"cpus"`
|
||||
Hvm string `json:"hvm"`
|
||||
UserHz int `json:"user_hz"`
|
||||
Flags string `json:"flags"`
|
||||
Cores int `json:"cores"`
|
||||
Sockets int `json:"sockets"`
|
||||
Mhz string `json:"mhz"`
|
||||
} `json:"cpuinfo"`
|
||||
|
||||
Memory struct {
|
||||
Free uint64 `json:"free"`
|
||||
Used uint64 `json:"used"`
|
||||
Total uint64 `json:"total"`
|
||||
} `json:"memory"`
|
||||
|
||||
RootFs struct {
|
||||
Available int64 `json:"avail"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Free int64 `json:"free"`
|
||||
} `json:"rootfs"`
|
||||
}
|
Loading…
Add table
Reference in a new issue