소스 검색

Merge d2bed6b658efe381ed1695faa13c041c828469a1 into 3b79c8e09fc9d3056e978006d7989e0e1f70c6bc

Andrejs Baranovskis 3 달 전
부모
커밋
e88a9fdba6
3개의 변경된 파일347개의 추가작업 그리고 18개의 파일을 삭제
  1. 187 18
      internal/glance/widget-server-stats.go
  2. 83 0
      pkg/proxmox/proxmox.go
  3. 77 0
      pkg/proxmox/types.go

+ 187 - 18
internal/glance/widget-server-stats.go

@@ -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 - 0
pkg/proxmox/proxmox.go

@@ -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 - 0
pkg/proxmox/types.go

@@ -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"`
+}