package web import ( "context" "net/http" "strconv" "strings" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) // CapacityAdmin sets what parts of the tenancy tree are guaranteed of the // cluster and may hold. The admin API implements it; the page and the CLI go // through the same checks. type CapacityAdmin interface { QuotaViews(ctx context.Context) ([]adminapi.QuotaView, error) SetQuota(ctx context.Context, req adminapi.QuotaRequest, by string) (string, error) ClearQuota(ctx context.Context, scope string) (string, error) Scopes(ctx context.Context) ([]adminapi.ScopeName, error) } // SetCapacity gives the quotas page capacity quotas to show and set. Without // it the section is left off. func (p *AccountPages) SetCapacity(c CapacityAdmin) { p.capacity = c } // capacityRow is one scope's capacity quota as the page shows it. type capacityRow struct { adminapi.QuotaView GPUs, Cores, Memory string } // capacityForm is what the form starts with: a scope's quota to change, or // nothing for a new one. type capacityForm struct { Scope string GPUs, Cores, Memory string LGPUs, LCores, LMem string } func (p *AccountPages) capacitySection(ctx context.Context, l *Lang, d *adminQuotasData, editing string) { if p.capacity == nil { return } d.HasCapacity = true views, err := p.capacity.QuotaViews(ctx) if err != nil { d.Error = l.Err(err) return } for _, v := range views { d.Capacity = append(d.Capacity, capacityRow{QuotaView: v, GPUs: l.capCell(v.Holds.GPUs, v.Guarantee.GPUs, v.Limit.GPUs), Cores: l.capCell(v.Holds.CPUCores, v.Guarantee.CPUCores, v.Limit.CPUCores), Memory: l.capCell(v.Holds.MemoryGB, v.Guarantee.MemoryGB, v.Limit.MemoryGB)}) if v.Scope == editing { d.CapForm = capacityForm{Scope: v.Scope, GPUs: intStr(v.Guarantee.GPUs), Cores: intStr(v.Guarantee.CPUCores), Memory: intStr(v.Guarantee.MemoryGB), LGPUs: intStr(v.Limit.GPUs), LCores: intStr(v.Limit.CPUCores), LMem: intStr(v.Limit.MemoryGB)} } } if d.Scopes, err = p.capacity.Scopes(ctx); err != nil { d.Error = l.Err(err) } } // capCell is a capacity figure in English, for a caller with no request to // take a language from. func capCell(holds int, g, l *int) string { return english.capCell(holds, g, l) } // capCell is "6 of 8 (at most 16)": held, guaranteed, limit. func (lang *Lang) capCell(holds int, g, l *int) string { if g == nil && l == nil { return "" } out := lang.Int(int64(holds)) if g != nil { out = lang.T("capacity.cell.of", out, lang.Int(int64(*g))) } if l != nil { out = lang.T("capacity.cell.at_most", out, lang.Int(int64(*l))) } return out } func intStr(v *int) string { if v == nil { return "" } return strconv.Itoa(*v) } // applyCapacity sets or clears one scope's capacity quota from the form, and // says what it did in the request's language. What the admin API refuses is // its own English. func (p *AccountPages) applyCapacity(r *http.Request, admin store.User) (string, error) { l := langOf(r) scope := strings.TrimSpace(r.FormValue("scope")) if scope == "" { return "", quotaRefusal{key: "capacity.err.scope"} } if r.FormValue("clear") != "" { key, err := p.capacity.ClearQuota(r.Context(), scope) if err != nil { return "", err } return l.T("capacity.msg.removed", key), nil } field := func(name string) (*int, error) { v := strings.TrimSpace(r.FormValue(name)) if v == "" { return nil, nil } n, err := strconv.Atoi(v) if err != nil || n < 0 { return nil, quotaRefusal{key: "capacity.err." + name} } return &n, nil } var req adminapi.QuotaRequest req.Scope = scope var err error for _, f := range []struct { dst **int name string }{ {&req.Guarantee.GPUs, "gpus"}, {&req.Guarantee.CPUCores, "cores"}, {&req.Guarantee.MemoryGB, "memory"}, {&req.Limit.GPUs, "limit_gpus"}, {&req.Limit.CPUCores, "limit_cores"}, {&req.Limit.MemoryGB, "limit_memory"}, } { if *f.dst, err = field(f.name); err != nil { return "", err } } key, err := p.capacity.SetQuota(r.Context(), req, admin.Email) if err != nil { return "", err } return l.T("capacity.msg.set", key), nil }