Simplifies the primary path from "build a custom ISO" to "install from git and run one menu", which is what the operator actually asked for. The ISO tooling stays in the tree as a secondary/optional route and is not removed. admin/bf-bootstrap.sh -- one command on Ubuntu: - clones the repo to /opt/blueforce-linux or fast-forwards an existing checkout (a dirty tree is reported and skipped, never reset or stashed) - installs git/curl/ansible-core, links admin/bf-* and scripts/*/bf-* into /usr/local/bin, and finishes with the exact next command - --check is read-only, works without root, and mutates nothing; the piped form (curl ... | sudo -S -p '' bash) is guarded against eating the script admin/bf-menu -- numbered operator console (categories 1-5): - 1 installs THIS device locally; 2 and 5 drive the fleet through the existing 21 Ansible playbooks; 3 prepares and verifies central integration without installing a central server; 4 holds local tools - every operation picks one target: a dealer number or an approved wave group. Scope is derived from the playbook file itself, so a playbook whose declared scope disagrees with the console's table is refused rather than run - the command is always shown before it runs; release-gated operations require a typed wave token, production requires a second token, and "all" runs wave-by-wave, stopping at the first failure - audit log at /var/log/blueforce-console.log (mode 600, falls back to $XDG_STATE_HOME when not writable), recording sanitized target/command/exit only: no playbook output, no secrets admin/bf-creds -- optional credential record, DEFAULT OFF: - a bare run generates nothing and reports what already exists (device id, WireGuard public key, private key path only, RustDesk id) - --generate writes /root/blueforce-credentials.txt (0600) and --fetch delivers it to admin/secrets/<dealer>.txt (0700/0600, git-ignored). The secret never reaches stdout, stderr or argv - this does not change the SSH design: password auth stays off and the blueforce account stays locked, per docs/06. It is a local delivery aid only docs/33-OPERATOR-CONSOLE.md -- step-by-step guide for an operator new to Linux and DevOps: which command to type where, what the screen will ask, both end-to-end sample sessions, the five most common errors, and the security rules. Tests: run-all.sh -> 9/9 pass, exit 0 (adds check-admin-static.sh, 159 checks, with negative controls and mutation-proven non-vacuity). |
||
|---|---|---|
| .. | ||
| lib | ||
| bf-bootstrap.sh | ||
| bf-creds | ||
| bf-menu | ||
| README.md | ||
Blueforce operator console (bf-menu)
Numbered terminal console for the Blueforce field fleet. One entry point for the operator: no command has to be memorised, every command is printed before it runs, and the dangerous ones ask twice.
Three layers, one screen:
| Layer | What it is | Where |
|---|---|---|
| Local install | This device, brought to the Field OS standard | category 1 |
| Fleet operations | The repository's existing Ansible playbooks, always with an explicit --limit |
category 2 |
| Read-only information | Device and fleet state, no change anywhere | categories 3, 4, 5 |
The console is a thin front end. It does not reimplement installer logic, does
not invent playbooks, and does not talk to devices itself: it selects the right
bf-* tool, playbook and target, then executes exactly that.
1. Requirements
- Ubuntu LTS field device or admin workstation (the console is plain bash 4+).
gitfor the checkout;ansible-core/ansibleonly on the machine that drives fleet operations (categories 2 and 3). A field device that only installs itself needs neither Ansible nor an inventory.sudofor category 1 (local install) and for writing to/var/log/blueforce-console.log.- Optional, per category:
python3(readsstate.json),wireguard-tools(wg),systemd,dpkg.
2. Quick start
git clone <repository-url> Blueforce-Linux
cd Blueforce-Linux
sudo admin/bf-bootstrap.sh # optional: links the tools into /usr/local/bin
sudo admin/bf-menu # or: sudo bf-menu, once bootstrap ran
An install is git-based on purpose: the running revision is traceable with
git log, and the device identity (dealer number) is never baked into an image
— it is typed in the first console session. ISO/firstboot remain a secondary,
optional path (docs/26, docs/27).
Options:
-h, --help usage
-V, --version console version
--repo PATH repository root (default: parent of this script, symlinks resolved)
--inventory PATH Ansible inventory (default: <repo>/ansible/inventory/hosts.yml)
--log-file PATH audit log (default: /var/log/blueforce-console.log)
--dry-run print every command instead of running it
--no-color disable ANSI colors (NO_COLOR is honored too)
--list print the operation catalogue and exit (non-interactive)
--list-targets print inventory groups and host counts, then exit
Environment: BF_REPO, BF_INVENTORY, BF_CONSOLE_LOG mirror the options.
Exit status: 0 success or clean quit, 1 failure, 2 usage error.
3. Menu map
0 is Back on every screen, q is Quit on every screen. The console
never changes anything from a menu that only displays information.
1) Installation — this device, locally
| # | Action | Under the hood |
|---|---|---|
| 1 | Pre-flight check | blueforce-install.sh --dealer-id <no> --check (writes nothing) |
| 2 | Install this device | --dealer-id <no> [--offline] [--resume] |
| 3 | Resume a failed run | --dealer-id <no> --resume |
| 4 | Run a single module | --dealer-id <no> --only <NN-name> |
| 5 | Show module state | /var/lib/blueforce/install-state |
| 6 | Show the installer log | tail -n 40 /var/log/blueforce-install.log |
Dealer numbers are validated against ^[0-9]{8}$ before anything runs; a wrong
format stops the flow immediately. Actions 2–4 require root and will say so.
2) Device operations — the fleet, via Ansible
2.1 Read-only reports (11 operations, nothing is changed):
| # | Operation | Playbook |
|---|---|---|
| 1 | Ping / reachability | bf-ping.yml |
| 2 | Uptime | bf-uptime.yml |
| 3 | Disk usage | bf-disk-check.yml |
| 4 | Docker status | bf-docker-status.yml |
| 5 | Package versions | bf-package-check.yml |
| 6 | Pending security updates | bf-security-updates-check.yml |
| 7 | Provisioning phase report | bf-provisioning-status.yml |
| 8 | Enrollment + central evidence | bf-enrollment-status.yml |
| 9 | Field OS version report | bf-fieldos-version.yml |
| 10 | Remote access channels | bf-remote-channels-check.yml |
| 11 | Offline readiness report | bf-offline-ready-check.yml |
2.2 Maintenance (10 operations, destructive ones ask twice):
| # | Operation | Playbook | Scope |
|---|---|---|---|
| 1 | Restart RDP | bf-rdp-restart.yml |
simple |
| 2 | Restart RustDesk | bf-rustdesk-restart.yml |
simple |
| 3 | Restart WireGuard | bf-wireguard-restart.yml |
simple |
| 4 | Restart an allowlisted service | bf-service-restart.yml |
simple |
| 5 | GUI off (headless) | bf-gui-off.yml |
wave |
| 6 | GUI on (maintenance) | bf-gui-on.yml |
wave |
| 7 | Collect logs / support bundle | bf-collect-logs.yml |
wave |
| 8 | Run an approved script | bf-run-script.yml |
wave |
| 9 | Controlled reboot | bf-reboot.yml |
release |
| 10 | Deploy approved update | bf-deploy-update.yml |
release |
2.3 Show inventory targets lists the waves with their device counts.
The console cross-checks its own table against the playbook file before running (see §5); a playbook whose gates changed refuses to run rather than run ungated.
3) Central (preparation) — verifies and prepares central integration; it
installs no central component. Central readiness report (inventory, placeholders
in group_vars/all.yml, admin key path), configured endpoints, a read-only
DNS/TCP probe of those endpoints, the per-wave gate state, and the preparation
checklist.
4) Tools — git pull --ff-only of the checkout, environment/reference info,
the operation catalogue, the local gates (bf-check-local, bf-check-enrollment,
bf-check-ready), bf-diagnostics, bf-live-hw-check, bf-remote-status --check,
bf-release, bf-hardware-inventory.sh, bf-support-bundle, and the console's
own log.
5) Device information — identity (hostname, BF-<no>, dealer number),
bf-status, provisioning/enrollment state, network (addresses, routes, DNS,
WireGuard peer counts and handshake ages), disk/memory/CPU, installed pinned
packages, service status, recent journal errors, plus the fleet inventory summary
and a "resolve a target" helper that shows the exact --limit for copy/paste.
4. Target selection
Every device operation asks for a target first — never a default:
Target selection (mode: any|wave|release)
1) Single device by dealer number -> bf-<8 digits>
2) Group / wave -> lab, pilot_1, pilot_2, wave_1, wave_2, production
3) All devices in the fleet -> --limit all
9) Type the target directly -> dealer number, group, hostname or 'all'
0) Back q) Quit
Rules the console enforces:
- Single device — the dealer number must match
^[0-9]{8}$and the device must exist in the inventory. The wave is derived from the inventory, so a wave-scoped playbook gets-e operation_wave=<its wave>. A device outside every wave cannot be used for a wave-scoped operation. - Group / wave — the group must exist in the inventory. For wave-scoped and release-gated operations only the six approved waves are offered.
- All devices — allowed for simple and wave-scoped playbooks. It shows how
many devices are affected and requires typing
allbefore anything runs. For wave-scoped playbooks the fleet is processed one wave at a time inlab → pilot_1 → pilot_2 → wave_1 → wave_2 → production, each wave shown and confirmed on its own, stopping at the first failure so no later wave is touched. - Release-gated playbooks (
bf-reboot.yml,bf-deploy-update.yml) accept an approved wave group only: no single device, no fleet-wide run. That is the playbooks' own gate (docs/10) and the console does not work around it.
5. What gets added to a command
The console derives the gate variables from the playbook file itself (its non-comment lines), so the table in the menu can never silently drop a gate:
| Scope | Detected by | Variables added |
|---|---|---|
simple |
neither wave variable | only --limit |
wave |
uses operation_wave |
-e operation_wave=<wave> -e operation_confirmed=true, plus -e production_override=true for the production wave |
release |
uses release_wave |
-e release_wave=<wave>, -e reboot_confirmed=true / -e wave_gate_confirmed=true when the playbook declares them, plus the production override for the production wave |
Confirmations, in order:
- the target and the full command are printed;
- for
releasescope, the wave gate must be typed asGATE(the console never setswave_gate_confirmed=trueon its own — only after the previous wave's health gate and central approval, docs/10); - for the production wave, the word
productionmust be typed; - for fleet-wide scope, the word
allmust be typed; yruns,cruns with--check(dry run of the playbook), anything else cancels.
Extra variables are validated with the same rules the playbooks enforce, so the
operator gets a clear message instead of an Ansible assertion failure: package
pins must be package=version; service names must be in the playbook allowlist;
script paths must be inside /opt/blueforce/bin or /usr/local/sbin; the reboot
grace period must be a whole number of seconds.
6. Dry runs
--dry-runprints every command and executes nothing.- Answering
cat the run prompt executes the command with--checkappended, which is a real Ansible dry run. - Category 1's pre-flight already is the installer's own
--check.
7. Audit log
Default /var/log/blueforce-console.log, created with mode 600 (it is
re-chmoded to 600 if it already exists). If it is not writable, the console
warns and falls back to $XDG_STATE_HOME/blueforce/blueforce-console.log; it
never logs into a world-readable file.
Each run records one line per event: session start, target, the exact command, and the exit status. It does not store raw playbook output: tool output may contain whatever a device script prints, and copying it into a log is how a secret ends up on disk. Output is shown on screen and stays there.
Before anything reaches the log it passes a single sanitizer that redacts
password=, token=, secret=, api_key=, private_key=, passphrase=,
psk= and BEGIN … PRIVATE KEY payloads. The console itself never asks for a
secret: no password, token or private key is typed into or stored by it.
8. Security notes
- No secret material is read, printed or logged. The console never runs bare
wg show(private and preshared keys stay off the screen) — only the listen port, peer count and handshake ages. state.jsonis read through a whitelist of non-secret keys (phase,enrollment_status,fleet_status,device_id,provisioning_id). A future token field cannot leak into the UI or the log.- No ungated update. There is no
apt upgrade, nolatestpull and no free-form command anywhere in the menu; updates go through the release-gated playbook or not at all (docs/10). - Destructive actions ask twice: a warning plus an explicit
y, and the highest-risk scopes need a typed word (GATE,production,all, or the wave name in a fleet-wide loop). - Fail-closed on drift: if the playbook's declared gates do not match the console's expectations, the console refuses to run that playbook at all.
- Root is used where the underlying tool needs it (local installer, support bundle). Read-only information screens are careful about what they read.
- The console never edits the inventory. Adding a device stays a reviewed
edit of
ansible/inventory/hosts.yml:
pilot_1:
hosts:
bf-12010193:
ansible_host: 10.42.1.20
9. Deliberately out of scope
- No distro/ISO build. ISO and firstboot remain a secondary path (
docs/26,docs/27); the primary install path isgit clone+ install. Category 4 does not offerprovisioning/iso/build-blueforce-iso.sh. - No password generation. Credential files are the separate, optional
admin/bf-credstool and default to off; the console does not call it. - No central server installation. Category 3 verifies and prepares; installing the hub, monitoring or the token service is infrastructure work.
- No new playbooks, no free-form commands. If an operation is not one of the 21 existing playbooks, the console does not fake it.
10. Troubleshooting
| Symptom | Why | What to do |
|---|---|---|
ansible-playbook was not found on this machine |
categories 2/3 need Ansible on the driving machine | sudo apt install ansible (or pipx install ansible-core), or run the command from another admin host |
No Ansible inventory found under … |
no ansible/inventory/hosts.yml yet |
cp ansible/inventory/hosts.example.yml ansible/inventory/hosts.yml, then fill in real bf-<no> hosts |
Only hosts.example.yml exists |
it holds example IDs and is never used automatically | copy it and replace the example devices |
Invalid dealer number '<x>' |
must be exactly 8 digits | type the number as printed on the device record, leading zeros included |
bf-<no> is not in the inventory |
the device is not registered | add it to the right wave group (§8), then retry |
… is not an approved wave |
a wave-scoped or release-gated operation got a non-wave group | pick one of lab, pilot_1, pilot_2, wave_1, wave_2, production |
Refusing to run <playbook>: the console expects scope … |
the playbook's gates changed since the console table was written | update admin/bf-menu before using that playbook |
This operation needs root on THIS device |
local install runs as root | sudo bf-menu |
Log warning about /var/log/blueforce-console.log |
not writable, another admin owns it, or the mode is wrong | run with sudo, or pass --log-file <path> |
11. Files
| File | Role |
|---|---|
admin/bf-menu |
entry point: menu screens, operation catalogue, command building, confirmations, audit log |
admin/lib/tui.sh |
presentation and input helpers (screens, items, prompts, confirmations, messages) |
admin/lib/targets.sh |
device identity, inventory parsing, target selection, --limit construction |
Library contract: bf-menu sets BF_REPO/BF_INVENTORY and sources both
libraries; targets.sh fills BF_TARGET_KIND, BF_TARGET_LIMIT,
BF_TARGET_WAVE, BF_TARGET_COUNT, BF_TARGET_LABEL and never renders UI or
executes anything; tui.sh never exits the process except in tui_die.
Both libraries are safe to source on their own, which is how the target logic and the inventory parser can be tested without a terminal.
12. Verifying a change
bash -n admin/bf-menu admin/lib/tui.sh admin/lib/targets.sh
shellcheck -x admin/bf-menu admin/lib/tui.sh admin/lib/targets.sh
admin/bf-menu --list # catalogue, no inventory needed
admin/bf-menu --list-targets # groups, hosts and counts
--list and --list-targets are the non-interactive entry points: they exercise
the catalogue and the inventory parser without touching a device.
The console was checked on an admin workstation with a stub ansible-playbook
that echoes its arguments, covering: every scope/target combination and the exact
-e variables produced, the refusal paths (single device for release scope,
fleet-wide release run, wrong wave for a wave-scoped run, bad dealer number,
unknown device, bad package pin, service outside the allowlist, script outside
the approved directories, production/gate/fleet-wide words withheld), --dry-run
executing nothing, --check being appended, the missing-Ansible and
missing-inventory explanations, the log's 600 mode and its redaction of a test
secret, and clean EOF behaviour on a closed stdin. Playbook coverage was
cross-checked programmatically: all 21 playbooks in ansible/playbooks/ appear
exactly once and every declared scope matches the playbook's own gate variables.
Real devices have not been touched yet — the LAB run is what turns these
expectations into evidence.