cli QoL: vm prune, list→ls aliases, delete→rm aliases
- `banger vm prune` sweeps every non-running VM (stopped, created, error) with an interactive confirmation; -f/--force skips the prompt. Partial failures report which VM failed and exit non-zero. - list commands gain `ls` alias: vm list already had it; added to image list, kernel list, and vm session list. - delete commands gain `rm` alias: vm delete and image delete. kernel rm already aliased delete/remove. Uses new test seams (vmListFunc) plus the existing vmDeleteFunc so prune unit-tests without touching the daemon socket.
This commit is contained in:
parent
e3eaa0c797
commit
221fb03d68
4 changed files with 430 additions and 8 deletions
|
|
@ -2,6 +2,7 @@ package cli
|
|||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
|
|
@ -80,6 +81,9 @@ var (
|
|||
_, err := rpc.Call[api.VMShowResult](ctx, socketPath, "vm.delete", api.VMRefParams{IDOrName: idOrName})
|
||||
return err
|
||||
}
|
||||
vmListFunc = func(ctx context.Context, socketPath string) (api.VMListResult, error) {
|
||||
return rpc.Call[api.VMListResult](ctx, socketPath, "vm.list", api.Empty{})
|
||||
}
|
||||
daemonPingFunc = func(ctx context.Context, socketPath string) (api.PingResult, error) {
|
||||
return rpc.Call[api.PingResult](ctx, socketPath, "ping", api.Empty{})
|
||||
}
|
||||
|
|
@ -732,7 +736,8 @@ func newVMCommand() *cobra.Command {
|
|||
newVMActionCommand("stop", "Stop a VM", "vm.stop"),
|
||||
newVMKillCommand(),
|
||||
newVMActionCommand("restart", "Restart a VM", "vm.restart"),
|
||||
newVMActionCommand("delete", "Delete a VM", "vm.delete"),
|
||||
newVMActionCommand("delete", "Delete a VM", "vm.delete", "rm"),
|
||||
newVMPruneCommand(),
|
||||
newVMSetCommand(),
|
||||
newVMSSHCommand(),
|
||||
newVMWorkspaceCommand(),
|
||||
|
|
@ -894,6 +899,104 @@ func newVMKillCommand() *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func newVMPruneCommand() *cobra.Command {
|
||||
var force bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "prune",
|
||||
Short: "Delete every VM that isn't running",
|
||||
Long: "Scan for VMs in state other than 'running' (stopped, created, error) and delete them after confirmation. Use -f to skip the prompt.",
|
||||
Args: noArgsUsage("usage: banger vm prune [-f|--force]"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := system.EnsureSudo(cmd.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runVMPrune(cmd, layout.SocketPath, force)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "skip the confirmation prompt")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runVMPrune(cmd *cobra.Command, socketPath string, force bool) error {
|
||||
ctx := cmd.Context()
|
||||
stdout := cmd.OutOrStdout()
|
||||
stderr := cmd.ErrOrStderr()
|
||||
|
||||
list, err := vmListFunc(ctx, socketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var victims []model.VMRecord
|
||||
for _, vm := range list.VMs {
|
||||
if vm.State != model.VMStateRunning {
|
||||
victims = append(victims, vm)
|
||||
}
|
||||
}
|
||||
if len(victims) == 0 {
|
||||
_, err := fmt.Fprintln(stdout, "no non-running VMs to prune")
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(stdout, "The following %d VM(s) will be deleted:\n", len(victims))
|
||||
w := tabwriter.NewWriter(stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, " ID\tNAME\tSTATE")
|
||||
for _, vm := range victims {
|
||||
fmt.Fprintf(w, " %s\t%s\t%s\n", shortID(vm.ID), vm.Name, vm.State)
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !force {
|
||||
ok, err := promptYesNo(cmd.InOrStdin(), stdout, "Delete these VMs? [y/N] ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
_, err := fmt.Fprintln(stdout, "aborted")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var failed int
|
||||
for _, vm := range victims {
|
||||
ref := vm.Name
|
||||
if ref == "" {
|
||||
ref = shortID(vm.ID)
|
||||
}
|
||||
if err := vmDeleteFunc(ctx, socketPath, vm.ID); err != nil {
|
||||
fmt.Fprintf(stderr, "delete %s: %v\n", ref, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(stdout, "deleted", ref)
|
||||
}
|
||||
if failed > 0 {
|
||||
return fmt.Errorf("%d VM(s) failed to delete", failed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// promptYesNo reads a line from in and returns true iff the trimmed
|
||||
// lowercase answer is "y" or "yes". EOF is treated as "no". Any other
|
||||
// read error is surfaced to the caller.
|
||||
func promptYesNo(in io.Reader, out io.Writer, prompt string) (bool, error) {
|
||||
if _, err := fmt.Fprint(out, prompt); err != nil {
|
||||
return false, err
|
||||
}
|
||||
reader := bufio.NewReader(in)
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return false, err
|
||||
}
|
||||
answer := strings.ToLower(strings.TrimSpace(line))
|
||||
return answer == "y" || answer == "yes", nil
|
||||
}
|
||||
|
||||
func newVMCreateCommand() *cobra.Command {
|
||||
var (
|
||||
name string
|
||||
|
|
@ -1035,9 +1138,10 @@ func newVMShowCommand() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
func newVMActionCommand(use, short, method string) *cobra.Command {
|
||||
func newVMActionCommand(use, short, method string, aliases ...string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: use + " <id-or-name>...",
|
||||
Aliases: aliases,
|
||||
Short: short,
|
||||
Args: minArgsUsage(1, fmt.Sprintf("usage: banger vm %s <id-or-name>...", use)),
|
||||
ValidArgsFunction: completeVMNames,
|
||||
|
|
@ -1351,6 +1455,7 @@ func newVMSessionStartCommand() *cobra.Command {
|
|||
func newVMSessionListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list <id-or-name>",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List managed guest commands for a VM",
|
||||
Args: exactArgsUsage(1, "usage: banger vm session list <id-or-name>"),
|
||||
ValidArgsFunction: completeVMNameOnlyAtPos0,
|
||||
|
|
@ -1862,9 +1967,10 @@ func newImagePromoteCommand() *cobra.Command {
|
|||
|
||||
func newImageListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List images",
|
||||
Args: noArgsUsage("usage: banger image list"),
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List images",
|
||||
Args: noArgsUsage("usage: banger image list"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
|
|
@ -1902,6 +2008,7 @@ func newImageShowCommand() *cobra.Command {
|
|||
func newImageDeleteCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete <id-or-name>",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Delete an image",
|
||||
Args: exactArgsUsage(1, "usage: banger image delete <id-or-name>"),
|
||||
ValidArgsFunction: completeImageNameOnlyAtPos0,
|
||||
|
|
@ -2002,9 +2109,10 @@ func newKernelImportCommand() *cobra.Command {
|
|||
func newKernelListCommand() *cobra.Command {
|
||||
var available bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List kernels (local by default, or --available for the catalog)",
|
||||
Args: noArgsUsage("usage: banger kernel list [--available]"),
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List kernels (local by default, or --available for the catalog)",
|
||||
Args: noArgsUsage("usage: banger kernel list [--available]"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue