diff --git a/install.sh b/install.sh
index b354b703..5c28bd7d 100755
--- a/install.sh
+++ b/install.sh
@@ -20,7 +20,9 @@ LOG_FILE="/var/log/monitor-agent.log"
SERVER=""
TOKEN=""
REGISTER=""
-INTERVAL=1
+IFACE=""
+IFACE_SET=""
+INTERVAL=""
INSECURE=""
UNINSTALL=""
@@ -28,13 +30,14 @@ while [ $# -gt 0 ]; do
# A flag with no argument: under set -u, `$2` aborts with the shell's own
# message rather than the usage below, and `shift 2` cannot proceed.
case "$1" in
- --server | --token | --register | --interval)
+ --server | --token | --register | --iface | --interval)
[ $# -ge 2 ] || { echo "$1 needs a value" >&2; exit 2; } ;;
esac
case "$1" in
--server) SERVER="$2"; shift 2 ;;
--token) TOKEN="$2"; shift 2 ;;
--register) REGISTER="$2"; shift 2 ;;
+ --iface) IFACE="$2"; IFACE_SET=1; shift 2 ;;
--interval) INTERVAL="$2"; shift 2 ;;
--insecure) INSECURE=1; shift ;;
--uninstall) UNINSTALL=1; shift ;;
@@ -63,12 +66,44 @@ if [ -n "$UNINSTALL" ]; then
fi
[ -n "$SERVER" ] && { [ -n "$TOKEN" ] || [ -n "$REGISTER" ]; } || {
- echo "usage: install.sh --server URL (--token TOKEN | --register KEY) [--interval SECONDS] [--insecure]" >&2
+ echo "usage: install.sh --server URL (--token TOKEN | --register KEY) [--interval SECONDS] [--iface LIST] [--insecure]" >&2
echo " install.sh --uninstall" >&2
exit 2
}
+# A setting of this machine, kept by a rerun without the flag for the reason
+# given for --iface below: the batch command carries none. It is read back from
+# the service definition the last install wrote; a first install takes 1.
+if [ -z "$INTERVAL" ]; then
+ INTERVAL=$(cat "$UNIT_FILE" "$RC_FILE" 2>/dev/null | sed -n \
+ -e 's/^ExecStart=.* --interval \([0-9][0-9]*\).*/\1/p' \
+ -e 's/^command_args="--interval \([0-9][0-9]*\).*/\1/p' | tail -n 1)
+ if [ -n "$INTERVAL" ]; then echo "keeping --interval $INTERVAL from the previous install"; else INTERVAL=1; fi
+fi
case "$INTERVAL" in "" | *[!0-9]*) echo "interval must be an integer from 1 to 3600" >&2; exit 2 ;; esac
[ "$INTERVAL" -ge 1 ] && [ "$INTERVAL" -le 3600 ] || { echo "interval must be from 1 to 3600" >&2; exit 2; }
+# Which interfaces carry this machine's traffic is known only on the machine,
+# and the batch command a fleet shares cannot carry one value per machine. A
+# rerun without --iface, the documented upgrade, therefore keeps the value in
+# the env file; --iface '' clears it.
+#
+# A kept value is written back as found, the last assignment being the one
+# systemd and OpenRC apply: root wrote it, both already read it, and a hand edit
+# with quotes must not block every later upgrade. A value given here is held to
+# what the agent accepts -- full names separated by commas, each optionally led
+# by one `-` -- since the agent refuses anything else at startup and would
+# restart forever while this script reported success. The character set also
+# keeps it inert where OpenRC sources the file as shell.
+if [ -z "$IFACE_SET" ]; then
+ IFACE=$(sed -n 's/^MONITOR_IFACE=//p' "$ENV_FILE" 2>/dev/null | tail -n 1)
+ [ -z "$IFACE" ] || echo "keeping --iface $IFACE from the previous install"
+else
+ case ",$IFACE," in
+ *[!A-Za-z0-9._,-]* | *,-,* | *,--*)
+ echo "--iface takes full interface names separated by commas, each optionally led by -, not: $IFACE" >&2
+ exit 2
+ ;;
+ esac
+fi
# A bare host implies TLS, matching the upgrade the agent's ws_url() performs,
# and the same reversal under --insecure where the hub has no TLS to upgrade to.
# Without this the two diverge: the agent would dial wss:// while curl below
@@ -233,6 +268,7 @@ install -m 0755 "$TMP" "$BIN"
MONITOR_SERVER=$SERVER
MONITOR_TOKEN=$TOKEN
ENV
+ [ -z "$IFACE" ] || printf 'MONITOR_IFACE=%s\n' "$IFACE" >>"$ENV_FILE"
)
if [ "$INIT" = openrc ]; then
diff --git a/src/db.rs b/src/db.rs
index bd816fca..4a85ce8b 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -882,9 +882,10 @@ impl Db {
/// Folds one report's raw kernel counters into the node's running totals.
///
- /// A changed boot_id, or a counter that moved backwards, means the kernel
- /// restarted its counting; the total must not follow it downward. `None`
- /// denotes a report carrying no readable counters at all -- see below.
+ /// A changed boot_id, or a counter that moved backwards, means the readings
+ /// no longer continue the previous ones; the total must not follow them
+ /// downward. `None` denotes a report carrying no readable counters at all --
+ /// see below.
///
/// The billing reset day is read here rather than passed in: it is one join
/// from a row this already reads, and fetching it separately would cost every
@@ -935,7 +936,9 @@ impl Db {
// first report has none. A reading that shrank under the same boot lost
// one -- an interface included in the sum has disappeared -- so the
// reading is the remainder of that history and booking it would count it
- // twice. A changed boot_id means the counters restarted or,
+ // twice. A changed boot_id means the counters restarted, that the agent
+ // now sums a different set of interfaces (it appends a digest of them,
+ // so a device joining the sum is caught as well as one leaving it), or,
// indistinguishably from here, that a second machine shares the token.
// Realigning costs the seconds since the reboot; the alternative costs
// hundreds of gigabytes against a total that only increases.
@@ -946,11 +949,13 @@ impl Db {
let (d_rx, d_tx) = match counters {
None => (0, 0),
Some(_) if prev_boot.is_empty() || prev_boot != boot_id => {
- // Logged in either case: on a healthy node this is a reboot,
- // while one every few seconds indicates two machines sharing a
- // token.
+ // Logged in either case: on a healthy node this is a reboot or
+ // the agent summing a different set of interfaces, while one
+ // every few seconds indicates two machines sharing a token or
+ // counted interfaces coming and going. The value stays out of
+ // the log: it is the agent's text.
if !prev_boot.is_empty() {
- info!("node {node_id} reports a new boot; re-aligning");
+ info!("node {node_id} reports a new boot_id; re-aligning");
}
(0, 0)
}
diff --git a/web-admin/src/components/Admin.tsx b/web-admin/src/components/Admin.tsx
index fb9bbc3a..870fc913 100644
--- a/web-admin/src/components/Admin.tsx
+++ b/web-admin/src/components/Admin.tsx
@@ -12,7 +12,7 @@ import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
-import { addresses, api, changes, GIB, provisioningSite, trafficCorrection, upload, type Node, type PingTask, type Source } from "@/lib/api"
+import { addresses, api, badIfaceName, changes, currentIface, GIB, ifaceChoice, ifaceSpec, provisioningSite, trafficCorrection, upload, type IfaceChoice, type Node, type PingTask, type Source } from "@/lib/api"
import { bytes, CYCLES, FOREVER, money, uptime } from "@/lib/format"
// Counters the panel can correct after migration or an accounting error.
@@ -157,22 +157,28 @@ function Field({ label, hint, className = "", children }: { label: string; hint?
}
// A titled option with its control at the right. `toggle` makes the whole row a
-// label, so a click anywhere flips the Switch it holds.
-function OptionRow({ title, hint, toggle = false, children }: {
+// label, so a click anywhere flips the Switch it holds; `below` opens beneath it
+// in the same card.
+function OptionRow({ title, hint, toggle = false, below, children }: {
title: React.ReactNode
hint?: React.ReactNode
toggle?: boolean
+ below?: React.ReactNode
children: React.ReactNode
}) {
const Row = toggle ? "label" : "div"
return (
-
-
- {title}
- {hint && {hint}}
-
- {children}
-
+
+ {/* flex-1: stretched by a grid, the row fills the card and stays clickable. */}
+
+
+ {title}
+ {hint && {hint}}
+
+ {children}
+
+ {below &&
{below}
}
+
)
}
@@ -505,16 +511,22 @@ function scriptCommand(site: string, args: (site: string) => string[]) {
// Built here rather than fetched: the node list already carries the token, so
// viewing an install command is a read rather than an action. Reissuing one to
// display it would take the running agent offline.
-function installCommand(site: string, token: string, seconds: number) {
- return scriptCommand(site, (s) => [`--server ${s}`, `--token ${token}`, `--interval ${seconds}`])
+function installCommand(site: string, token: string, seconds: number, iface: string | undefined) {
+ return scriptCommand(site, (s) => [`--server ${s}`, `--token ${token}`, `--interval ${seconds}`, ...ifaceArg(iface)])
+}
+
+// '' is how install.sh is told to clear a value it would otherwise keep; a
+// name needs no quoting, as ifaceSpec admits no shell metacharacter.
+function ifaceArg(iface: string | undefined) {
+ return iface === undefined ? [] : [`--iface ${iface || "''"}`]
}
// One command for a batch of machines. The key belongs to the hub, is valid only
// within the window it opened, and each machine exchanges it for a token of its
// own, so unlike an install command this text is no one's credential and can be
// used directly in a loop.
-function registerCommand(site: string, key: string) {
- return scriptCommand(site, (s) => [`--server ${s}`, `--register ${key}`])
+function registerCommand(site: string, key: string, iface: string | undefined) {
+ return scriptCommand(site, (s) => [`--server ${s}`, `--register ${key}`, ...ifaceArg(iface)])
}
// Carries no token, so it is the same for every node and remains valid after the
@@ -568,7 +580,8 @@ function RegisterDialog({ site, reg, onClose }: {
reg: ReturnType
onClose: () => void
}) {
- const command = reg.left > 0 ? registerCommand(site, reg.key) : ""
+ const iface = useIfaceOption(undefined)
+ const command = reg.left > 0 && iface.valid ? registerCommand(site, reg.key, iface.flag) : ""
const clock = `${Math.floor(reg.left / 60)}:${String(reg.left % 60).padStart(2, "0")}`
return (
@@ -577,18 +590,26 @@ function RegisterDialog({ site, reg, onClose }: {
批量添加
-
+
+ {/* One string: JSX turns a line break inside CJK text into a visible space. */}
-
- setInterval(e.target.value)} />
-
-
-
- {/* A node added before the hub kept tokens has nothing to show
- until one is reissued. */}
- {command || "旧版本创建的凭证不可读取,换发后显示"}
+
+
安装选项
+
+
+ {/* Text rather than number: no spinner arrows, and no wheel
+ changing the value under a passing scroll. */}
+ setInterval(e.target.value.replace(/\D/g, ""))}
+ aria-label="上报间隔(秒)"
+ className="tnum h-8 w-20 bg-background text-right"
+ />
+ 秒
+
+
+
+
+
+
安装命令
+ {/* A node added before the hub kept tokens has nothing to show
+ until one is reissued. */}
+
+ {command || (token ? "网卡名有误,改正后显示命令" : "旧版本创建的凭证不可读取,换发后显示")}
-
+