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" ) func eineID(befehl string) cobra.PositionalArgs { return func(_ *cobra.Command, argumente []string) error { if len(argumente) != 1 { return bedienfehler("Auftrags-ID fehlt: proxdeploy %s ", befehl) } return nil } } func (u *umgebung) statusBefehl() *cobra.Command { var folgen bool befehl := &cobra.Command{ Use: "status ", Short: "Stand eines Auftrags (mit --follow live bis zum Ende)", Args: eineID("status"), RunE: func(cmd *cobra.Command, argumente []string) error { c, _, err := u.client() if err != nil { return err } if folgen { _, err := u.folgen(cmd.Context(), c, argumente[0]) return err } a, err := c.Auftrag(cmd.Context(), argumente[0]) if err != nil { return err } fertig := endzustaende[a.Status] if u.opt.json { if err := u.jsonAusgeben(a); err != nil { return err } } else { d := u.neuerDarsteller() d.live = true // einmal vollständig zeichnen, ohne Neuzeichnen d.zeichnen(a, fertig) } if fertig && a.Status != api.AuftragStatusERFOLGREICH { return &exitFehler{code: ExitFehlschlag, gemeldet: true} } return nil }, } befehl.Flags().BoolVarP(&folgen, "follow", "f", false, "live verfolgen bis zum Ende") return befehl } func (u *umgebung) logsBefehl() *cobra.Command { var folgen bool befehl := &cobra.Command{ Use: "logs ", Short: "Detail-Log eines Auftrags (mit --follow live)", Args: eineID("logs"), RunE: func(cmd *cobra.Command, argumente []string) error { c, _, err := u.client() if err != nil { return err } id := argumente[0] zeilen, err := c.Log(cmd.Context(), id, "") if err != nil { return err } for _, z := range zeilen { u.logZeile(z) } if !folgen { return nil } letzte := "" if len(zeilen) > 0 { letzte = zeilen[len(zeilen)-1].Id } return u.logFolgen(cmd.Context(), c, id, letzte) }, } befehl.Flags().BoolVarP(&folgen, "follow", "f", false, "neue Zeilen live anhängen, bis der Auftrag fertig ist") return befehl } func (u *umgebung) logZeile(z api.LogZeile) { if u.opt.json { daten, _ := json.Marshal(z) fmt.Fprintln(u.aus, string(daten)) return } s := u.stil() zeit := z.Zeit if t, err := time.Parse(time.RFC3339Nano, z.Zeit); err == nil { zeit = t.Local().Format("15:04:05") } phase := "" if z.Phase != nil { phase = *z.Phase } nachricht := z.Nachricht switch z.Stufe { case "error": nachricht = s.Rot(nachricht) case "warn": nachricht = s.Amber(nachricht) default: nachricht = s.Paper(nachricht) } fmt.Fprintf(u.aus, "%s %s %s\n", s.Muted(zeit), s.Muted(fmt.Sprintf("%-16s", phase)), nachricht) } // logFolgen hängt neue Zeilen an (SSE-Ereignis `log`), bis der Auftrag fertig ist. func (u *umgebung) logFolgen(ctx context.Context, c *api.Client, id, ab string) error { for versuch := 0; versuch < 5; versuch++ { err := c.Ereignisse(ctx, id, ab, func(e api.Ereignis) error { switch e.Art { case "log": var zeilen []api.LogZeile if err := json.Unmarshal(e.Daten, &zeilen); err != nil { return err } for _, z := range zeilen { u.logZeile(z) ab = z.Id } case "ende": return api.Ende } return nil }) if errors.Is(err, api.Ende) || ctx.Err() != nil { return nil } var apiFehler *api.Fehler if errors.As(err, &apiFehler) { return err } time.Sleep(time.Duration(versuch+1) * time.Second) } return &api.VerbindungsFehler{Ursache: errors.New("Ereignisstrom bricht wiederholt ab")} } func (u *umgebung) cancelBefehl() *cobra.Command { return &cobra.Command{ Use: "cancel ", Short: "Auftrag abbrechen (hält an der nächsten Phasengrenze)", Args: eineID("cancel"), RunE: func(cmd *cobra.Command, argumente []string) error { c, _, err := u.client() if err != nil { return err } if err := c.Abbrechen(cmd.Context(), argumente[0]); err != nil { return err } if !u.opt.json { fmt.Fprintln(u.aus, u.stil().Amber("Abbruch angefordert – "+strings.TrimSpace(argumente[0])+" hält an der nächsten Phasengrenze.")) } return nil }, } } // 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 ", 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 }