feat(verlauf): Filter, Wiederholen und Aufräumen
Verlauf filterbar (URL, API, proxdeploy history); Wiederholen füllt den Assistenten aus einem alten Auftrag; Aufräumen löscht nach Fehlschlag oder Abbruch den halb angelegten Gast – nur, wenn dessen Beschreibung auf den Auftrag verweist. Deshalb setzt das Klonen die Beschreibung jetzt sofort. Auf pve-demo geprüft (CT 109). Closes #014 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -927,6 +927,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"aufraeumbar": {
|
||||
"nullable": true,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"typ": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"VM",
|
||||
"LXC"
|
||||
]
|
||||
},
|
||||
"vmid": {
|
||||
"type": "integer",
|
||||
"minimum": -9007199254740991,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"knoten": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"typ",
|
||||
"vmid",
|
||||
"knoten"
|
||||
]
|
||||
},
|
||||
"phasen": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -954,9 +981,42 @@
|
||||
"gestartetAm",
|
||||
"beendetAm",
|
||||
"abschluss",
|
||||
"aufraeumbar",
|
||||
"phasen"
|
||||
]
|
||||
},
|
||||
"Verlauf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eintraege": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Auftrag"
|
||||
}
|
||||
},
|
||||
"gesamt": {
|
||||
"type": "integer",
|
||||
"minimum": -9007199254740991,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"seite": {
|
||||
"type": "integer",
|
||||
"minimum": -9007199254740991,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"seiten": {
|
||||
"type": "integer",
|
||||
"minimum": -9007199254740991,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"eintraege",
|
||||
"gesamt",
|
||||
"seite",
|
||||
"seiten"
|
||||
]
|
||||
},
|
||||
"LogZeile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1347,6 +1407,81 @@
|
||||
}
|
||||
},
|
||||
"/auftraege": {
|
||||
"get": {
|
||||
"operationId": "verlauf",
|
||||
"summary": "Verlauf, neueste zuerst, 25 je Seite",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "typ",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"vm",
|
||||
"lxc",
|
||||
"template",
|
||||
"aufraeumen"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "distribution",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seite",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Seite des Verlaufs",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Verlauf"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Fehler",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FehlerAntwort"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "ausrollen",
|
||||
"summary": "VM oder LXC ausrollen (Auftrag)",
|
||||
@@ -1558,6 +1693,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auftraege/{id}/aufraeumen": {
|
||||
"post": {
|
||||
"operationId": "auftragAufraeumen",
|
||||
"summary": "Halb angelegten Gast eines fehlgeschlagenen/abgebrochenen Auftrags löschen (Auftrag)",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "ID oder Kurz-ID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Auftrag angenommen",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AuftragAngenommen"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Fehler",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FehlerAntwort"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auftraege/{id}/abbrechen": {
|
||||
"post": {
|
||||
"operationId": "auftragAbbrechen",
|
||||
|
||||
@@ -200,6 +200,27 @@ func (c *Client) Log(ctx context.Context, id, ab string) ([]LogZeile, error) {
|
||||
return zeilen, c.Aufrufen(ctx, http.MethodGet, pfad, nil, &zeilen)
|
||||
}
|
||||
|
||||
// Verlauf: eine Seite, gefiltert. Leere Filter fallen weg.
|
||||
func (c *Client) Verlauf(ctx context.Context, filter map[string]string) (Verlauf, error) {
|
||||
werte := url.Values{}
|
||||
for k, v := range filter {
|
||||
if v != "" {
|
||||
werte.Set(k, v)
|
||||
}
|
||||
}
|
||||
pfad := "/auftraege"
|
||||
if len(werte) > 0 {
|
||||
pfad += "?" + werte.Encode()
|
||||
}
|
||||
var v Verlauf
|
||||
return v, c.Aufrufen(ctx, http.MethodGet, pfad, nil, &v)
|
||||
}
|
||||
|
||||
func (c *Client) Aufraeumen(ctx context.Context, id string) (AuftragAngenommen, error) {
|
||||
var a AuftragAngenommen
|
||||
return a, c.Aufrufen(ctx, http.MethodPost, "/auftraege/"+url.PathEscape(id)+"/aufraeumen", nil, &a)
|
||||
}
|
||||
|
||||
func (c *Client) Abbrechen(ctx context.Context, id string) error {
|
||||
return c.Aufrufen(ctx, http.MethodPost, "/auftraege/"+url.PathEscape(id)+"/abbrechen", nil, nil)
|
||||
}
|
||||
|
||||
@@ -30,18 +30,36 @@ func (e AuftragArt) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for AuftragAufraeumbarTyp.
|
||||
const (
|
||||
AuftragAufraeumbarTypLXC AuftragAufraeumbarTyp = "LXC"
|
||||
AuftragAufraeumbarTypVM AuftragAufraeumbarTyp = "VM"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the AuftragAufraeumbarTyp enum.
|
||||
func (e AuftragAufraeumbarTyp) Valid() bool {
|
||||
switch e {
|
||||
case AuftragAufraeumbarTypLXC:
|
||||
return true
|
||||
case AuftragAufraeumbarTypVM:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for AuftragGastTyp.
|
||||
const (
|
||||
LXC AuftragGastTyp = "LXC"
|
||||
VM AuftragGastTyp = "VM"
|
||||
AuftragGastTypLXC AuftragGastTyp = "LXC"
|
||||
AuftragGastTypVM AuftragGastTyp = "VM"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the AuftragGastTyp enum.
|
||||
func (e AuftragGastTyp) Valid() bool {
|
||||
switch e {
|
||||
case LXC:
|
||||
case AuftragGastTypLXC:
|
||||
return true
|
||||
case VM:
|
||||
case AuftragGastTypVM:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -201,6 +219,30 @@ func (e ProfilVariableType) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for VerlaufParamsTyp.
|
||||
const (
|
||||
VerlaufParamsTypAufraeumen VerlaufParamsTyp = "aufraeumen"
|
||||
VerlaufParamsTypLxc VerlaufParamsTyp = "lxc"
|
||||
VerlaufParamsTypTemplate VerlaufParamsTyp = "template"
|
||||
VerlaufParamsTypVm VerlaufParamsTyp = "vm"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the VerlaufParamsTyp enum.
|
||||
func (e VerlaufParamsTyp) Valid() bool {
|
||||
switch e {
|
||||
case VerlaufParamsTypAufraeumen:
|
||||
return true
|
||||
case VerlaufParamsTypLxc:
|
||||
return true
|
||||
case VerlaufParamsTypTemplate:
|
||||
return true
|
||||
case VerlaufParamsTypVm:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for DistributionenListeParamsTyp.
|
||||
const (
|
||||
DistributionenListeParamsTypLxc DistributionenListeParamsTyp = "lxc"
|
||||
@@ -229,8 +271,13 @@ type Abschluss struct {
|
||||
|
||||
// Auftrag defines model for Auftrag.
|
||||
type Auftrag struct {
|
||||
Abschluss *Abschluss `json:"abschluss"`
|
||||
Art AuftragArt `json:"art"`
|
||||
Abschluss *Abschluss `json:"abschluss"`
|
||||
Art AuftragArt `json:"art"`
|
||||
Aufraeumbar *struct {
|
||||
Knoten *string `json:"knoten"`
|
||||
Typ AuftragAufraeumbarTyp `json:"typ"`
|
||||
Vmid int `json:"vmid"`
|
||||
} `json:"aufraeumbar"`
|
||||
|
||||
// BeendetAm ISO 8601
|
||||
BeendetAm *string `json:"beendetAm"`
|
||||
@@ -267,6 +314,9 @@ type Auftrag struct {
|
||||
// AuftragArt defines model for Auftrag.Art.
|
||||
type AuftragArt string
|
||||
|
||||
// AuftragAufraeumbarTyp defines model for Auftrag.Aufraeumbar.Typ.
|
||||
type AuftragAufraeumbarTyp string
|
||||
|
||||
// AuftragGastTyp defines model for Auftrag.GastTyp.
|
||||
type AuftragGastTyp string
|
||||
|
||||
@@ -537,6 +587,14 @@ type TokenAnmeldung struct {
|
||||
Tage *int `json:"tage,omitempty"`
|
||||
}
|
||||
|
||||
// Verlauf defines model for Verlauf.
|
||||
type Verlauf struct {
|
||||
Eintraege []Auftrag `json:"eintraege"`
|
||||
Gesamt int `json:"gesamt"`
|
||||
Seite int `json:"seite"`
|
||||
Seiten int `json:"seiten"`
|
||||
}
|
||||
|
||||
// Vorlage defines model for Vorlage.
|
||||
type Vorlage struct {
|
||||
Beschreibung *string `json:"beschreibung"`
|
||||
@@ -544,6 +602,18 @@ type Vorlage struct {
|
||||
Titel string `json:"titel"`
|
||||
}
|
||||
|
||||
// VerlaufParams defines parameters for Verlauf.
|
||||
type VerlaufParams struct {
|
||||
Server *string `form:"server,omitempty" json:"server,omitempty"`
|
||||
Typ *VerlaufParamsTyp `form:"typ,omitempty" json:"typ,omitempty"`
|
||||
Distribution *string `form:"distribution,omitempty" json:"distribution,omitempty"`
|
||||
Status *string `form:"status,omitempty" json:"status,omitempty"`
|
||||
Seite *int `form:"seite,omitempty" json:"seite,omitempty"`
|
||||
}
|
||||
|
||||
// VerlaufParamsTyp defines parameters for Verlauf.
|
||||
type VerlaufParamsTyp string
|
||||
|
||||
// AuftragEreignisseParams defines parameters for AuftragEreignisse.
|
||||
type AuftragEreignisseParams struct {
|
||||
LastEventID *string `json:"Last-Event-ID,omitempty"`
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package befehle
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"git.wilde-server.de/florian.wilde/proxdeploy/cli/internal/api"
|
||||
)
|
||||
@@ -172,3 +175,146 @@ func (u *umgebung) cancelBefehl() *cobra.Command {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Statustexte wie in der Web-GUI.
|
||||
var statusText = map[api.AuftragStatus]string{
|
||||
api.AuftragStatusWARTEND: "wartet", api.AuftragStatusLAEUFT: "läuft", api.AuftragStatusERFOLGREICH: "erfolgreich",
|
||||
api.AuftragStatusFEHLGESCHLAGEN: "fehlgeschlagen", api.AuftragStatusABGEBROCHEN: "abgebrochen",
|
||||
api.AuftragStatusAUFGERAEUMT: "aufgeräumt",
|
||||
}
|
||||
|
||||
// statusAusText nimmt „fehlgeschlagen“, „läuft“, „aufgeräumt“ oder den API-Wert.
|
||||
func statusAusText(text string) string {
|
||||
for wert, t := range statusText {
|
||||
if strings.EqualFold(t, text) || strings.EqualFold(string(wert), text) {
|
||||
return string(wert)
|
||||
}
|
||||
}
|
||||
return strings.ToUpper(text)
|
||||
}
|
||||
|
||||
func (u *umgebung) historyBefehl() *cobra.Command {
|
||||
var server, typ, distro, status string
|
||||
var seite int
|
||||
befehl := &cobra.Command{
|
||||
Use: "history",
|
||||
Aliases: []string{"verlauf"},
|
||||
Short: "Verlauf der Aufträge, neueste zuerst",
|
||||
Example: " proxdeploy history --type lxc --status FEHLGESCHLAGEN\n proxdeploy history --server rz1 --page 2",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
c, _, err := u.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filter := map[string]string{"server": server, "typ": typ, "distribution": distro, "status": ""}
|
||||
if status != "" {
|
||||
filter["status"] = statusAusText(status)
|
||||
}
|
||||
if seite > 1 {
|
||||
filter["seite"] = fmt.Sprint(seite)
|
||||
}
|
||||
v, err := c.Verlauf(cmd.Context(), filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.opt.json {
|
||||
return u.jsonAusgeben(v)
|
||||
}
|
||||
s := u.stil()
|
||||
if len(v.Eintraege) == 0 {
|
||||
fmt.Fprintln(u.aus, s.Muted("Keine Aufträge."))
|
||||
return nil
|
||||
}
|
||||
symbole := map[api.AuftragStatus]string{
|
||||
api.AuftragStatusERFOLGREICH: "✓", api.AuftragStatusFEHLGESCHLAGEN: "✗", api.AuftragStatusABGEBROCHEN: "✗",
|
||||
api.AuftragStatusLAEUFT: "◐", api.AuftragStatusWARTEND: "○", api.AuftragStatusAUFGERAEUMT: "○",
|
||||
}
|
||||
zeilen := [][]string{}
|
||||
for _, a := range v.Eintraege {
|
||||
art := map[api.AuftragArt]string{api.AUSROLLEN: "VM", api.TEMPLATEBAUEN: "Template", api.AUFRAEUMEN: "Aufräumen"}[a.Art]
|
||||
if a.Art == api.AUSROLLEN && a.GastTyp != nil && *a.GastTyp == api.AuftragGastTypLXC {
|
||||
art = "LXC"
|
||||
}
|
||||
id := "–"
|
||||
if a.Vmid != nil {
|
||||
id = fmt.Sprint(*a.Vmid)
|
||||
}
|
||||
zustand := symbole[a.Status] + " " + statusText[a.Status]
|
||||
if s.ASCII {
|
||||
zustand = strings.NewReplacer("✓", "[OK]", "✗", "[!!]", "◐", "[..]", "○", "[--]").Replace(zustand)
|
||||
}
|
||||
zeit := a.ErstelltAm
|
||||
if t, ok := zeitAus(&a.ErstelltAm); ok {
|
||||
zeit = t.Local().Format("02.01. 15:04")
|
||||
}
|
||||
name := wert(a.Name)
|
||||
if name == "" {
|
||||
name = "–"
|
||||
}
|
||||
zeilen = append(zeilen, []string{a.KurzId, name, art, a.Server.Name, wert(a.DistributionId), id, zustand, zeit})
|
||||
}
|
||||
u.tabelle(s, []string{"AUFTRAG", "NAME", "TYP", "SERVER", "DISTRIBUTION", "ID", "STATUS", "ZEIT"}, zeilen)
|
||||
if v.Seiten > 1 {
|
||||
fmt.Fprintln(u.aus, s.Muted(fmt.Sprintf("Seite %d von %d · %d Aufträge · weiter mit --page %d", v.Seite, v.Seiten, v.Gesamt, min(v.Seite+1, v.Seiten))))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
f := befehl.Flags()
|
||||
f.StringVar(&server, "server", "", "nur dieser Server")
|
||||
f.StringVar(&typ, "type", "", "vm, lxc, template oder aufraeumen")
|
||||
f.StringVar(&distro, "distro", "", "nur diese Distribution")
|
||||
f.StringVar(&status, "status", "", "z. B. fehlgeschlagen, erfolgreich, laeuft")
|
||||
f.IntVar(&seite, "page", 1, "Seite (25 je Seite)")
|
||||
return befehl
|
||||
}
|
||||
|
||||
func (u *umgebung) cleanupBefehl() *cobra.Command {
|
||||
var ja, nichtWarten bool
|
||||
befehl := &cobra.Command{
|
||||
Use: "cleanup <job-id>",
|
||||
Short: "Halb angelegten Gast eines fehlgeschlagenen Auftrags löschen",
|
||||
Long: "Stoppt und löscht die VM bzw. den Container, den ein fehlgeschlagener oder abgebrochener\n" +
|
||||
"Auftrag angelegt hat. Gelöscht wird nur, was laut Beschreibung zu diesem Auftrag gehört.",
|
||||
Args: eineID("cleanup"),
|
||||
RunE: func(cmd *cobra.Command, argumente []string) error {
|
||||
c, _, err := u.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a, err := c.Auftrag(cmd.Context(), argumente[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.Aufraeumbar == nil {
|
||||
return bedienfehler("bei %s gibt es nichts aufzuräumen (Status %s)", a.KurzId, statusText[a.Status])
|
||||
}
|
||||
z := a.Aufraeumbar
|
||||
wort := map[api.AuftragAufraeumbarTyp]string{api.AuftragAufraeumbarTypVM: "VM", api.AuftragAufraeumbarTypLXC: "Container"}[z.Typ]
|
||||
ziel := fmt.Sprintf("%s %d", wort, z.Vmid)
|
||||
if z.Knoten != nil {
|
||||
ziel += " auf " + *z.Knoten
|
||||
}
|
||||
if !ja {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return bedienfehler("%s löschen? Ohne Terminal mit --yes bestätigen", ziel)
|
||||
}
|
||||
fmt.Fprintf(u.fehler, "%s stoppen und mit allen Disks löschen? [j/N] ", ziel)
|
||||
antwort, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if a := strings.ToLower(strings.TrimSpace(antwort)); a != "j" && a != "ja" && a != "y" && a != "yes" {
|
||||
fmt.Fprintln(u.fehler, u.stil().Muted("Nichts gelöscht."))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
angenommen, err := c.Aufraeumen(cmd.Context(), a.KurzId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.angenommen(cmd.Context(), c, angenommen, nichtWarten)
|
||||
},
|
||||
}
|
||||
befehl.Flags().BoolVarP(&ja, "yes", "y", false, "ohne Rückfrage")
|
||||
befehl.Flags().BoolVar(&nichtWarten, "no-wait", false, "nicht verfolgen, nur die Auftrags-ID ausgeben")
|
||||
return befehl
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var aktualisieren = flag.Bool("update", false, "Golden-Dateien neu schreiben")
|
||||
@@ -78,6 +79,8 @@ type fakeAPI struct {
|
||||
plan map[string]any
|
||||
status int
|
||||
antwort any
|
||||
verlauf map[string]any
|
||||
einzeln map[string]any
|
||||
}
|
||||
|
||||
func (f *fakeAPI) handler(t *testing.T) http.Handler {
|
||||
@@ -88,6 +91,11 @@ func (f *fakeAPI) handler(t *testing.T) http.Handler {
|
||||
fmt.Fprint(w, `{"fehler":{"code":"nicht_angemeldet","nachricht":"Nicht angemeldet."}}`)
|
||||
return
|
||||
}
|
||||
f.mu.Lock()
|
||||
if f.anfragen == nil {
|
||||
f.anfragen = map[string][]map[string]any{}
|
||||
}
|
||||
f.mu.Unlock()
|
||||
if r.Body != nil && r.Method == http.MethodPost {
|
||||
var rumpf map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&rumpf)
|
||||
@@ -112,6 +120,15 @@ func (f *fakeAPI) handler(t *testing.T) http.Handler {
|
||||
schreiben(200, map[string]any{"benutzername": "florian", "anzeigename": "Florian", "rolle": "OPERATOR", "rechte": []string{}, "version": "0.4.0"})
|
||||
case r.URL.Path == "/api/v1/auftraege" && r.Method == http.MethodPost:
|
||||
schreiben(202, map[string]any{"id": "c1", "kurzId": "a-7f3k"})
|
||||
case r.URL.Path == "/api/v1/auftraege" && r.Method == http.MethodGet:
|
||||
f.mu.Lock()
|
||||
f.anfragen["GET "+r.URL.RawQuery] = nil
|
||||
f.mu.Unlock()
|
||||
schreiben(200, f.verlauf)
|
||||
case strings.HasSuffix(r.URL.Path, "/aufraeumen"):
|
||||
schreiben(202, map[string]any{"id": "c2", "kurzId": "c-9x9x9"})
|
||||
case r.URL.Path == "/api/v1/auftraege/d-kaputt":
|
||||
schreiben(200, f.einzeln)
|
||||
case r.URL.Path == "/api/v1/auftraege/plan":
|
||||
schreiben(200, f.plan)
|
||||
case strings.HasSuffix(r.URL.Path, "/ereignisse"):
|
||||
@@ -323,3 +340,48 @@ func TestLoginMitToken(t *testing.T) {
|
||||
t.Error("falsches Token hat die Konfiguration verändert")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistory(t *testing.T) {
|
||||
ok := auftrag("ERFOLGREICH", alle, nil)
|
||||
ok["erstelltAm"] = "2026-10-12T08:05:00.000Z"
|
||||
ct := auftrag("FEHLGESCHLAGEN", alle, nil)
|
||||
ct["kurzId"], ct["name"], ct["gastTyp"], ct["vmid"], ct["distributionId"] = "d-ct001", "dns01", "LXC", 121, "ubuntu-24.04"
|
||||
ct["erstelltAm"] = "2026-10-12T08:00:00.000Z"
|
||||
f := &fakeAPI{verlauf: map[string]any{"eintraege": []any{ok, ct}, "gesamt": 30, "seite": 1, "seiten": 2}}
|
||||
alt := time.Local
|
||||
time.Local = time.UTC
|
||||
t.Cleanup(func() { time.Local = alt })
|
||||
aus, fehler, code := lauf(t, f, "history", "--type", "lxc", "--status", "fehlgeschlagen")
|
||||
if code != 0 {
|
||||
t.Fatalf("Exit %d: %s", code, fehler)
|
||||
}
|
||||
golden(t, "history.golden", aus)
|
||||
if _, gesehen := f.anfragen["GET status=FEHLGESCHLAGEN&typ=lxc"]; !gesehen {
|
||||
t.Errorf("Filter nicht gesendet: %v", f.anfragen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanup(t *testing.T) {
|
||||
kaputt := auftrag("FEHLGESCHLAGEN", alle, nil)
|
||||
kaputt["kurzId"] = "d-kaputt"
|
||||
kaputt["aufraeumbar"] = map[string]any{"typ": "VM", "vmid": 120, "knoten": "pve01"}
|
||||
f := &fakeAPI{einzeln: kaputt}
|
||||
// Ohne Terminal keine Rückfrage möglich: ohne --yes wird nichts gelöscht.
|
||||
_, fehler, code := lauf(t, f, "cleanup", "d-kaputt")
|
||||
if code != ExitBedienung || !strings.Contains(fehler, "VM 120 auf pve01 löschen?") {
|
||||
t.Errorf("ohne --yes: Exit %d, %s", code, fehler)
|
||||
}
|
||||
if len(f.anfragen["/api/v1/auftraege/d-kaputt/aufraeumen"]) != 0 {
|
||||
t.Error("ohne Bestätigung aufgeräumt")
|
||||
}
|
||||
aus, _, code := lauf(t, f, "cleanup", "d-kaputt", "--yes", "--no-wait")
|
||||
if code != 0 || strings.TrimSpace(aus) != "c-9x9x9" || len(f.anfragen["/api/v1/auftraege/d-kaputt/aufraeumen"]) != 1 {
|
||||
t.Errorf("mit --yes: Exit %d, %q", code, aus)
|
||||
}
|
||||
|
||||
kaputt["aufraeumbar"] = nil
|
||||
_, fehler, code = lauf(t, f, "cleanup", "d-kaputt", "--yes")
|
||||
if code != ExitBedienung || !strings.Contains(fehler, "nichts aufzuräumen") {
|
||||
t.Errorf("nicht aufräumbar: Exit %d, %s", code, fehler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
AUFTRAG NAME TYP SERVER DISTRIBUTION ID STATUS ZEIT
|
||||
a-7f3k web01 VM rz1 debian-13 120 ✓ erfolgreich 12.10. 08:05
|
||||
d-ct001 dns01 LXC rz1 ubuntu-24.04 121 ✗ fehlgeschlagen 12.10. 08:00
|
||||
Seite 1 von 2 · 30 Aufträge · weiter mit --page 2
|
||||
@@ -140,6 +140,8 @@ func (u *umgebung) wurzel() *cobra.Command {
|
||||
u.statusBefehl(),
|
||||
u.logsBefehl(),
|
||||
u.cancelBefehl(),
|
||||
u.historyBefehl(),
|
||||
u.cleanupBefehl(),
|
||||
)
|
||||
hilfeEindeutschen(wurzel)
|
||||
return wurzel
|
||||
|
||||
Reference in New Issue
Block a user