Rename maglev-frontend → maglevd-frontend; v0.9.1; API RX/TX pulse
Rename the web dashboard binary to maglevd-frontend and move it to /usr/sbin (it's a daemon and belongs with maglevd). The systemd unit name stays vpp-maglev-frontend.service since that prefix is the package name. Manpage, README, user-guide, and debian packaging all updated in lockstep; bump to 0.9.1 for the first real release. All frontend env vars are now prefixed MAGLEV_FRONTEND_ so a single /etc/default/vpp-maglev can be shared with maglevd without collisions. Every flag has an env equivalent for Docker use. MAGLEV_FRONTEND_USER and MAGLEV_FRONTEND_PASSWORD still gate the /admin surface. VPPInfoPanel now pulses "API: ↑↓" indicators in the zippy title whenever a vpp-api-send / vpp-api-recv log event arrives on the SSE stream for the scoped maglevd — 250ms blue flash, re-triggerable, with the two arrows tightly kerned via negative letter-spacing.
This commit is contained in:
16
Makefile
16
Makefile
@@ -1,4 +1,4 @@
|
||||
BINARIES := maglevd maglevc maglev-frontend
|
||||
BINARIES := maglevd maglevc maglevd-frontend
|
||||
MODULE := git.ipng.ch/ipng/vpp-maglev
|
||||
PROTO_DIR := proto
|
||||
PROTO_FILE := $(PROTO_DIR)/maglev.proto
|
||||
@@ -15,7 +15,7 @@ FRONTEND_WEB_SRC := $(shell find cmd/frontend/web/src -type f 2>/dev/null) \
|
||||
FRONTEND_WEB_DIST := cmd/frontend/web/dist/index.html
|
||||
|
||||
NATIVE_ARCH := $(shell go env GOARCH)
|
||||
VERSION := 0.1.1
|
||||
VERSION := 0.9.1
|
||||
COMMIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \
|
||||
@@ -26,7 +26,7 @@ TEST ?= tests/
|
||||
|
||||
VPP_API_DIR ?= $(HOME)/src/vpp/build-root/install-vpp_debug-native/vpp/share/vpp/api
|
||||
|
||||
.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb robot-test clean maglev-frontend-web
|
||||
.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb robot-test clean maglevd-frontend-web
|
||||
|
||||
all: build
|
||||
|
||||
@@ -34,24 +34,24 @@ build: $(GEN_FILES) $(FRONTEND_WEB_DIST)
|
||||
mkdir -p build/$(NATIVE_ARCH)
|
||||
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd ./cmd/maglevd/
|
||||
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevc ./cmd/maglevc/
|
||||
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglev-frontend ./cmd/frontend/
|
||||
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd-frontend ./cmd/frontend/
|
||||
|
||||
build-amd64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
|
||||
mkdir -p build/amd64
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd ./cmd/maglevd/
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevc ./cmd/maglevc/
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglev-frontend ./cmd/frontend/
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd-frontend ./cmd/frontend/
|
||||
|
||||
build-arm64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
|
||||
mkdir -p build/arm64
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd ./cmd/maglevd/
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevc ./cmd/maglevc/
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglev-frontend ./cmd/frontend/
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd-frontend ./cmd/frontend/
|
||||
|
||||
# maglev-frontend-web rebuilds the SolidJS bundle. The Go binary embeds the
|
||||
# maglevd-frontend-web rebuilds the SolidJS bundle. The Go binary embeds the
|
||||
# resulting cmd/frontend/web/dist/ via //go:embed, so a `go build` after
|
||||
# this target picks up any asset changes automatically.
|
||||
maglev-frontend-web: $(FRONTEND_WEB_DIST)
|
||||
maglevd-frontend-web: $(FRONTEND_WEB_DIST)
|
||||
|
||||
$(FRONTEND_WEB_DIST): $(FRONTEND_WEB_SRC)
|
||||
cd cmd/frontend/web && npm install && npm run build
|
||||
|
||||
81
README.md
81
README.md
@@ -1,52 +1,89 @@
|
||||
# maglevd
|
||||
# vpp-maglev
|
||||
|
||||
Health checker and gRPC control plane for VPP Maglev load balancing.
|
||||
Health checker, gRPC control plane, CLI, and web dashboard for the VPP
|
||||
`lb` (load-balancer) plugin. Runs as a set of three binaries under one
|
||||
Debian package:
|
||||
|
||||
## Build and Install
|
||||
- **`maglevd`** — the long-running health-checker daemon. Probes backends
|
||||
(HTTP, TCP, ICMP), tracks their aggregate state, programs the VPP
|
||||
dataplane via the `lb` plugin binary API, and exposes everything
|
||||
over a gRPC API + Prometheus `/metrics` endpoint.
|
||||
- **`maglevc`** — the interactive CLI client. Tab-completing shell with
|
||||
inline help; also runs one-shot commands for scripting.
|
||||
- **`maglevd-frontend`** — optional web dashboard. One binary with the
|
||||
SolidJS SPA embedded via `//go:embed`; connects to one or more
|
||||
maglevds over gRPC and serves a live HTTP view (read-only `/view/`
|
||||
and optional basic-auth `/admin/`).
|
||||
|
||||
## Build and install
|
||||
|
||||
```sh
|
||||
make # builds build/<arch>/maglevd and build/<arch>/maglevc
|
||||
make # builds build/<arch>/{maglevd,maglevc,maglevd-frontend}
|
||||
make test # runs all tests
|
||||
make pkg-deb # Creates a debian package for arm64 and amd64
|
||||
make pkg-deb # creates a Debian package for amd64 and arm64
|
||||
```
|
||||
|
||||
Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go` and
|
||||
`protoc-gen-go-grpc`.
|
||||
Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go`
|
||||
and `protoc-gen-go-grpc`. The SolidJS bundle under
|
||||
`cmd/frontend/web/` is built automatically via `make` through the
|
||||
`maglevd-frontend-web` target, which needs `npm`.
|
||||
|
||||
Produces `vpp-maglev_<version>_amd64.deb` and `vpp-maglev_<version>_arm64.deb`
|
||||
in the `build/` directory by cross-compiling with `GOOS=linux GOARCH=<arch>`.
|
||||
Requires `dpkg-deb` (available on any Debian/Ubuntu host). The installed
|
||||
binaries report the exact git commit via `maglevd --version` (and
|
||||
similarly for `maglevc` / `maglev-frontend`).
|
||||
Produces `vpp-maglev_<version>_amd64.deb` and
|
||||
`vpp-maglev_<version>_arm64.deb` in the `build/` directory by
|
||||
cross-compiling with `GOOS=linux GOARCH=<arch>`. Requires `dpkg-deb`
|
||||
(available on any Debian/Ubuntu host). The installed binaries report
|
||||
the exact git commit via `maglevd --version` (and similarly for
|
||||
`maglevc` / `maglevd-frontend`).
|
||||
|
||||
## Running
|
||||
|
||||
After installing, the unit is enabled but not started automatically:
|
||||
After installing, `maglevd` is enabled automatically but
|
||||
`maglevd-frontend` is **not** — it's opt-in, so the web dashboard
|
||||
doesn't surprise anyone who just wanted the daemon:
|
||||
|
||||
```sh
|
||||
# edit /etc/vpp-maglev/maglev.yaml, then:
|
||||
systemctl enable --now vpp-maglev
|
||||
|
||||
# optional: web dashboard. Edit /etc/default/vpp-maglev to set
|
||||
# MAGLEV_FRONTEND_ARGS and (optionally) MAGLEV_FRONTEND_USER /
|
||||
# MAGLEV_FRONTEND_PASSWORD for /admin/ access, then:
|
||||
systemctl enable --now vpp-maglev-frontend
|
||||
```
|
||||
|
||||
Or run the server and client by hand:
|
||||
Or run the components by hand:
|
||||
|
||||
```sh
|
||||
maglevd --config /etc/vpp-maglev/maglev.yaml --grpc-addr :9090
|
||||
maglevd --version # print version and exit
|
||||
maglevd --version # print version and exit
|
||||
|
||||
maglevc --server localhost:9090 # interactive shell
|
||||
maglevc show frontends # one-shot
|
||||
maglevc -color=false show backends # one-shot, no ANSI color
|
||||
maglevc --server localhost:9090 # interactive shell
|
||||
maglevc show frontends # one-shot
|
||||
maglevc -color=false show backends # one-shot, no ANSI color
|
||||
maglevc set backend nginx0-ams pause
|
||||
|
||||
maglevd-frontend -server localhost:9090 -listen :8080
|
||||
```
|
||||
|
||||
Send `SIGHUP` to `maglevd` to reload config without restarting.
|
||||
`maglevd` requires `CAP_NET_RAW` for ICMP health checks.
|
||||
|
||||
Check out a minimal configuration file in [[debian/maglev.yaml](debian/maglev.yaml)].
|
||||
See [docs/user-guide.md](docs/user-guide.md) for flags, signals, and `maglevc` usage.
|
||||
See [docs/config-guide.md](docs/config-guide.md) for the full configuration reference.
|
||||
See [docs/healthchecks.md](docs/healthchecks.md) for health state machine details.
|
||||
Every flag on every binary also has an environment-variable
|
||||
equivalent (e.g. `MAGLEV_CONFIG`, `MAGLEV_GRPC_ADDR`,
|
||||
`MAGLEV_SERVERS`, `MAGLEV_LISTEN`, `MAGLEV_LOG_LEVEL`) so all three
|
||||
programs can be driven entirely via env in containerized
|
||||
deployments.
|
||||
|
||||
## Documentation
|
||||
|
||||
- A minimal configuration file in
|
||||
[debian/maglev.yaml](debian/maglev.yaml) shows every knob.
|
||||
- [docs/user-guide.md](docs/user-guide.md) — flags, signals, and
|
||||
`maglevc` command reference.
|
||||
- [docs/config-guide.md](docs/config-guide.md) — full YAML reference.
|
||||
- [docs/healthchecks.md](docs/healthchecks.md) — health state
|
||||
machine, probe scheduling, rise/fall semantics.
|
||||
- Manpages: `maglevd(8)`, `maglevc(1)`, `maglevd-frontend(8)`.
|
||||
|
||||
## Docker
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ func registerHandlers(mux *http.ServeMux, clients []*maglevClient, broker *Broke
|
||||
adminAPI := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handleAdminAPI(w, r, byName)
|
||||
})
|
||||
realm := "maglev-frontend admin"
|
||||
realm := "maglevd-frontend admin"
|
||||
// Register /admin/api/ before /admin/ so the more specific
|
||||
// pattern wins in net/http's ServeMux.
|
||||
mux.Handle("/admin/api/", basicAuth(realm, admin.User, admin.Password, adminAPI))
|
||||
|
||||
@@ -26,14 +26,18 @@ func main() {
|
||||
}
|
||||
|
||||
func run() error {
|
||||
// All env vars are prefixed with MAGLEV_FRONTEND_ so a single
|
||||
// /etc/default/vpp-maglev (or a Docker env file) can be shared
|
||||
// with maglevd without its MAGLEV_LOG_LEVEL / MAGLEV_GRPC_ADDR /
|
||||
// etc. leaking into this process's config.
|
||||
printVersion := flag.Bool("version", false, "print version and exit")
|
||||
servers := stringFlag("server", "", "MAGLEV_SERVERS", "comma-separated maglevd gRPC addresses (required)")
|
||||
listen := stringFlag("listen", ":8080", "MAGLEV_LISTEN", "HTTP listen address")
|
||||
logLevel := stringFlag("log-level", "info", "MAGLEV_LOG_LEVEL", "log verbosity (debug|info|warn|error)")
|
||||
servers := stringFlag("server", "", "MAGLEV_FRONTEND_SERVERS", "comma-separated maglevd gRPC addresses (required)")
|
||||
listen := stringFlag("listen", ":8080", "MAGLEV_FRONTEND_LISTEN", "HTTP listen address")
|
||||
logLevel := stringFlag("log-level", "info", "MAGLEV_FRONTEND_LOG_LEVEL", "log verbosity (debug|info|warn|error)")
|
||||
flag.Parse()
|
||||
|
||||
if *printVersion {
|
||||
fmt.Printf("maglev-frontend %s (commit %s, built %s)\n",
|
||||
fmt.Printf("maglevd-frontend %s (commit %s, built %s)\n",
|
||||
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type MaglevdInfo struct {
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
// VersionInfo is the build metadata of this maglev-frontend binary
|
||||
// VersionInfo is the build metadata of this maglevd-frontend binary
|
||||
// plus runtime capability flags the SPA needs to know at mount time.
|
||||
type VersionInfo struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
cmd/frontend/web/dist/assets/index-DjixLt11.js
vendored
Normal file
1
cmd/frontend/web/dist/assets/index-DjixLt11.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
cmd/frontend/web/dist/index.html
vendored
4
cmd/frontend/web/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>maglev</title>
|
||||
<script type="module" crossorigin src="/view/assets/index-C-XMkBf5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/view/assets/index-CxDuAfMR.css">
|
||||
<script type="module" crossorigin src="/view/assets/index-DjixLt11.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/view/assets/index-CExoCDXh.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -676,6 +676,45 @@
|
||||
background: var(--state-down);
|
||||
}
|
||||
|
||||
.vpp-io {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin-left: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.15em;
|
||||
padding-right: 0.15em;
|
||||
}
|
||||
.vpp-io-label {
|
||||
color: var(--fg-muted);
|
||||
font-weight: 500;
|
||||
letter-spacing: normal;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.vpp-tx,
|
||||
.vpp-rx {
|
||||
display: inline-block;
|
||||
color: var(--fg-muted);
|
||||
opacity: 0.25;
|
||||
transition:
|
||||
color 120ms ease-out,
|
||||
opacity 120ms ease-out,
|
||||
transform 120ms ease-out;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.vpp-tx.lit {
|
||||
color: var(--accent);
|
||||
opacity: 1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.vpp-rx.lit {
|
||||
color: var(--accent);
|
||||
opacity: 1;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.kv {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
|
||||
@@ -24,7 +24,7 @@ const Overview: Component = () => {
|
||||
<div class="frontend-list">
|
||||
<For each={s().frontends}>{(fe) => <FrontendCard snap={s()} frontend={fe} />}</For>
|
||||
</div>
|
||||
<VPPInfoPanel info={s().vpp_info} state={s().vpp_state} />
|
||||
<VPPInfoPanel name={s().maglevd.name} info={s().vpp_info} state={s().vpp_state} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Show, type Component } from "solid-js";
|
||||
import { Show, createEffect, createSignal, onCleanup, type Component } from "solid-js";
|
||||
import Zippy from "../components/Zippy";
|
||||
import Flash from "../components/Flash";
|
||||
import type { VPPInfoSnapshot } from "../types";
|
||||
import { events } from "../stores/events";
|
||||
import type { LogEventPayload, VPPInfoSnapshot } from "../types";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
info?: VPPInfoSnapshot;
|
||||
state?: string; // "connected" | "disconnected" | ""
|
||||
};
|
||||
@@ -15,6 +17,45 @@ const VPPInfoPanel: Component<Props> = (props) => {
|
||||
props.info?.connecttime_ns ? new Date(props.info.connecttime_ns / 1e6).toISOString() : "";
|
||||
const label = () => (props.state === "connected" ? "connected" : "disconnected");
|
||||
|
||||
// RX/TX indicators: pulse for 100ms whenever a vpp-api-send / vpp-api-recv
|
||||
// debug log arrives for this maglevd. The upstream events() signal is
|
||||
// updated one push at a time, so looking at the newest entry in an effect
|
||||
// sees every event.
|
||||
const [tx, setTx] = createSignal(false);
|
||||
const [rx, setRx] = createSignal(false);
|
||||
let txTimer: number | undefined;
|
||||
let rxTimer: number | undefined;
|
||||
const flashTx = () => {
|
||||
setTx(true);
|
||||
if (txTimer) clearTimeout(txTimer);
|
||||
txTimer = window.setTimeout(() => {
|
||||
setTx(false);
|
||||
txTimer = undefined;
|
||||
}, 250);
|
||||
};
|
||||
const flashRx = () => {
|
||||
setRx(true);
|
||||
if (rxTimer) clearTimeout(rxTimer);
|
||||
rxTimer = window.setTimeout(() => {
|
||||
setRx(false);
|
||||
rxTimer = undefined;
|
||||
}, 250);
|
||||
};
|
||||
createEffect(() => {
|
||||
const evs = events();
|
||||
if (!evs.length) return;
|
||||
const ev = evs[evs.length - 1];
|
||||
if (ev.maglevd !== props.name || ev.type !== "log") return;
|
||||
const msg = (ev.payload as LogEventPayload | undefined)?.msg;
|
||||
if (!msg) return;
|
||||
if (msg.startsWith("vpp-api-send")) flashTx();
|
||||
else if (msg.startsWith("vpp-api-recv")) flashRx();
|
||||
});
|
||||
onCleanup(() => {
|
||||
if (txTimer) clearTimeout(txTimer);
|
||||
if (rxTimer) clearTimeout(rxTimer);
|
||||
});
|
||||
|
||||
const title = (
|
||||
<span class="zippy-title">
|
||||
VPP
|
||||
@@ -23,6 +64,15 @@ const VPPInfoPanel: Component<Props> = (props) => {
|
||||
{label()}
|
||||
</span>
|
||||
</Flash>
|
||||
<span class="vpp-io" aria-hidden="true">
|
||||
<span class="vpp-io-label">API:</span>
|
||||
<span class="vpp-tx" classList={{ lit: tx() }}>
|
||||
↑
|
||||
</span>
|
||||
<span class="vpp-rx" classList={{ lit: rx() }}>
|
||||
↓
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
||||
18
debian/build-deb.sh
vendored
18
debian/build-deb.sh
vendored
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# The commit hash is baked into the binaries at link time via -ldflags
|
||||
# in the Makefile, so `maglevd --version` / `maglevc --version` /
|
||||
# `maglev-frontend --version` are the source of truth for "which
|
||||
# `maglevd-frontend --version` are the source of truth for "which
|
||||
# build". The .deb itself carries only the release version.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -28,15 +28,17 @@ install -d "$STAGING/etc/default"
|
||||
install -d "$STAGING/etc/vpp-maglev"
|
||||
install -d "$STAGING/DEBIAN"
|
||||
|
||||
# Binaries
|
||||
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevd" "$STAGING/usr/sbin/maglevd"
|
||||
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevc" "$STAGING/usr/bin/maglevc"
|
||||
install -m 755 "$REPO_ROOT/build/${ARCH}/maglev-frontend" "$STAGING/usr/bin/maglev-frontend"
|
||||
# Binaries. maglevd and maglevd-frontend are daemons and live under
|
||||
# /usr/sbin; maglevc is the interactive CLI client and lives under
|
||||
# /usr/bin so it's on every login shell's PATH.
|
||||
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevd" "$STAGING/usr/sbin/maglevd"
|
||||
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevc" "$STAGING/usr/bin/maglevc"
|
||||
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevd-frontend" "$STAGING/usr/sbin/maglevd-frontend"
|
||||
|
||||
# Man pages
|
||||
gzip -9 -c "$REPO_ROOT/docs/maglevd.8" > "$STAGING/usr/share/man/man8/maglevd.8.gz"
|
||||
gzip -9 -c "$REPO_ROOT/docs/maglevc.1" > "$STAGING/usr/share/man/man1/maglevc.1.gz"
|
||||
gzip -9 -c "$REPO_ROOT/docs/maglev-frontend.8" > "$STAGING/usr/share/man/man8/maglev-frontend.8.gz"
|
||||
gzip -9 -c "$REPO_ROOT/docs/maglevd.8" > "$STAGING/usr/share/man/man8/maglevd.8.gz"
|
||||
gzip -9 -c "$REPO_ROOT/docs/maglevc.1" > "$STAGING/usr/share/man/man1/maglevc.1.gz"
|
||||
gzip -9 -c "$REPO_ROOT/docs/maglevd-frontend.8" > "$STAGING/usr/share/man/man8/maglevd-frontend.8.gz"
|
||||
|
||||
# Systemd units
|
||||
install -m 644 "$REPO_ROOT/debian/vpp-maglev.service" "$STAGING/lib/systemd/system/vpp-maglev.service"
|
||||
|
||||
2
debian/control.in
vendored
2
debian/control.in
vendored
@@ -13,7 +13,7 @@ Description: Maglev health-checker daemon, CLI client, and web frontend
|
||||
maglevc is an interactive CLI client for maglevd with tab completion,
|
||||
inline help, and one-shot mode for scripting.
|
||||
.
|
||||
maglev-frontend is an optional web dashboard that fans one or more
|
||||
maglevd-frontend is an optional web dashboard that fans one or more
|
||||
maglevd gRPC streams out to browsers over Server-Sent Events. It is
|
||||
installed but not enabled by default; enable with:
|
||||
systemctl enable --now vpp-maglev-frontend
|
||||
|
||||
15
debian/default.vpp-maglev
vendored
15
debian/default.vpp-maglev
vendored
@@ -17,12 +17,21 @@ MAGLEV_CONFIG=/etc/vpp-maglev/maglev.yaml
|
||||
# Log level: debug, info, warn, error (default: info)
|
||||
#MAGLEV_LOG_LEVEL=info
|
||||
|
||||
# ---- maglev-frontend -------------------------------------------------------
|
||||
# ---- maglevd-frontend ------------------------------------------------------
|
||||
# The web dashboard is installed but not enabled by default. Enable with
|
||||
# systemctl enable --now vpp-maglev-frontend
|
||||
# after reviewing the arguments below.
|
||||
|
||||
# Command-line arguments passed to /usr/bin/maglev-frontend. At minimum
|
||||
# Command-line arguments passed to /usr/sbin/maglevd-frontend. At minimum
|
||||
# -server is required (comma-separated list of maglevd gRPC addresses).
|
||||
# -listen controls the HTTP bind address. See maglev-frontend(8).
|
||||
# -listen controls the HTTP bind address. See maglevd-frontend(8).
|
||||
MAGLEV_FRONTEND_ARGS="-server localhost:9090 -listen=:8080"
|
||||
|
||||
# Basic-auth credentials for the /admin/ surface. When both are set to
|
||||
# non-empty values, /admin/ is reachable and the SPA exposes backend
|
||||
# lifecycle mutations (pause/resume/enable/disable/set-weight). When
|
||||
# either is missing or empty, /admin/ returns 404 and the SPA hides
|
||||
# the admin toggle entirely. Leave commented out for a read-only
|
||||
# deployment.
|
||||
#MAGLEV_FRONTEND_USER=admin
|
||||
#MAGLEV_FRONTEND_PASSWORD=changeme
|
||||
|
||||
7
debian/vpp-maglev-frontend.service
vendored
7
debian/vpp-maglev-frontend.service
vendored
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=Maglev web frontend dashboard
|
||||
Documentation=man:maglev-frontend(8)
|
||||
Documentation=man:maglevd-frontend(8)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -8,12 +8,13 @@ Wants=network-online.target
|
||||
User=maglevd
|
||||
Group=maglevd
|
||||
EnvironmentFile=/etc/default/vpp-maglev
|
||||
ExecStart=/usr/bin/maglev-frontend $MAGLEV_FRONTEND_ARGS
|
||||
ExecStart=/usr/sbin/maglevd-frontend $MAGLEV_FRONTEND_ARGS
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
Type=simple
|
||||
|
||||
# Read-only presentation layer — needs no capabilities.
|
||||
# Presentation layer — needs no capabilities. /admin/ mutations go
|
||||
# through the gRPC client to maglevd, which does the privileged work.
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
|
||||
@@ -91,6 +91,7 @@ command, with examples and operational notes — see the user guide at:
|
||||
https://git.ipng.ch/ipng/vpp-maglev/docs/user-guide.md
|
||||
.RE
|
||||
.SH SEE ALSO
|
||||
.BR maglevd (8)
|
||||
.BR maglevd (8),
|
||||
.BR maglevd\-frontend (8)
|
||||
.SH AUTHOR
|
||||
Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
.TH MAGLEV\-FRONTEND 8 "April 2026" "vpp\-maglev" "System Administration"
|
||||
.TH MAGLEVD\-FRONTEND 8 "April 2026" "vpp\-maglev" "System Administration"
|
||||
.SH NAME
|
||||
maglev\-frontend \- web dashboard for one or more running maglevd instances
|
||||
maglevd\-frontend \- web dashboard for one or more running maglevd instances
|
||||
.SH SYNOPSIS
|
||||
.B maglev\-frontend
|
||||
.B maglevd\-frontend
|
||||
\fB\-server\fR \fIaddr\fR[,\fIaddr\fR...]
|
||||
[\fB\-listen\fR \fIaddr\fR]
|
||||
[\fB\-log\-level\fR \fIlevel\fR]
|
||||
[\fB\-version\fR]
|
||||
.SH DESCRIPTION
|
||||
.B maglev\-frontend
|
||||
.B maglevd\-frontend
|
||||
is a single\-binary web dashboard that connects to one or more running
|
||||
.BR maglevd (8)
|
||||
instances over gRPC and renders a live view of frontends, backends,
|
||||
@@ -21,7 +21,7 @@ one or more maglevds with
|
||||
is enough to serve the dashboard.
|
||||
.PP
|
||||
For each configured maglevd,
|
||||
.B maglev\-frontend
|
||||
.B maglevd\-frontend
|
||||
maintains:
|
||||
.IP \(bu 2
|
||||
A long\-lived
|
||||
@@ -45,16 +45,36 @@ A 5\-second health probe that surfaces maglevd connection drops
|
||||
quickly and flips the scope\-selector indicator dot red.
|
||||
.PP
|
||||
Browsers connect to
|
||||
.B maglev\-frontend
|
||||
.B maglevd\-frontend
|
||||
over HTTP. State is hydrated once via REST and then kept live via a
|
||||
Server\-Sent Events stream. Short SSE disconnects (nginx idle timeout,
|
||||
wifi flap, laptop wake) are handled silently via a 30\-second replay
|
||||
ring buffer; longer outages fall through to a full refetch. The SPA
|
||||
is stateless on reload so refreshing the page at any time returns a
|
||||
consistent view.
|
||||
.PP
|
||||
The frontend exposes two base paths:
|
||||
.B /view/
|
||||
is the read\-only dashboard and serves without authentication;
|
||||
.B /admin/
|
||||
is a basic\-auth\-protected variant of the same SPA that exposes
|
||||
lifecycle mutations (pause / resume / enable / disable a backend,
|
||||
set configured weight within a pool). The admin surface is only
|
||||
mounted when both
|
||||
.B MAGLEV_FRONTEND_USER
|
||||
and
|
||||
.B MAGLEV_FRONTEND_PASSWORD
|
||||
are set to non\-empty values at startup; otherwise
|
||||
.B /admin/
|
||||
returns 404 and the SPA hides the admin\-toggle button entirely.
|
||||
.SH OPTIONS
|
||||
Each flag may also be supplied via an environment variable (shown in
|
||||
parentheses); the flag takes precedence.
|
||||
parentheses); the flag takes precedence when both are set. All env
|
||||
vars are prefixed with
|
||||
.B MAGLEV_FRONTEND_
|
||||
so a single env file can be shared with
|
||||
.BR maglevd (8)
|
||||
without variables leaking across processes.
|
||||
.TP
|
||||
.BI \-server " addr[,addr...]"
|
||||
Comma\-separated list of maglevd gRPC addresses. Required. Each
|
||||
@@ -62,11 +82,11 @@ entry is in
|
||||
.I host:port
|
||||
form; a short display name is derived from the hostname label (for
|
||||
IP literals the full address is used).
|
||||
.RI "(env: " MAGLEV_SERVERS )
|
||||
.RI "(env: " MAGLEV_FRONTEND_SERVERS )
|
||||
.TP
|
||||
.BI \-listen " addr"
|
||||
HTTP bind address for the dashboard.
|
||||
.RI "(default: " :8080 "; env: " MAGLEV_LISTEN )
|
||||
.RI "(default: " :8080 "; env: " MAGLEV_FRONTEND_LISTEN )
|
||||
.TP
|
||||
.BI \-log\-level " level"
|
||||
Structured\-log verbosity:
|
||||
@@ -76,19 +96,19 @@ Structured\-log verbosity:
|
||||
or
|
||||
.BR error .
|
||||
Affects
|
||||
.B maglev\-frontend 's
|
||||
.B maglevd\-frontend 's
|
||||
own logs, not the log level it subscribes to on the upstream maglevd
|
||||
(which is always
|
||||
.BR debug
|
||||
so the probe heartbeat can animate).
|
||||
.RI "(default: " info "; env: " MAGLEV_LOG_LEVEL )
|
||||
.RI "(default: " info "; env: " MAGLEV_FRONTEND_LOG_LEVEL )
|
||||
.TP
|
||||
.B \-version
|
||||
Print version, commit hash, and build date, then exit.
|
||||
.SH HTTP ENDPOINTS
|
||||
.TP
|
||||
.I /view/
|
||||
Static SPA (HTML, JS, CSS, assets).
|
||||
Static SPA (HTML, JS, CSS, assets). Read\-only.
|
||||
.TP
|
||||
.I /view/api/maglevds
|
||||
JSON array describing the configured maglevds and their current
|
||||
@@ -101,22 +121,39 @@ Full JSON state snapshot for every maglevd.
|
||||
Full JSON state snapshot for a single maglevd.
|
||||
.TP
|
||||
.I /view/api/version
|
||||
Build version, commit hash, and build date.
|
||||
Build version, commit hash, and build date, plus an
|
||||
.B admin_enabled
|
||||
flag the SPA uses to decide whether to show the admin toggle.
|
||||
.TP
|
||||
.I /view/api/events
|
||||
Server\-Sent Events stream. Long\-lived HTTP/1.1 chunked response
|
||||
fanning out log, backend, frontend, maglevd\-status, and vpp\-status
|
||||
events to every connected browser. Supports
|
||||
.B Last\-Event\-ID
|
||||
replay from a 30\-second / 2000\-event ring buffer.
|
||||
replay from a 30\-second / 2000\-event ring buffer, plus a
|
||||
.B resync
|
||||
control event emitted after every maglevd config reload so the SPA
|
||||
re\-hydrates from the now\-fresh server cache.
|
||||
.TP
|
||||
.I /healthz
|
||||
Liveness endpoint; returns 200 if the HTTP server is up.
|
||||
.TP
|
||||
.I /admin/
|
||||
Placeholder for a future basic\-auth mutation surface. Currently
|
||||
returns
|
||||
.B 501 Not Implemented .
|
||||
SPA shell served behind basic auth when
|
||||
.B MAGLEV_FRONTEND_USER
|
||||
and
|
||||
.B MAGLEV_FRONTEND_PASSWORD
|
||||
are configured. Returns 404 when they're not.
|
||||
.TP
|
||||
.I "/admin/api/{maglevd}/backend/{name}/{action}"
|
||||
Backend lifecycle POST. Action is
|
||||
.BR pause ", " resume ", " enable ", or " disable .
|
||||
Returns the fresh backend snapshot as JSON.
|
||||
.TP
|
||||
.I "/admin/api/{maglevd}/frontend/{fe}/pool/{pool}/backend/{name}/weight"
|
||||
Weight change POST. Body is
|
||||
.B {"weight": 0\-100, "flush": bool} .
|
||||
Returns the fresh frontend snapshot as JSON.
|
||||
.SH REVERSE PROXY NOTES
|
||||
The SSE stream has a handful of operational requirements that every
|
||||
reverse proxy must satisfy:
|
||||
@@ -124,7 +161,7 @@ reverse proxy must satisfy:
|
||||
Disable buffering on the events endpoint. Nginx honours
|
||||
.B X\-Accel\-Buffering: no
|
||||
(sent by
|
||||
.BR maglev\-frontend )
|
||||
.BR maglevd\-frontend )
|
||||
but a global
|
||||
.B proxy_buffering off;
|
||||
in the server block is the more robust answer.
|
||||
@@ -136,36 +173,72 @@ to at least
|
||||
so the stream isn't torn down between the 15\-second
|
||||
.B :\ ping
|
||||
heartbeats that
|
||||
.B maglev\-frontend
|
||||
.B maglevd\-frontend
|
||||
sends.
|
||||
.IP \(bu 2
|
||||
Do not wrap the events endpoint in a gzip/brotli middleware — response
|
||||
compression buffers until its window fills and destroys the live\-stream
|
||||
property.
|
||||
.SH ENVIRONMENT
|
||||
All environment variables are prefixed with
|
||||
.B MAGLEV_FRONTEND_
|
||||
so this daemon can share
|
||||
.I /etc/default/vpp-maglev
|
||||
(or a container env file) with
|
||||
.BR maglevd (8)
|
||||
— whose env vars use only the shorter
|
||||
.B MAGLEV_
|
||||
prefix — without cross\-contamination.
|
||||
.TP
|
||||
.B MAGLEV_SERVERS
|
||||
.B MAGLEV_FRONTEND_SERVERS
|
||||
Default value of
|
||||
.BR \-server .
|
||||
.TP
|
||||
.B MAGLEV_LISTEN
|
||||
.B MAGLEV_FRONTEND_LISTEN
|
||||
Default value of
|
||||
.BR \-listen .
|
||||
.TP
|
||||
.B MAGLEV_LOG_LEVEL
|
||||
.B MAGLEV_FRONTEND_LOG_LEVEL
|
||||
Default value of
|
||||
.BR \-log\-level .
|
||||
.TP
|
||||
.B MAGLEV_FRONTEND_USER
|
||||
HTTP basic\-auth username for
|
||||
.BR /admin/ .
|
||||
When set together with
|
||||
.B MAGLEV_FRONTEND_PASSWORD
|
||||
the admin surface is enabled; when either is missing or empty the
|
||||
admin surface is hidden entirely (the SPA doesn't render the admin
|
||||
toggle button and
|
||||
.B /admin/
|
||||
itself returns 404).
|
||||
.TP
|
||||
.B MAGLEV_FRONTEND_PASSWORD
|
||||
HTTP basic\-auth password for
|
||||
.BR /admin/ .
|
||||
See
|
||||
.B MAGLEV_FRONTEND_USER
|
||||
above.
|
||||
.TP
|
||||
.B MAGLEV_FRONTEND_ARGS
|
||||
Extra command\-line arguments picked up by the systemd unit's
|
||||
.B ExecStart
|
||||
line. Not read directly by the process — the unit expands it before
|
||||
exec\-ing the binary.
|
||||
.SH FILES
|
||||
.TP
|
||||
.I /etc/default/vpp-maglev
|
||||
Environment file sourced by the systemd unit before starting
|
||||
.BR maglev\-frontend .
|
||||
.BR maglevd\-frontend .
|
||||
The same file is shared with
|
||||
.BR maglevd (8);
|
||||
the
|
||||
.B MAGLEV_FRONTEND_ARGS
|
||||
variable there is passed on the command line to
|
||||
.B maglev\-frontend .
|
||||
.BR maglevd\-frontend ,
|
||||
and
|
||||
.B MAGLEV_FRONTEND_USER / MAGLEV_FRONTEND_PASSWORD
|
||||
are read from the process environment.
|
||||
.SH SEE ALSO
|
||||
.BR maglevd (8),
|
||||
.BR maglevc (1)
|
||||
@@ -130,6 +130,7 @@ https://git.ipng.ch/ipng/vpp-maglev/docs/user-guide.md
|
||||
.RE
|
||||
.SH SEE ALSO
|
||||
.BR maglevc (1),
|
||||
.BR maglevd\-frontend (8),
|
||||
.BR vpp (8)
|
||||
.SH AUTHOR
|
||||
Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
@@ -241,3 +241,83 @@ keyword. The `?` character is not added to the input line.
|
||||
|
||||
Commands and keywords support **prefix matching**: typing `sh ba` is equivalent
|
||||
to `show backends`, and `sh ba nginx0` is equivalent to `show backends nginx0`.
|
||||
|
||||
---
|
||||
|
||||
## maglevd-frontend
|
||||
|
||||
`maglevd-frontend` is an optional web dashboard that connects to one or
|
||||
more running `maglevd` instances over gRPC and renders a live view of
|
||||
frontends, backends, health checks, and VPP load-balancer state. It is
|
||||
a single Go binary with the SolidJS SPA embedded via `//go:embed`; no
|
||||
runtime file dependencies.
|
||||
|
||||
Installed by the Debian package to `/usr/sbin/maglevd-frontend` but
|
||||
**not** enabled by default — the operator opts in via:
|
||||
|
||||
```sh
|
||||
systemctl enable --now vpp-maglev-frontend
|
||||
```
|
||||
|
||||
The systemd unit (`vpp-maglev-frontend.service`) reads its arguments
|
||||
from `/etc/default/vpp-maglev` via `MAGLEV_FRONTEND_ARGS`. The same
|
||||
env file is shared with `maglevd`; all `maglevd-frontend`-specific
|
||||
variables are prefixed with `MAGLEV_FRONTEND_` so there's no overlap.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Environment variable | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `--server` | `MAGLEV_FRONTEND_SERVERS` | *(required)* | Comma-separated list of `host:port` maglevd addresses. |
|
||||
| `--listen` | `MAGLEV_FRONTEND_LISTEN` | `:8080` | HTTP bind address. |
|
||||
| `--log-level` | `MAGLEV_FRONTEND_LOG_LEVEL` | `info` | Structured-log verbosity for `maglevd-frontend`'s own logs. |
|
||||
| `--version` | — | — | Print version, commit hash, and build date, then exit. |
|
||||
|
||||
In addition to flags, two env-only variables control the admin surface:
|
||||
|
||||
| Environment variable | Purpose |
|
||||
|---|---|
|
||||
| `MAGLEV_FRONTEND_USER` | HTTP basic-auth username for `/admin/`. |
|
||||
| `MAGLEV_FRONTEND_PASSWORD` | HTTP basic-auth password for `/admin/`. |
|
||||
|
||||
When **both** are set and non-empty the admin surface is mounted and
|
||||
the SPA's "admin…" toggle becomes visible. When either is missing or
|
||||
empty the `/admin/` route returns 404 and the SPA hides the toggle —
|
||||
`/view/` is always reachable read-only.
|
||||
|
||||
### HTTP surface
|
||||
|
||||
- **`/view/`** — static SPA (dashboard). No authentication.
|
||||
- **`/view/api/state`**, **`/view/api/state/{name}`** — full JSON
|
||||
snapshot for every maglevd, or one maglevd.
|
||||
- **`/view/api/maglevds`** — configured maglevds and connection status.
|
||||
- **`/view/api/version`** — build info + `admin_enabled` flag.
|
||||
- **`/view/api/events`** — Server-Sent Events stream; log, backend,
|
||||
frontend, maglevd-status, vpp-status events with
|
||||
`Last-Event-ID` replay from a 30-second / 2000-event ring buffer.
|
||||
- **`/healthz`** — liveness; returns 200 if the HTTP server is up.
|
||||
- **`/admin/`** — SPA shell behind basic auth (when configured).
|
||||
- **`POST /admin/api/{maglevd}/backend/{name}/{action}`** — backend
|
||||
lifecycle action. `action` is `pause`, `resume`, `enable`, or
|
||||
`disable`. Returns the fresh backend snapshot as JSON.
|
||||
- **`POST /admin/api/{maglevd}/frontend/{fe}/pool/{pool}/backend/{name}/weight`**
|
||||
— weight change. Body: `{"weight": 0-100, "flush": bool}`. When
|
||||
`flush=true`, VPP's flow table for the backend is cleared;
|
||||
otherwise only the new-buckets map is updated and existing
|
||||
sessions keep reaching the backend until they finish.
|
||||
|
||||
### Reverse-proxy requirements (SSE)
|
||||
|
||||
Nginx, HAProxy, or any proxy in front of `maglevd-frontend` must:
|
||||
|
||||
- Disable buffering on the events endpoint. `X-Accel-Buffering: no`
|
||||
is sent by the server; a global `proxy_buffering off;` in the
|
||||
nginx server block is the more robust answer.
|
||||
- Raise `proxy_read_timeout` to at least 300s so the stream isn't
|
||||
torn down between the 15-second `: ping` heartbeats the server
|
||||
sends.
|
||||
- Not wrap the events endpoint in any gzip/brotli middleware —
|
||||
response compression buffers until its window fills and destroys
|
||||
the live-stream property.
|
||||
|
||||
See `maglevd-frontend(8)` for the full reference.
|
||||
|
||||
Reference in New Issue
Block a user