feat: deploy onto the pi and add scripts for boot
This commit is contained in:
parent
f4d95c595e
commit
16ad3c6181
13 changed files with 399 additions and 175 deletions
212
pi/README.md
212
pi/README.md
|
|
@ -1,145 +1,181 @@
|
|||
# Pi
|
||||
|
||||
Rust service and NixOS config for the Raspberry Pi at Noisebridge. Reads a magnetic door sensor via GPIO, serves the current state over HTTP, and pushes changes to the cache service.
|
||||
Rust service and deployment workflow for the Raspberry Pi at Noisebridge.
|
||||
|
||||
Runs NixOS with Tailscale for remote access and agenix for secrets.
|
||||
The current recommended setup is:
|
||||
|
||||
## How it works
|
||||
1. run Raspberry Pi OS Lite on the Pi
|
||||
2. keep the Pi itself free of Nix
|
||||
3. build a static `aarch64` Noisebell binary on your laptop with Nix
|
||||
4. copy the binary, secrets, and systemd service to the Pi over SSH
|
||||
|
||||
The service watches a GPIO pin for rising/falling edges with a configurable debounce. When the door state changes, it:
|
||||
This avoids the Raspberry Pi Zero 2 W NixOS boot issues while still keeping the application build reproducible.
|
||||
|
||||
1. Updates in-memory state (atomics)
|
||||
2. POSTs `{"status": "open", "timestamp": ...}` to the cache service with a Bearer token
|
||||
3. Retries with exponential backoff on failure
|
||||
## What stays on Raspberry Pi OS
|
||||
|
||||
On startup it also syncs the initial state.
|
||||
- bootloader
|
||||
- kernel
|
||||
- firmware
|
||||
- Wi-Fi and local networking
|
||||
- SSH base access
|
||||
- Tailscale package/runtime
|
||||
- Avahi package/runtime
|
||||
|
||||
## Setup
|
||||
## What Nix manages
|
||||
|
||||
### Prerequisites
|
||||
- building a static `noisebell` binary for `aarch64-linux`
|
||||
- the exact app binary you deploy
|
||||
- encrypted secrets in the repo
|
||||
- repeatable deployment from your laptop
|
||||
|
||||
If you're building on an x86_64 machine, you need binfmt emulation for aarch64. On NixOS, add this to your system config and rebuild:
|
||||
## Initial Pi OS setup
|
||||
|
||||
```nix
|
||||
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
|
||||
```
|
||||
|
||||
### 1. Flash the SD card
|
||||
|
||||
Preferred: one command builds the bootstrap image, writes it to the SD card, and installs the
|
||||
bootstrap agenix identity onto the boot partition so the full Pi system can come up on first boot:
|
||||
### 1. Flash Raspberry Pi OS Lite
|
||||
|
||||
```sh
|
||||
nix run .#flash-pi-sd -- /dev/sdX
|
||||
curl -L "https://downloads.raspberrypi.org/raspios_lite_arm64_latest" | xz -d -c | sudo dd of=/dev/sdb bs=16M conv=fsync status=progress && sync
|
||||
```
|
||||
|
||||
This bootstrap image already includes the normal Noisebell service, Tailscale, and the Pi config.
|
||||
### 2. Configure the flashed SD card
|
||||
|
||||
Manual build if you need it:
|
||||
Configure it for:
|
||||
|
||||
- Wi-Fi on `Noisebridge`
|
||||
- SSH enabled
|
||||
- serial enabled if you want a recovery console
|
||||
|
||||
The helper script is:
|
||||
|
||||
```sh
|
||||
nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage
|
||||
dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress
|
||||
sudo scripts/configure-pios-sd.sh /run/media/jet/bootfs /run/media/jet/rootfs
|
||||
```
|
||||
|
||||
Boot the Pi. It connects to the Noisebridge WiFi automatically.
|
||||
This setup expects SSH key login for user `pi`; it does not configure a password.
|
||||
|
||||
### 2. SSH host key
|
||||
### 3. Boot the Pi and verify SSH
|
||||
|
||||
Grab the key and add it to `secrets/secrets.nix`:
|
||||
After boot, verify SSH works:
|
||||
|
||||
```sh
|
||||
ssh pi@noisebridge-pi.local
|
||||
```
|
||||
|
||||
## Add the Pi host key to age recipients
|
||||
|
||||
The deploy flow decrypts secrets locally on your laptop, but the Pi host key should still be a recipient for the Pi-facing secrets so the repo stays accurate.
|
||||
|
||||
Grab the Pi host key:
|
||||
|
||||
```sh
|
||||
ssh-keyscan noisebridge-pi.local 2>/dev/null | grep ed25519
|
||||
```
|
||||
|
||||
```nix
|
||||
# secrets/secrets.nix
|
||||
let
|
||||
pi = "ssh-ed25519 AAAA...";
|
||||
in
|
||||
{
|
||||
"api-key.age".publicKeys = [ pi ];
|
||||
"inbound-api-key.age".publicKeys = [ pi ];
|
||||
"tailscale-auth-key.age".publicKeys = [ pi ];
|
||||
}
|
||||
```
|
||||
Add that key to `secrets/secrets.nix` for:
|
||||
|
||||
### 3. Create secrets
|
||||
- `pi-to-cache-key.age`
|
||||
- `cache-to-pi-key.age`
|
||||
- `tailscale-auth-key.age`
|
||||
|
||||
```sh
|
||||
cd secrets
|
||||
agenix -e api-key.age # key for POSTing to the cache
|
||||
agenix -e inbound-api-key.age # key the cache uses to poll us
|
||||
agenix -e tailscale-auth-key.age # tailscale auth key
|
||||
```
|
||||
|
||||
### 4. Bootstrap agenix identity
|
||||
|
||||
The Pi uses a dedicated bootstrap age identity stored at `/boot/noisebell-bootstrap.agekey` to
|
||||
decrypt its runtime secrets, so first boot does not depend on the machine's freshly generated SSH
|
||||
host key.
|
||||
|
||||
To refresh recipients after changing `secrets/secrets.nix`:
|
||||
Then refresh recipients if needed:
|
||||
|
||||
```sh
|
||||
cd secrets
|
||||
agenix -r
|
||||
```
|
||||
|
||||
If you use `nix run .#flash-pi-sd -- /dev/sdX`, this file is installed automatically.
|
||||
|
||||
To install the bootstrap identity manually onto a flashed card before first boot:
|
||||
## Edit secrets
|
||||
|
||||
```sh
|
||||
cd secrets
|
||||
agenix -d bootstrap-identity.age > /boot/noisebell-bootstrap.agekey
|
||||
chmod 600 /boot/noisebell-bootstrap.agekey
|
||||
agenix -e pi-to-cache-key.age
|
||||
agenix -e cache-to-pi-key.age
|
||||
agenix -e tailscale-auth-key.age
|
||||
```
|
||||
|
||||
### 5. SSH access
|
||||
These stay encrypted in git. The deploy script decrypts them locally on your laptop and copies the plaintext files to the Pi as root-only files.
|
||||
|
||||
Add your public key to `configuration.nix`:
|
||||
## Deploy to Raspberry Pi OS
|
||||
|
||||
```nix
|
||||
users.users.root.openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAA..."
|
||||
];
|
||||
```
|
||||
|
||||
### 6. Deploy
|
||||
|
||||
After first boot, the Pi should already be running the normal service stack from the flashed image.
|
||||
Use this only for later updates:
|
||||
From your laptop:
|
||||
|
||||
```sh
|
||||
nixos-rebuild switch --flake .#pi --target-host root@noisebell
|
||||
scripts/deploy-pios-pi.sh pi@noisebridge-pi.local
|
||||
```
|
||||
|
||||
## Configuration
|
||||
If you only know the IP:
|
||||
|
||||
Options under `services.noisebell` in `flake.nix`:
|
||||
```sh
|
||||
scripts/deploy-pios-pi.sh pi@10.21.x.x
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
That script:
|
||||
|
||||
1. builds `.#packages.aarch64-linux.noisebell-static` locally
|
||||
2. decrypts the Pi-facing secrets locally with `agenix`
|
||||
3. uploads the binary and secrets to the Pi
|
||||
4. installs Tailscale and Avahi if needed
|
||||
5. writes `/etc/noisebell/noisebell.env`
|
||||
6. installs `noisebell.service`
|
||||
7. enables and starts the service
|
||||
8. runs `tailscale up` with the decrypted auth key
|
||||
|
||||
## Files written on the Pi
|
||||
|
||||
The deploy script creates:
|
||||
|
||||
- `/opt/noisebell/releases/<timestamp>/noisebell`
|
||||
- `/opt/noisebell/current` -> current release symlink
|
||||
- `/etc/noisebell/pi-to-cache-key`
|
||||
- `/etc/noisebell/cache-to-pi-key`
|
||||
- `/etc/noisebell/tailscale-auth-key`
|
||||
- `/etc/noisebell/noisebell.env`
|
||||
- `/etc/systemd/system/noisebell.service`
|
||||
|
||||
All secret files are root-only.
|
||||
|
||||
## Tailscale
|
||||
|
||||
Tailscale is kept on Raspberry Pi OS rather than NixOS.
|
||||
|
||||
The deploy script:
|
||||
|
||||
- installs the Tailscale package if missing
|
||||
- enables `tailscaled`
|
||||
- runs `tailscale up --auth-key=... --hostname=noisebridge-pi`
|
||||
|
||||
So Tailscale stays part of the base OS, while its auth key is still managed as an encrypted `age` secret in this repo.
|
||||
|
||||
## Later updates
|
||||
|
||||
Normal iteration is just rerunning the deploy script:
|
||||
|
||||
```sh
|
||||
scripts/deploy-pios-pi.sh pi@noisebridge-pi.local
|
||||
```
|
||||
|
||||
That rebuilds the binary locally, uploads a new release, refreshes secrets, and restarts the service.
|
||||
|
||||
## Service configuration
|
||||
|
||||
The deployed service uses these environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `endpointUrl` | required | Webhook URL to POST state changes to |
|
||||
| `apiKeyFile` | required | Outbound API key file (agenix secret) |
|
||||
| `inboundApiKeyFile` | required | Inbound API key file for GET auth |
|
||||
| `gpioPin` | `17` | GPIO pin number |
|
||||
| `debounceSecs` | `5` | Debounce delay in seconds |
|
||||
| `port` | `8080` | HTTP server port |
|
||||
| `retryAttempts` | `3` | Webhook retry count |
|
||||
| `retryBaseDelaySecs` | `1` | Exponential backoff base delay |
|
||||
| `httpTimeoutSecs` | `10` | Outbound request timeout |
|
||||
| `bindAddress` | `0.0.0.0` | HTTP bind address |
|
||||
| `activeLow` | `true` | Low GPIO = door open (depends on wiring) |
|
||||
| `restartDelaySecs` | `5` | systemd restart delay on failure |
|
||||
| `watchdogSecs` | `30` | systemd watchdog timeout |
|
||||
| `NOISEBELL_GPIO_PIN` | `17` | GPIO pin number |
|
||||
| `NOISEBELL_DEBOUNCE_SECS` | `5` | Debounce delay in seconds |
|
||||
| `NOISEBELL_PORT` | `80` | HTTP server port |
|
||||
| `NOISEBELL_ENDPOINT_URL` | required | Webhook URL to POST state changes to |
|
||||
| `NOISEBELL_RETRY_ATTEMPTS` | `3` | Webhook retry count |
|
||||
| `NOISEBELL_RETRY_BASE_DELAY_SECS` | `1` | Exponential backoff base delay |
|
||||
| `NOISEBELL_HTTP_TIMEOUT_SECS` | `10` | Outbound request timeout |
|
||||
| `NOISEBELL_BIND_ADDRESS` | `0.0.0.0` | HTTP bind address |
|
||||
| `NOISEBELL_ACTIVE_LOW` | `true` | Low GPIO = door open |
|
||||
|
||||
## API
|
||||
|
||||
All endpoints require `Authorization: Bearer <token>`.
|
||||
|
||||
**`GET /`** — door state
|
||||
**`GET /`**
|
||||
|
||||
```json
|
||||
{"status": "open", "timestamp": 1710000000}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue