Finish dm-edworth bootstrap: fix Tailscale install, add system-layout meshcore_capture support

- base: Tailscale's per-codename .nosetup.sh install scripts are gone
  (404); switch to the universal tailscale.com/install.sh, and split
  install/enable (always) from auth (only when a key is given) so a
  node without an authkey still gets Tailscale installed, just not
  authenticated.
- meshcore_capture: the current upstream installer (v2.0.0+, what a
  fresh install now produces) uses a completely different layout than
  dm-baldock/dm-ashwell's older flat ~/.meshcore-packet-capture setup —
  dedicated system user, /opt + /etc/meshcore-packet-capture,
  config.d/*.toml, meshcore-packet-capture.service. Added
  meshcore_capture_layout (system/legacy) so the role targets the right
  paths/service name per host without touching the two working legacy
  nodes. New 99-user.toml.j2 replicates the old MQTT broker coverage
  (letsmesh us/eu, meshrank, ukmesh, meshmapper) in the new config.d
  override format.
- dm-edworth: fill in serial_port now that the MeshCore device is
  connected; confirmed running and publishing to all 5 brokers.
- README: fix broken list formatting, document the installer's
  required manual step and why.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-07-06 22:39:08 +01:00
co-authored by Claude Sonnet 5
parent 77562b1591
commit 1121835678
9 changed files with 176 additions and 22 deletions
+20 -6
View File
@@ -13,17 +13,28 @@ Ansible playbooks for deploying MeshCore monitoring nodes (Raspberry Pi Zero W /
## Prerequisites
**Local machine:**
```bash
pip install ansible
# or: sudo apt install ansible
```
**New Pi node checklist:**
1. Flash Raspberry Pi OS Lite (Trixie) with Raspberry Pi Imager, using "Edit Settings" (OS customisation) to set hostname, the `david` user + password, and the `dotnetwork` wifi (SSID/password only — the imager only supports one network at flash time; the deployed-location and `dotmobile` networks get added later by the `wifi` role, see below)
1. Flash Raspberry Pi OS Lite (Trixie) with Raspberry Pi Imager, using "Edit Settings" (OS customisation) to set hostname, the `david` user + password, and the `dotnetwork` wifi (SSID/password only — the imager only supports one network at flash time; the deployed-location and `dotmobile` networks get added later by the `wifi` role, see below).
2. **Before ejecting the card**, verify the customisation actually got written — mount the boot partition and check `network-config`/`user-data` aren't just the commented-out stock template (see "Imager gotcha" below). Only `cmdline.txt`'s regdomain getting a fresh timestamp while the rest stay at the image's build date is the tell that it silently failed.
3. Boot the Pi, confirm SSH access (password auth, since no key is seeded at flash time)
4. Add `serial_port` (and `wifi_ssid_location`) to `ansible/host_vars/<hostname>/vars.yml`, and the location wifi password to `ansible/host_vars/<hostname>/vault.yml` (see "Vault" below)
5. Run `site.yml` against just that host (see Usage) — this authorizes your SSH keys, joins the deployed-location + dotmobile wifi networks, installs Tailscale, and deploys everything else in one pass
3. Boot the Pi, find its LAN IP (e.g. from the router's DHCP leases), confirm SSH access with the password you set — no key is seeded at flash time.
4. Run `ssh-copy-id david@<ip>` **from an interactive terminal** (not through a non-interactive shell/script — it needs a real TTY to prompt for the password) so ansible can connect with a key.
5. Connect the MeshCore device via USB, then find its serial ID: `ls /dev/serial/by-id/`.
6. Add `serial_port` (and `wifi_ssid_location`) to `ansible/host_vars/<hostname>/vars.yml`, and the location wifi password to `ansible/host_vars/<hostname>/vault.yml` (see "Vault" below).
7. Run `site.yml` against just that host, overriding the host's address since Tailscale/DNS won't resolve it yet: `ansible-playbook site.yml --limit <hostname> -e "ansible_host=<ip>"`. This authorizes your SSH keys, joins the deployed-location + dotmobile wifi networks, installs (but does not authenticate) Tailscale, and deploys everything else in one pass.
8. Tailscale needs one manual step: SSH in and run `sudo tailscale up`, then open the printed URL in a browser to approve the device on your tailnet. (You *can* pass `-e tailscale_auth_key=tskey-...` — from the [admin console](https://login.tailscale.com/admin/settings/keys) — to authenticate non-interactively instead, but there's no stored key anywhere for this repo, so the interactive route is simplest for a one-off node.)
9. The meshcore-packet-capture installer also needs one manual step (see "meshcore-packet-capture install is interactive" below): SSH in and run `sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/agessaman/meshcore-packet-capture/main/install.sh)"`, answering its ~3 prompts (service account, install method — pick **1**, IATA/broker config — defaults are fine, `site.yml` overwrites `.env.local` afterward anyway).
10. Once Tailscale is up, re-run `site.yml` without the `ansible_host` override — it'll resolve via the Tailscale hostname from here on, and will now just write `.env.local` + enable the service since the installer step is already satisfied.
### meshcore-packet-capture install is interactive
The `install.sh` bootstrap has no real non-interactive path for a *fresh* install — its `--update` flag only changes behavior when an installation already exists. It also refuses to run at all with piped stdin (`curl | sudo bash` errors out asking you to download the script first) and its Python layer explicitly opens `/dev/tty` for prompts, which just hangs forever over plain SSH/ansible (no human there to answer). We tried feeding it scripted answers via `script`/a pty and it's not worth the fragility — just run it manually once per node (step 9 above); `ansible/roles/meshcore_capture/tasks/main.yml`'s `creates:` guard means ansible never touches it again afterward.
### Imager gotcha (2026-07)
@@ -34,17 +45,20 @@ Fix: grab the real `.deb` from the [GitHub releases page](https://github.com/ras
## Usage
**Deploy to a single host (recommended for first run / testing):**
```bash
cd ansible
ansible-playbook -i inventory.yml site.yml --limit dm-edworth
```
**Deploy to all nodes:**
```bash
ansible-playbook -i inventory.yml site.yml
```
**Dry run:**
```bash
ansible-playbook -i inventory.yml site.yml --limit dm-edworth --check
```
@@ -56,9 +70,9 @@ You'll be prompted for a Tailscale auth key — leave blank if the node is alrea
## What it does
1. **wifi** — configures NetworkManager connections for `dotnetwork` (home), `dotmobile` (phone hotspot, field troubleshooting fallback), and the host's deployed-location network
2. **base** — apt upgrade, installs screen/pipx/vnstat/git, sets MOTD, authorizes SSH keys for both laptop partitions, installs and authenticates Tailscale
2. **base** — apt upgrade, installs screen/pipx/vnstat/git, sets MOTD, authorizes SSH keys for both laptop partitions, installs Tailscale (always) and authenticates it (only if `tailscale_auth_key` is set — otherwise run `sudo tailscale up` manually once, see checklist above)
3. **meshcore_cli** — installs `meshcore-cli` via pipx
4. **meshcore_capture** — runs the agessaman/meshcore-packet-capture install script, writes `.env.local` config, enables `meshcore-capture.service`, deploys update/log helper scripts
4. **meshcore_capture** — runs the agessaman/meshcore-packet-capture install script (skipped once already installed — see "meshcore-packet-capture install is interactive" above, this needs a manual first run), writes `.env.local` config, enables `meshcore-capture.service`, deploys update/log helper scripts
5. **scripts** — deploys `voltage.sh` and `bandwidth.sh`
## Config
+5
View File
@@ -16,3 +16,8 @@ wifi_password_dotnetwork: "{{ vault_wifi_dotnetwork }}"
wifi_ssid_dotmobile: "dotmobile"
wifi_password_dotmobile: "{{ vault_wifi_dotnetwork }}"
# meshcore-packet-capture install generation — see roles/meshcore_capture/tasks/main.yml.
# New nodes get the current upstream layout by default; older nodes override
# this to "legacy" in their own host_vars.
meshcore_capture_layout: "system"
+3
View File
@@ -3,3 +3,6 @@ serial_port: /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_1C:DB:D4
# Deployed-location wifi
wifi_ssid_location: "awesome"
wifi_password_location: "{{ vault_wifi_location }}"
# Predates the current upstream installer's system-service layout
meshcore_capture_layout: "legacy"
+3
View File
@@ -3,3 +3,6 @@ serial_port: /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_98:3D:AE
# Deployed-location wifi
wifi_ssid_location: "H-NeT"
wifi_password_location: "{{ vault_wifi_location }}"
# Predates the current upstream installer's system-service layout
meshcore_capture_layout: "legacy"
+1 -2
View File
@@ -1,5 +1,4 @@
# Find this by connecting the MeshCore device then running: ls /dev/serial/by-id/
serial_port: FILL_IN_SERIAL_PORT
serial_port: /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_10:B4:1D:E7:FA:C8-if00
# Deployed-location wifi
wifi_ssid_location: "Ridgeway"
+4 -7
View File
@@ -26,19 +26,17 @@
mode: "0644"
become: true
- name: Add Tailscale apt signing key
- name: Download Tailscale install script
get_url:
url: https://pkgs.tailscale.com/stable/debian/bookworm.nosetup.sh
dest: /tmp/tailscale-setup.sh
url: https://tailscale.com/install.sh
dest: /tmp/tailscale-install.sh
mode: "0755"
when: tailscale_auth_key != ""
- name: Run Tailscale install script
shell: sh /tmp/tailscale-setup.sh
shell: sh /tmp/tailscale-install.sh
args:
creates: /usr/bin/tailscale
become: true
when: tailscale_auth_key != ""
- name: Enable and start tailscaled
systemd:
@@ -46,7 +44,6 @@
enabled: true
state: started
become: true
when: tailscale_auth_key != ""
- name: Authenticate Tailscale
shell: tailscale up --authkey {{ tailscale_auth_key }}
@@ -1,6 +1,6 @@
---
- name: restart meshcore-capture
systemd:
name: meshcore-capture
name: "{{ 'meshcore-capture' if meshcore_capture_layout == 'legacy' else 'meshcore-packet-capture' }}"
state: restarted
become: true
+42 -6
View File
@@ -1,23 +1,54 @@
---
# This is a genuine first-run interactive installer (asks for service
# account, install method, IATA/broker config) with no real non-interactive
# path for a fresh install — its own --update flag only changes behavior
# when an install already exists. So: run it manually once per node,
# answering its ~3 prompts (see README), then ansible's `creates:` guard
# skips it forever after. Do NOT try to script answers into it — it insists
# on a real controlling tty and hangs waiting for one over plain SSH/ansible.
#
# meshcore_capture_layout distinguishes two generations of this upstream
# installer:
# system (default) — install method 1, current (v2.0.0+) upstream default.
# Dedicated meshcore-capture system user, /opt + /etc/meshcore-packet-capture,
# config.d/*.toml, service meshcore-packet-capture.service.
# legacy — what dm-baldock/dm-ashwell were set up with (older installer):
# flat ~/.meshcore-packet-capture, .env.local, service meshcore-capture.service.
# Kept only so this role stays a no-op/safe on those two nodes; new nodes
# should use "system".
- name: Run meshcore-packet-capture install script
shell: |
bash <(curl -fsSL https://raw.githubusercontent.com/agessaman/meshcore-packet-capture/main/install.sh)
args:
executable: /bin/bash
creates: /etc/systemd/system/meshcore-capture.service
become: false
creates: >-
{{ '/etc/systemd/system/meshcore-capture.service' if meshcore_capture_layout == 'legacy'
else '/etc/systemd/system/meshcore-packet-capture.service' }}
become: true
- name: Write .env.local config
- name: Write .env.local config (legacy layout)
template:
src: env.local.j2
dest: "{{ ansible_env.HOME }}/.meshcore-packet-capture/.env.local"
mode: "0640"
become: false
when: meshcore_capture_layout == 'legacy'
notify: restart meshcore-capture
- name: Write user config overrides (system layout)
template:
src: 99-user.toml.j2
dest: /etc/meshcore-packet-capture/config.d/99-user.toml
owner: root
group: meshcore-capture
mode: "0640"
become: true
when: meshcore_capture_layout != 'legacy'
notify: restart meshcore-capture
- name: Enable and start meshcore-capture service
systemd:
name: meshcore-capture
name: "{{ 'meshcore-capture' if meshcore_capture_layout == 'legacy' else 'meshcore-packet-capture' }}"
enabled: true
state: started
daemon_reload: true
@@ -25,14 +56,19 @@
- name: Deploy meshcore-capture-update script
copy:
content: "#!/usr/bin/env bash\nbash <(curl -fsSL https://raw.githubusercontent.com/agessaman/meshcore-packet-capture/main/install.sh)\n"
content: >-
#!/usr/bin/env bash
bash <(curl -fsSL https://raw.githubusercontent.com/agessaman/meshcore-packet-capture/main/install.sh){{ ' --update' if meshcore_capture_layout != 'legacy' else '' }}
dest: "{{ ansible_env.HOME }}/meshcore-capture-update.sh"
mode: "0755"
become: false
- name: Deploy meshcore-capture-logs script
copy:
content: "#!/usr/bin/env bash\nsudo journalctl -u meshcore-capture -f\n"
content: |
#!/usr/bin/env bash
sudo journalctl -u {{ 'meshcore-capture' if meshcore_capture_layout == 'legacy' else 'meshcore-packet-capture' }} -f
dest: "{{ ansible_env.HOME }}/meshcore-capture-logs.sh"
mode: "0755"
become: false
@@ -0,0 +1,97 @@
# MeshCore Packet Capture - user overrides
# Managed by Ansible - local changes will be overwritten on next playbook run
#
# [[broker]] tables merge by name across config.d/*.toml, so the letsmesh-us
# and letsmesh-eu entries below only add owner identity to the connection
# details already installed as config.d/10-letsmesh.toml.
[general]
iata = "{{ packetcapture_iata }}"
log_level = "{{ packetcapture_log_level }}"
[update]
repo = "{{ packetcapture_update_repo }}"
branch = "{{ packetcapture_update_branch }}"
[serial]
ports = ["{{ serial_port }}"]
[capture]
connection_type = "serial"
advert_interval_hours = {{ packetcapture_advert_interval_hours }}
owner_public_key = "{{ packetcapture_owner_public_key }}"
owner_email = "{{ packetcapture_owner_email }}"
[[broker]]
name = "letsmesh-us"
[broker.auth]
owner = "{{ packetcapture_owner_public_key }}"
email = "{{ packetcapture_owner_email }}"
[[broker]]
name = "letsmesh-eu"
[broker.auth]
owner = "{{ packetcapture_owner_public_key }}"
email = "{{ packetcapture_owner_email }}"
# MeshRank
[[broker]]
name = "meshrank"
enabled = true
server = "meshrank.net"
port = 8883
transport = "tcp"
qos = 0
retain = false
[broker.tls]
enabled = true
verify = true
[broker.auth]
method = "none"
topic_token = "{{ mqtt_meshrank_token }}"
[broker.topics]
packets = "meshrank/uplink/{TOKEN}/{PUBLIC_KEY}/packets"
status = "meshrank/uplink/{TOKEN}/{PUBLIC_KEY}/status"
# UKMesh (custom broker, no upstream preset)
[[broker]]
name = "ukmesh"
enabled = true
server = "mqtt.ukmesh.com"
port = 443
transport = "websockets"
qos = 0
retain = false
[broker.tls]
enabled = true
verify = true
[broker.auth]
method = "password"
username = "{{ mqtt_ukmesh_username }}"
password = "{{ mqtt_ukmesh_password }}"
# MeshMapper
[[broker]]
name = "meshmapper"
enabled = true
server = "mqtt.meshmapper.net"
port = 443
transport = "websockets"
keepalive = 55
qos = 0
retain = true
[broker.tls]
enabled = true
verify = true
[broker.auth]
method = "token"
audience = "mqtt.meshmapper.net"