023 Project 23: Proxmox Batch Operations CLI

023 Build a Proxmox Batch CLI

Operate many VMs in one command (start/stop/reboot) using Proxmox API token auth.

parse vmid list -> concurrent API calls -> result summary

Full main.go

package main

import (
    "crypto/tls"
    "flag"
    "fmt"
    "net/http"
    "os"
    "strconv"
    "strings"
    "sync"
    "time"
)

func action(base, token, node string, vmid int, op string) error {
    hc := &http.Client{Timeout: 8 * time.Second, Transport: &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }}
    url := fmt.Sprintf("%s/api2/json/nodes/%s/qemu/%d/status/%s", base, node, vmid, op)
    req, _ := http.NewRequest(http.MethodPost, url, nil)
    req.Header.Set("Authorization", "PVEAPIToken="+token)
    resp, err := hc.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if resp.StatusCode >= 300 {
        return fmt.Errorf("status %d", resp.StatusCode)
    }
    return nil
}

func main() {
    base := flag.String("base", os.Getenv("PVE_BASE_URL"), "proxmox base url")
    token := flag.String("token", os.Getenv("PVE_TOKEN"), "api token")
    node := flag.String("node", "pve", "node name")
    op := flag.String("op", "start", "start|stop|reboot")
    ids := flag.String("ids", "", "comma list vmids")
    workers := flag.Int("w", 8, "parallel workers")
    flag.Parse()

    if *base == "" || *token == "" || *ids == "" {
        fmt.Println("need -base, -token, -ids")
        os.Exit(2)
    }

    jobs := make(chan int)
    var wg sync.WaitGroup
    for i := 0; i < *workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for id := range jobs {
                err := action(*base, *token, *node, id, *op)
                if err != nil {
                    fmt.Printf("vm %d fail: %v\n", id, err)
                } else {
                    fmt.Printf("vm %d ok\n", id)
                }
            }
        }()
    }

    for _, s := range strings.Split(*ids, ",") {
        id, err := strconv.Atoi(strings.TrimSpace(s))
        if err == nil {
            jobs <- id
        }
    }
    close(jobs)
    wg.Wait()
}

Run

go run . -node pve -op start -ids 101,102,103

Step-by-Step Explanation

  1. Model jobs, workers, and outputs explicitly.
  2. Bound concurrency using worker pools and buffered channels.
  3. Use sync.WaitGroup for lifecycle control.
  4. Aggregate worker results in one place.
  5. Verify behavior under both normal and failure paths.

Code Anatomy

  • Producer pushes jobs into a channel.
  • Workers consume jobs and emit results.
  • Aggregator merges results and prints summary.

Learning Goals

  • Build leak-free goroutine patterns.
  • Balance throughput and resource limits.
  • Understand fan-out/fan-in architecture.