Files
nmon/cmd/nmon-devtools/main.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
}
}