Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
143 lines
3.6 KiB
Go
143 lines
3.6 KiB
Go
// Command nmon-devtools holds developer helpers for the gates and for screenshots: backfill (synthetic
|
|
// history into the hot store) and seed-demo (a simulated network added to a running nmon). It talks to nmon
|
|
// only through its HTTP API and to the hot store through --hot-url, and is never shipped (docs/dev.md).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
var errUsage = errors.New("usage: nmon-devtools backfill|seed-demo|topology-agents|topology-add|webhook-sink|gate-dump|" +
|
|
"proxmox-mock|push-replay|backfill-long|oidc-mock|bcrypt [flags]" +
|
|
" (see docs/dev.md)")
|
|
|
|
func main() {
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
if err := run(ctx, os.Args[1:]); err != nil {
|
|
fmt.Fprintln(os.Stderr, "nmon-devtools:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run(ctx context.Context, args []string) error {
|
|
if len(args) == 0 {
|
|
return errUsage
|
|
}
|
|
switch args[0] {
|
|
case "backfill":
|
|
return backfillCmd(ctx, args[1:])
|
|
case "seed-demo":
|
|
return seedDemoCmd(ctx, args[1:])
|
|
case "topology-agents":
|
|
return topologyAgentsCmd(ctx, args[1:])
|
|
case "topology-add":
|
|
return topologyAddCmd(ctx, args[1:])
|
|
case "webhook-sink":
|
|
return webhookSinkCmd(ctx, args[1:])
|
|
case "gate-dump":
|
|
return gateDumpCmd(ctx, args[1:])
|
|
case "proxmox-mock":
|
|
return proxmoxMockCmd(ctx, args[1:])
|
|
case "push-replay":
|
|
return pushReplayCmd(ctx, args[1:])
|
|
case "backfill-long":
|
|
return backfillLongCmd(ctx, args[1:])
|
|
case "bcrypt":
|
|
return bcryptCmd(ctx, args[1:])
|
|
case "oidc-mock":
|
|
return oidcMockCmd(ctx, args[1:])
|
|
default:
|
|
return fmt.Errorf("unknown command %q: %w", args[0], errUsage)
|
|
}
|
|
}
|
|
|
|
// flagErr turns flag.ErrHelp into a clean exit.
|
|
func flagErr(err error) error {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
os.Exit(0)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// stringList is a repeatable string flag.
|
|
type stringList []string
|
|
|
|
func (s *stringList) String() string { return strings.Join(*s, ",") }
|
|
|
|
func (s *stringList) Set(v string) error {
|
|
for _, p := range strings.Split(v, ",") {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
*s = append(*s, p)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// devFlagSet is a FlagSet that prints its own usage to stderr.
|
|
func devFlagSet(name string) *flag.FlagSet {
|
|
fs := flag.NewFlagSet("nmon-devtools "+name, flag.ContinueOnError)
|
|
fs.SetOutput(os.Stderr)
|
|
return fs
|
|
}
|
|
|
|
// httpJSON sends an optional JSON body (with the API token in NMON_TOKEN, if set) and decodes a 2xx reply into out (nil: discard).
|
|
func httpJSON(ctx context.Context, hc *http.Client, method, url string, in, out any) error {
|
|
var body io.Reader
|
|
if in != nil {
|
|
b, err := json.Marshal(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = strings.NewReader(string(b))
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if in != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
if tok := os.Getenv("NMON_TOKEN"); tok != "" { // an nmon with authentication on needs an admin API token
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
}
|
|
resp, err := hc.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }() // body read to the end below
|
|
data, err := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode/100 != 2 {
|
|
return fmt.Errorf("%s %s: %s: %s", method, url, resp.Status, strings.TrimSpace(string(data[:min(len(data), 300)])))
|
|
}
|
|
if out == nil {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(data, out)
|
|
}
|
|
|
|
func sleepCtx(ctx context.Context, d time.Duration) bool {
|
|
t := time.NewTimer(d)
|
|
defer t.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-t.C:
|
|
return true
|
|
}
|
|
}
|