diff --git a/app/common/types.go b/app/common/types.go index 2acde1e..aeaa9c6 100644 --- a/app/common/types.go +++ b/app/common/types.go @@ -31,7 +31,6 @@ type RequestType int type RequestContext struct { Cookies map[string]string - Body map[string]any } type Auth struct { diff --git a/app/common/utils.go b/app/common/utils.go index c5c9f3a..020780c 100644 --- a/app/common/utils.go +++ b/app/common/utils.go @@ -10,6 +10,7 @@ import ( "io" "io/fs" "log" + "math" "net/http" "os" "reflect" @@ -22,6 +23,12 @@ import ( var TMPL *template.Template var Global Config +type VMPath struct { + Node string + Type string + VMID string +} + func GetConfig(configPath string) Config { content, err := os.ReadFile(configPath) if err != nil { @@ -159,7 +166,7 @@ func HandleNonFatalError(c *gin.Context, err error) { c.Status(http.StatusInternalServerError) } -func RequestGetAPI(path string, context RequestContext) (*http.Response, int, error) { +func RequestGetAPI(path string, context RequestContext, body any) (*http.Response, int, error) { req, err := http.NewRequest("GET", Global.API+path, nil) if err != nil { return nil, 0, err @@ -186,9 +193,18 @@ func RequestGetAPI(path string, context RequestContext) (*http.Response, int, er return nil, response.StatusCode, err } - err = json.Unmarshal(data, &context.Body) - if err != nil { - return nil, response.StatusCode, err + switch body.(type) { // write json to body object depending on type, currently supports map[string]any (ie json) or []any (ie array of json) + case *map[string]any: + err = json.Unmarshal(data, &body) + if err != nil { + return nil, response.StatusCode, err + } + case *[]any: + err = json.Unmarshal(data, &body) + if err != nil { + return nil, response.StatusCode, err + } + default: } return response, response.StatusCode, nil @@ -205,3 +221,38 @@ func GetAuth(c *gin.Context) (Auth, error) { return Auth{username, token, csrf}, nil } } + +func ExtractVMPath(c *gin.Context) (VMPath, error) { + req_node := c.Query("node") + req_type := c.Query("type") + req_vmid := c.Query("vmid") + if req_node == "" || req_type == "" || req_vmid == "" { + return VMPath{}, fmt.Errorf("request missing required values: (node: %s, type: %s, vmid: %s)", req_node, req_type, req_vmid) + } + vm_path := VMPath{ + Node: req_node, + Type: req_type, + VMID: req_vmid, + } + return vm_path, nil +} + +func FormatNumber(val int64, base int64) (float64, string) { + valf := float64(val) + basef := float64(base) + steps := 0 + for math.Abs(valf) > basef && steps < 4 { + valf /= basef + steps++ + } + + if base == 1000 { + prefixes := []string{"", "K", "M", "G", "T"} + return valf, prefixes[steps] + } else if base == 1024 { + prefixes := []string{"", "Ki", "Mi", "Gi", "Ti"} + return valf, prefixes[steps] + } else { + return 0, "" + } +} diff --git a/app/routes/account.go b/app/routes/account.go index 0e5c6ce..7723aae 100644 --- a/app/routes/account.go +++ b/app/routes/account.go @@ -2,7 +2,6 @@ package routes import ( "fmt" - "math" "net/http" "proxmoxaas-dashboard/app/common" @@ -108,7 +107,7 @@ func HandleGETAccount(c *gin.Context) { for k, v := range account.Resources { switch t := v.(type) { case NumericResource: - avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base) + avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base) account.Resources[k] = ResourceChart{ Type: t.Type, Display: t.Display, @@ -121,7 +120,7 @@ func HandleGETAccount(c *gin.Context) { ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(), } case StorageResource: - avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base) + avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base) account.Resources[k] = ResourceChart{ Type: t.Type, Display: t.Display, @@ -181,45 +180,45 @@ func GetUserAccount(auth common.Auth) (Account, error) { "PVEAuthCookie": auth.Token, "CSRFPreventionToken": auth.CSRF, }, - Body: map[string]any{}, } // get user account basic data - res, code, err := common.RequestGetAPI("/user/config/cluster", ctx) + body := map[string]any{} + res, code, err := common.RequestGetAPI("/user/config/cluster", ctx, &body) if err != nil { return account, err } if code != 200 { return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res) } - err = mapstructure.Decode(ctx.Body, &account) + err = mapstructure.Decode(body, &account) if err != nil { return account, err } else { account.Username = auth.Username } - ctx.Body = map[string]any{} + body = map[string]any{} // get user resources - res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx) + res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx, &body) if err != nil { return account, err } if code != 200 { return account, fmt.Errorf("request to /user/dynamic/resources resulted in %+v", res) } - resources := ctx.Body + resources := body - ctx.Body = map[string]any{} + body = map[string]any{} // get resource meta data - res, code, err = common.RequestGetAPI("/global/config/resources", ctx) + res, code, err = common.RequestGetAPI("/global/config/resources", ctx, &body) if err != nil { return account, err } if code != 200 { return account, fmt.Errorf("request to /global/config/resources resulted in %+v", res) } - meta := ctx.Body["resources"].(map[string]any) + meta := body["resources"].(map[string]any) // build each resource by its meta type for k, v := range meta { @@ -259,26 +258,6 @@ func GetUserAccount(auth common.Auth) (Account, error) { return account, nil } -func FormatNumber(val int64, base int64) (float64, string) { - valf := float64(val) - basef := float64(base) - steps := 0 - for math.Abs(valf) > basef && steps < 4 { - valf /= basef - steps++ - } - - if base == 1000 { - prefixes := []string{"", "K", "M", "G", "T"} - return valf, prefixes[steps] - } else if base == 1024 { - prefixes := []string{"", "Ki", "Mi", "Gi", "Ti"} - return valf, prefixes[steps] - } else { - return 0, "" - } -} - // interpolate between min and max by normalized (0 - 1) val func InterpolateColorHSV(min color.RGB, max color.RGB, val float64) color.RGB { minhsl := min.ToHSL() diff --git a/app/routes/config.go b/app/routes/config.go index 280a1cd..6c02ac7 100644 --- a/app/routes/config.go +++ b/app/routes/config.go @@ -13,12 +13,6 @@ import ( "github.com/go-viper/mapstructure/v2" ) -type VMPath struct { - Node string - Type string - VMID string -} - // imported types from fabric type InstanceConfig struct { @@ -56,7 +50,7 @@ type CPUConfig struct { func HandleGETConfig(c *gin.Context) { auth, err := common.GetAuth(c) if err == nil { - vm_path, err := ExtractVMPath(c) + vm_path, err := common.ExtractVMPath(c) if err != nil { common.HandleNonFatalError(c, err) } @@ -91,7 +85,7 @@ func HandleGETConfig(c *gin.Context) { func HandleGETConfigVolumesFragment(c *gin.Context) { auth, err := common.GetAuth(c) if err == nil { - vm_path, err := ExtractVMPath(c) + vm_path, err := common.ExtractVMPath(c) if err != nil { common.HandleNonFatalError(c, err) } @@ -114,7 +108,7 @@ func HandleGETConfigVolumesFragment(c *gin.Context) { func HandleGETConfigNetsFragment(c *gin.Context) { auth, err := common.GetAuth(c) if err == nil { - vm_path, err := ExtractVMPath(c) + vm_path, err := common.ExtractVMPath(c) if err != nil { common.HandleNonFatalError(c, err) } @@ -137,7 +131,7 @@ func HandleGETConfigNetsFragment(c *gin.Context) { func HandleGETConfigDevicesFragment(c *gin.Context) { auth, err := common.GetAuth(c) if err == nil { - vm_path, err := ExtractVMPath(c) + vm_path, err := common.ExtractVMPath(c) if err != nil { common.HandleNonFatalError(c, err) } @@ -160,7 +154,7 @@ func HandleGETConfigDevicesFragment(c *gin.Context) { func HandleGETConfigBootFragment(c *gin.Context) { auth, err := common.GetAuth(c) if err == nil { - vm_path, err := ExtractVMPath(c) + vm_path, err := common.ExtractVMPath(c) if err != nil { common.HandleNonFatalError(c, err) } @@ -180,22 +174,7 @@ func HandleGETConfigBootFragment(c *gin.Context) { } } -func ExtractVMPath(c *gin.Context) (VMPath, error) { - req_node := c.Query("node") - req_type := c.Query("type") - req_vmid := c.Query("vmid") - if req_node == "" || req_type == "" || req_vmid == "" { - return VMPath{}, fmt.Errorf("request missing required values: (node: %s, type: %s, vmid: %s)", req_node, req_type, req_vmid) - } - vm_path := VMPath{ - Node: req_node, - Type: req_type, - VMID: req_vmid, - } - return vm_path, nil -} - -func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) { +func GetInstanceConfig(vm common.VMPath, auth common.Auth) (InstanceConfig, error) { config := InstanceConfig{} path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID) ctx := common.RequestContext{ @@ -204,9 +183,9 @@ func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) { "PVEAuthCookie": auth.Token, "CSRFPreventionToken": auth.CSRF, }, - Body: map[string]any{}, } - res, code, err := common.RequestGetAPI(path, ctx) + body := map[string]any{} + res, code, err := common.RequestGetAPI(path, ctx, &body) if err != nil { return config, err } @@ -214,7 +193,7 @@ func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) { return config, fmt.Errorf("request to %s resulted in %+v", path, res) } - err = mapstructure.Decode(ctx.Body, &config) + err = mapstructure.Decode(body, &config) if err != nil { return config, err } @@ -225,7 +204,7 @@ func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) { return config, nil } -func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) { +func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) { cputypes := common.Select{ ID: "proctype", Required: true, @@ -238,10 +217,10 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) { "PVEAuthCookie": auth.Token, "CSRFPreventionToken": auth.CSRF, }, - Body: map[string]any{}, } + body := map[string]any{} path := "/global/config/resources" - res, code, err := common.RequestGetAPI(path, ctx) + res, code, err := common.RequestGetAPI(path, ctx, &body) if err != nil { return cputypes, err } @@ -249,15 +228,15 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) { return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res) } global := GlobalConfig{} - err = mapstructure.Decode(ctx.Body["resources"], &global) + err = mapstructure.Decode(body["resources"], &global) if err != nil { return cputypes, err } // get user resource config - ctx.Body = map[string]any{} + body = map[string]any{} path = "/user/config/resources" - res, code, err = common.RequestGetAPI(path, ctx) + res, code, err = common.RequestGetAPI(path, ctx, &body) if err != nil { return cputypes, err } @@ -265,7 +244,7 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) { return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res) } user := UserConfigResources{} - err = mapstructure.Decode(ctx.Body, &user) + err = mapstructure.Decode(body, &user) if err != nil { return cputypes, err } @@ -287,9 +266,9 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) { } } else { // cpu is a blacklist // get the supported cpu types from the node - ctx.Body = map[string]any{} + body = map[string]any{} path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.Node) - res, code, err = common.RequestGetAPI(path, ctx) + res, code, err = common.RequestGetAPI(path, ctx, &body) if err != nil { return cputypes, err } @@ -299,7 +278,7 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) { supported := struct { data []CPUConfig }{} - err = mapstructure.Decode(ctx.Body, supported) + err = mapstructure.Decode(body, supported) if err != nil { return cputypes, err } diff --git a/app/routes/index.go b/app/routes/index.go index b8dac6c..66cacc0 100644 --- a/app/routes/index.go +++ b/app/routes/index.go @@ -26,6 +26,7 @@ type InstanceCard struct { NodeStatus string ConfigPath string ConsolePath string + BackupsPath string } // used in retriving cluster tasks @@ -85,9 +86,9 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No "PVEAuthCookie": auth.Token, "CSRFPreventionToken": auth.CSRF, }, - Body: map[string]any{}, } - res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", ctx) + body := map[string]any{} + res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", ctx, &body) if err != nil { return nil, nil, err } @@ -99,7 +100,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No nodes := map[string]Node{} // if we successfully retrieved the resources, then process it and return index - for _, v := range ctx.Body["data"].([]any) { + for _, v := range body["data"].([]any) { m := v.(map[string]any) if m["type"] == "node" { node := Node{} @@ -126,11 +127,12 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No } else if instance.Type == "lxc" { instance.ConsolePath = fmt.Sprintf("%s/?console=lxc&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&xtermjs=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node) } + instance.BackupsPath = fmt.Sprintf("backups?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID) instances[vmid] = instance } - ctx.Body = map[string]any{} - res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", ctx) + body = map[string]any{} + res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", ctx, &body) if err != nil { return nil, nil, err } @@ -141,7 +143,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No most_recent_task := map[uint]uint{} expected_state := map[uint]string{} - for _, v := range ctx.Body["data"].([]any) { + for _, v := range body["data"].([]any) { task := Task{} err := mapstructure.Decode(v, &task) if err != nil { @@ -180,8 +182,8 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No // get /status/current which is updated faster than /cluster/resources instance := instances[vmid] path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID) - ctx.Body = map[string]any{} - res, code, err := common.RequestGetAPI(path, ctx) + body = map[string]any{} + res, code, err := common.RequestGetAPI(path, ctx, &body) if err != nil { return nil, nil, err } @@ -190,7 +192,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No } status := InstanceStatus{} - mapstructure.Decode(ctx.Body["data"], &status) + mapstructure.Decode(body["data"], &status) instance.Status = status.Status instances[vmid] = instance diff --git a/app/routes/login.go b/app/routes/login.go index 9e27116..cae154f 100644 --- a/app/routes/login.go +++ b/app/routes/login.go @@ -26,9 +26,10 @@ func GetLoginRealms() ([]Realm, error) { ctx := common.RequestContext{ Cookies: nil, - Body: map[string]any{}, + //Body: map[string]any{}, } - res, code, err := common.RequestGetAPI("/proxmox/access/domains", ctx) + body := map[string]any{} + res, code, err := common.RequestGetAPI("/proxmox/access/domains", ctx, &body) if err != nil { return realms, err } @@ -36,7 +37,7 @@ func GetLoginRealms() ([]Realm, error) { return realms, fmt.Errorf("request to /proxmox/access/domains resulted in %+v", res) } - for _, v := range ctx.Body["data"].([]any) { + for _, v := range body["data"].([]any) { v = v.(map[string]any) realm := Realm{} err := mapstructure.Decode(v, &realm)