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
|
|
@ -14,7 +14,7 @@ Pi (door sensor) ──webhook──> Cache ──webhook──> Discord
|
|||
|
||||
| Directory | What it is |
|
||||
|-----------|------------|
|
||||
| [`pi/`](pi/) | NixOS config + Rust service for the Pi |
|
||||
| [`pi/`](pi/) | Raspberry Pi OS base with laptop-built Noisebell deploy |
|
||||
| [`remote/`](remote/) | Server-side services (cache and Discord bot) |
|
||||
| [`secrets/`](secrets/) | Shared agenix-encrypted secrets and recipient rules |
|
||||
|
||||
|
|
|
|||
155
flake.nix
155
flake.nix
|
|
@ -74,8 +74,17 @@
|
|||
overlays = [ rust-overlay.overlays.default ];
|
||||
};
|
||||
|
||||
muslPkgs = import nixpkgs {
|
||||
inherit system;
|
||||
crossSystem.config = "aarch64-unknown-linux-musl";
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
};
|
||||
|
||||
piRustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
targets = [ "aarch64-unknown-linux-gnu" ];
|
||||
targets = [
|
||||
"aarch64-unknown-linux-gnu"
|
||||
"aarch64-unknown-linux-musl"
|
||||
];
|
||||
};
|
||||
|
||||
piCraneLib = (crane.mkLib pkgs).overrideToolchain piRustToolchain;
|
||||
|
|
@ -106,7 +115,33 @@
|
|||
}
|
||||
);
|
||||
|
||||
bootstrapModule =
|
||||
piStaticArgs = {
|
||||
inherit src;
|
||||
pname = "noisebell-pi-static";
|
||||
version = "0.1.0";
|
||||
strictDeps = true;
|
||||
doCheck = false;
|
||||
|
||||
CARGO_BUILD_TARGET = "aarch64-unknown-linux-musl";
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER = "${muslPkgs.stdenv.cc.targetPrefix}cc";
|
||||
TARGET_CC = "${muslPkgs.stdenv.cc.targetPrefix}cc";
|
||||
CC_aarch64_unknown_linux_musl = "${muslPkgs.stdenv.cc.targetPrefix}cc";
|
||||
HOST_CC = "${pkgs.stdenv.cc.nativePrefix}cc";
|
||||
|
||||
depsBuildBuild = [ muslPkgs.stdenv.cc ];
|
||||
cargoExtraArgs = "-p noisebell";
|
||||
};
|
||||
|
||||
piStaticArtifacts = piCraneLib.buildDepsOnly piStaticArgs;
|
||||
|
||||
noisebell-pi-static = piCraneLib.buildPackage (
|
||||
piStaticArgs
|
||||
// {
|
||||
cargoArtifacts = piStaticArtifacts;
|
||||
}
|
||||
);
|
||||
|
||||
piImageBaseModule =
|
||||
{
|
||||
lib,
|
||||
nixos-raspberrypi,
|
||||
|
|
@ -132,66 +167,72 @@
|
|||
];
|
||||
boot.kernelParams = lib.mkAfter [ "cfg80211.ieee80211_regdom=US" ];
|
||||
|
||||
networking.hostName = "noisebridge-pi";
|
||||
networking.networkmanager.enable = lib.mkForce false;
|
||||
networking.wireless = {
|
||||
enable = true;
|
||||
networks."Noisebridge".psk = "noisebridge";
|
||||
};
|
||||
|
||||
services.avahi = {
|
||||
enable = true;
|
||||
nssmdns4 = true;
|
||||
openFirewall = true;
|
||||
};
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
PermitRootLogin = "prohibit-password";
|
||||
};
|
||||
};
|
||||
|
||||
users.users.root.openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"
|
||||
];
|
||||
};
|
||||
|
||||
flash-bootstrap-sd = pkgs.writeShellApplication {
|
||||
name = "flash-bootstrap-sd";
|
||||
flash-pi-sd = pkgs.writeShellApplication {
|
||||
name = "flash-pi-sd";
|
||||
runtimeInputs = [
|
||||
agenix.packages.${system}.default
|
||||
pkgs.coreutils
|
||||
pkgs.nix
|
||||
pkgs.parted
|
||||
pkgs.systemd
|
||||
pkgs.util-linux
|
||||
pkgs.xz
|
||||
pkgs.zstd
|
||||
];
|
||||
text = ''
|
||||
set -euo pipefail
|
||||
|
||||
PARTPROBE=${pkgs.parted}/bin/partprobe
|
||||
MOUNT=${pkgs.util-linux}/bin/mount
|
||||
UMOUNT=${pkgs.util-linux}/bin/umount
|
||||
MOUNTPOINT=${pkgs.util-linux}/bin/mountpoint
|
||||
FINDMNT=${pkgs.util-linux}/bin/findmnt
|
||||
UDEVADM=${pkgs.systemd}/bin/udevadm
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: flash-bootstrap-sd /dev/sdX" >&2
|
||||
echo "usage: flash-pi-sd /dev/sdX" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
device="$1"
|
||||
flake_path=${builtins.toString ./.}
|
||||
zstd_bin=${pkgs.zstd}/bin/zstd
|
||||
secrets_dir=${builtins.toString ./secrets}
|
||||
rules_file=${builtins.toString ./secrets/secrets.nix}
|
||||
key_name="bootstrap-identity.age"
|
||||
boot_mount_dir="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
if "$MOUNTPOINT" -q "$boot_mount_dir"; then
|
||||
sudo "$UMOUNT" "$boot_mount_dir"
|
||||
fi
|
||||
rm -rf "$boot_mount_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ ! -b "$device" ]; then
|
||||
echo "not a block device: $device" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
boot_part="''${device}1"
|
||||
case "$device" in
|
||||
*[0-9]) boot_part="''${device}p1" ;;
|
||||
esac
|
||||
|
||||
echo "Requesting sudo access before build and flash..."
|
||||
sudo -v
|
||||
echo "Sudo authentication successful."
|
||||
|
||||
echo "Building bootstrap NixOS Raspberry Pi Zero 2 W image..."
|
||||
echo "Building full Pi NixOS image..."
|
||||
image_out="$(nix build \
|
||||
--print-out-paths \
|
||||
--cores 0 \
|
||||
--max-jobs auto \
|
||||
"$flake_path#nixosConfigurations.bootstrap.config.system.build.sdImage")"
|
||||
"$flake_path#nixosConfigurations.pi.config.system.build.sdImage")"
|
||||
|
||||
image="$(echo "$image_out"/sd-image/*.img*)"
|
||||
if [ ! -f "$image" ]; then
|
||||
|
|
@ -202,12 +243,30 @@
|
|||
echo "Flashing $image to $device..."
|
||||
if [ "''${image##*.}" = "zst" ]; then
|
||||
"$zstd_bin" -d --stdout "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress
|
||||
elif [ "''${image##*.}" = "xz" ]; then
|
||||
xz -d -c "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress
|
||||
else
|
||||
sudo dd if="$image" of="$device" bs=16M conv=fsync status=progress
|
||||
fi
|
||||
sync
|
||||
|
||||
echo "Done. This is the custom bootstrap NixOS image."
|
||||
sudo "$PARTPROBE" "$device"
|
||||
sudo "$UDEVADM" settle
|
||||
|
||||
if "$FINDMNT" -rn "$boot_part" >/dev/null 2>&1; then
|
||||
sudo "$UMOUNT" "$boot_part"
|
||||
fi
|
||||
|
||||
echo "Installing bootstrap age identity onto $boot_part..."
|
||||
sudo "$MOUNT" "$boot_part" "$boot_mount_dir"
|
||||
(
|
||||
cd "$secrets_dir"
|
||||
RULES="$rules_file" agenix -d "$key_name"
|
||||
) | sudo tee "$boot_mount_dir/noisebell-bootstrap.agekey" >/dev/null
|
||||
sudo chmod 600 "$boot_mount_dir/noisebell-bootstrap.agekey"
|
||||
sync
|
||||
|
||||
echo "Done. This is the full Pi NixOS image."
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -288,7 +347,7 @@
|
|||
inherit
|
||||
noisebell-cache
|
||||
noisebell-discord
|
||||
flash-bootstrap-sd
|
||||
flash-pi-sd
|
||||
pi-serial
|
||||
;
|
||||
default = noisebell-cache;
|
||||
|
|
@ -296,6 +355,7 @@
|
|||
|
||||
packages.aarch64-linux = {
|
||||
noisebell = noisebell-pi;
|
||||
noisebell-static = noisebell-pi-static;
|
||||
default = noisebell-pi;
|
||||
};
|
||||
|
||||
|
|
@ -313,33 +373,26 @@
|
|||
};
|
||||
};
|
||||
|
||||
nixosConfigurations.pi = nixpkgs.lib.nixosSystem {
|
||||
system = "aarch64-linux";
|
||||
modules = [
|
||||
agenix.nixosModules.default
|
||||
nixos-hardware.nixosModules.raspberry-pi-3
|
||||
(import ./pi/module.nix {
|
||||
pkg = noisebell-pi;
|
||||
rev = self.shortRev or "dirty";
|
||||
})
|
||||
./pi/configuration.nix
|
||||
./pi/hardware-configuration.nix
|
||||
];
|
||||
};
|
||||
|
||||
nixosConfigurations.bootstrap = nixos-raspberrypi.lib.nixosSystem {
|
||||
nixosConfigurations.pi = nixos-raspberrypi.lib.nixosSystem {
|
||||
specialArgs = {
|
||||
inherit nixos-raspberrypi;
|
||||
};
|
||||
modules = [
|
||||
nixos-raspberrypi.nixosModules.sd-image
|
||||
bootstrapModule
|
||||
agenix.nixosModules.default
|
||||
piImageBaseModule
|
||||
(import ./pi/module.nix {
|
||||
pkg = noisebell-pi;
|
||||
rev = self.shortRev or "dirty";
|
||||
})
|
||||
./pi/configuration.nix
|
||||
];
|
||||
};
|
||||
|
||||
devShells.${system}.default = craneLib.devShell {
|
||||
packages = [
|
||||
flash-bootstrap-sd
|
||||
agenix.packages.${system}.default
|
||||
flash-pi-sd
|
||||
pi-serial
|
||||
pkgs.nix
|
||||
pkgs.parted
|
||||
|
|
@ -350,9 +403,9 @@
|
|||
};
|
||||
|
||||
apps.${system} = {
|
||||
flash-bootstrap-sd = {
|
||||
flash-pi-sd = {
|
||||
type = "app";
|
||||
program = "${flash-bootstrap-sd}/bin/flash-bootstrap-sd";
|
||||
program = "${flash-pi-sd}/bin/flash-pi-sd";
|
||||
};
|
||||
|
||||
pi-serial = {
|
||||
|
|
|
|||
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}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ HOSTNAME=noisebridge-pi
|
|||
WIFI_SSID=Noisebridge
|
||||
WIFI_PASSWORD=noisebridge
|
||||
PI_USERNAME=pi
|
||||
PI_PASSWORD=noisebridge
|
||||
SSH_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu'
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
|
|
@ -15,14 +14,26 @@ if [[ $EUID -ne 0 ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$BOOTFS" || ! -d "$ROOTFS" ]]; then
|
||||
echo "Expected mounted boot and root partitions." >&2
|
||||
if [[ ! -d "$BOOTFS" ]]; then
|
||||
echo "Expected mounted boot partition." >&2
|
||||
echo "Boot: $BOOTFS" >&2
|
||||
echo "Root: $ROOTFS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASSWORD_HASH=$(mkpasswd -m sha-512 "$PI_PASSWORD")
|
||||
CONFIG_TXT="$BOOTFS/config.txt"
|
||||
if [[ ! -f "$CONFIG_TXT" && -f "$BOOTFS/firmware/config.txt" ]]; then
|
||||
CONFIG_TXT="$BOOTFS/firmware/config.txt"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$CONFIG_TXT" ]]; then
|
||||
echo "Could not find config.txt in $BOOTFS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOTFS_READY=0
|
||||
if [[ -d "$ROOTFS" && -f "$ROOTFS/etc/shadow" ]]; then
|
||||
ROOTFS_READY=1
|
||||
fi
|
||||
|
||||
cat > "$BOOTFS/network-config" <<EOF
|
||||
network:
|
||||
|
|
@ -59,17 +70,15 @@ dsmode: local
|
|||
instance_id: ${HOSTNAME}-bootstrap-1
|
||||
EOF
|
||||
|
||||
grep -q '^enable_uart=1$' "$BOOTFS/config.txt" || printf '\nenable_uart=1\n' >> "$BOOTFS/config.txt"
|
||||
grep -q '^enable_uart=1$' "$CONFIG_TXT" || printf '\nenable_uart=1\n' >> "$CONFIG_TXT"
|
||||
: > "$BOOTFS/ssh"
|
||||
cat > "$BOOTFS/userconf.txt" <<EOF
|
||||
${PI_USERNAME}:${PASSWORD_HASH}
|
||||
EOF
|
||||
|
||||
cat > "$ROOTFS/etc/hostname" <<EOF
|
||||
if [[ "$ROOTFS_READY" -eq 1 ]]; then
|
||||
cat > "$ROOTFS/etc/hostname" <<EOF
|
||||
${HOSTNAME}
|
||||
EOF
|
||||
|
||||
cat > "$ROOTFS/etc/hosts" <<EOF
|
||||
cat > "$ROOTFS/etc/hosts" <<EOF
|
||||
127.0.0.1 localhost
|
||||
::1 localhost ip6-localhost ip6-loopback
|
||||
ff02::1 ip6-allnodes
|
||||
|
|
@ -78,13 +87,14 @@ ff02::2 ip6-allrouters
|
|||
127.0.1.1 ${HOSTNAME}
|
||||
EOF
|
||||
|
||||
mkdir -p "$ROOTFS/home/pi/.ssh"
|
||||
cat > "$ROOTFS/home/pi/.ssh/authorized_keys" <<EOF
|
||||
mkdir -p "$ROOTFS/home/pi/.ssh"
|
||||
cat > "$ROOTFS/home/pi/.ssh/authorized_keys" <<EOF
|
||||
${SSH_KEY}
|
||||
EOF
|
||||
chown -R 1000:1000 "$ROOTFS/home/pi/.ssh"
|
||||
chmod 700 "$ROOTFS/home/pi/.ssh"
|
||||
chmod 600 "$ROOTFS/home/pi/.ssh/authorized_keys"
|
||||
chown -R 1000:1000 "$ROOTFS/home/pi/.ssh"
|
||||
chmod 700 "$ROOTFS/home/pi/.ssh"
|
||||
chmod 600 "$ROOTFS/home/pi/.ssh/authorized_keys"
|
||||
fi
|
||||
|
||||
sync
|
||||
|
||||
|
|
@ -94,5 +104,7 @@ echo "- Wi-Fi: ${WIFI_SSID}"
|
|||
echo "- SSH enabled on first boot"
|
||||
echo "- Serial UART enabled"
|
||||
echo "- Username: ${PI_USERNAME}"
|
||||
echo "- Password: ${PI_PASSWORD}"
|
||||
echo "- Pi user authorized key installed"
|
||||
if [[ "$ROOTFS_READY" -ne 1 ]]; then
|
||||
echo "- Note: rootfs was not mounted; only boot partition config was updated"
|
||||
fi
|
||||
|
|
|
|||
6
scripts/deploy-full-pi.sh
Executable file
6
scripts/deploy-full-pi.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_HOST=${1:-root@noisebridge-pi.local}
|
||||
|
||||
exec nixos-rebuild switch --flake ".#pi" --target-host "$TARGET_HOST"
|
||||
117
scripts/deploy-pios-pi.sh
Executable file
117
scripts/deploy-pios-pi.sh
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_HOST=${1:-pi@noisebridge-pi.local}
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
RELEASE_ID=${RELEASE_ID:-$(date +%Y%m%d-%H%M%S)}
|
||||
REMOTE_RELEASE_DIR=${REMOTE_RELEASE_DIR:-/opt/noisebell/releases/$RELEASE_ID}
|
||||
REMOTE_CURRENT_LINK=${REMOTE_CURRENT_LINK:-/opt/noisebell/current}
|
||||
REMOTE_TMP_DIR=${REMOTE_TMP_DIR:-/home/pi/noisebell-deploy-tmp}
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
SSH_OPTS=(
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
)
|
||||
|
||||
echo "Building static aarch64 Noisebell binary locally..."
|
||||
PACKAGE_PATH=$(nix build .#packages.aarch64-linux.noisebell-static --print-out-paths --no-link)
|
||||
BIN_PATH="$PACKAGE_PATH/bin/noisebell"
|
||||
|
||||
if [[ ! -x "$BIN_PATH" ]]; then
|
||||
echo "built binary not found: $BIN_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v agenix >/dev/null 2>&1; then
|
||||
echo "agenix is required in your shell to decrypt secrets locally" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Decrypting Pi secrets locally..."
|
||||
(
|
||||
cd "$REPO_ROOT/secrets"
|
||||
RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d pi-to-cache-key.age > "$TMP_DIR/pi-to-cache-key"
|
||||
RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d cache-to-pi-key.age > "$TMP_DIR/cache-to-pi-key"
|
||||
RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d tailscale-auth-key.age > "$TMP_DIR/tailscale-auth-key"
|
||||
)
|
||||
chmod 600 "$TMP_DIR"/*
|
||||
|
||||
echo "Preparing remote directories on $TARGET_HOST..."
|
||||
ssh "${SSH_OPTS[@]}" "$TARGET_HOST" "mkdir -p '$REMOTE_TMP_DIR' && rm -f '$REMOTE_TMP_DIR/noisebell' '$REMOTE_TMP_DIR/pi-to-cache-key' '$REMOTE_TMP_DIR/cache-to-pi-key' '$REMOTE_TMP_DIR/tailscale-auth-key' && sudo mkdir -p '$REMOTE_RELEASE_DIR' /etc/noisebell /opt/noisebell/releases /var/lib/noisebell"
|
||||
|
||||
echo "Uploading binary and secret files..."
|
||||
scp "${SSH_OPTS[@]}" "$BIN_PATH" "$TARGET_HOST:$REMOTE_TMP_DIR/noisebell"
|
||||
scp "${SSH_OPTS[@]}" "$TMP_DIR/pi-to-cache-key" "$TARGET_HOST:$REMOTE_TMP_DIR/pi-to-cache-key"
|
||||
scp "${SSH_OPTS[@]}" "$TMP_DIR/cache-to-pi-key" "$TARGET_HOST:$REMOTE_TMP_DIR/cache-to-pi-key"
|
||||
scp "${SSH_OPTS[@]}" "$TMP_DIR/tailscale-auth-key" "$TARGET_HOST:$REMOTE_TMP_DIR/tailscale-auth-key"
|
||||
|
||||
echo "Installing service and Tailscale on $TARGET_HOST..."
|
||||
ssh "${SSH_OPTS[@]}" "$TARGET_HOST" "REMOTE_RELEASE_DIR='$REMOTE_RELEASE_DIR' REMOTE_CURRENT_LINK='$REMOTE_CURRENT_LINK' REMOTE_TMP_DIR='$REMOTE_TMP_DIR' bash -s" <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl rsync avahi-daemon
|
||||
|
||||
if ! command -v tailscale >/dev/null 2>&1; then
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
fi
|
||||
sudo systemctl enable --now ssh avahi-daemon tailscaled
|
||||
|
||||
sudo install -m 755 "$REMOTE_TMP_DIR/noisebell" "$REMOTE_RELEASE_DIR/noisebell"
|
||||
sudo mv "$REMOTE_TMP_DIR/pi-to-cache-key" /etc/noisebell/pi-to-cache-key
|
||||
sudo mv "$REMOTE_TMP_DIR/cache-to-pi-key" /etc/noisebell/cache-to-pi-key
|
||||
sudo mv "$REMOTE_TMP_DIR/tailscale-auth-key" /etc/noisebell/tailscale-auth-key
|
||||
sudo chown root:root /etc/noisebell/pi-to-cache-key /etc/noisebell/cache-to-pi-key /etc/noisebell/tailscale-auth-key
|
||||
sudo chmod 600 /etc/noisebell/pi-to-cache-key /etc/noisebell/cache-to-pi-key /etc/noisebell/tailscale-auth-key
|
||||
|
||||
sudo tee /etc/noisebell/noisebell.env >/dev/null <<'ENVEOF'
|
||||
NOISEBELL_GPIO_PIN=17
|
||||
NOISEBELL_DEBOUNCE_SECS=5
|
||||
NOISEBELL_PORT=80
|
||||
NOISEBELL_RETRY_ATTEMPTS=3
|
||||
NOISEBELL_RETRY_BASE_DELAY_SECS=1
|
||||
NOISEBELL_HTTP_TIMEOUT_SECS=10
|
||||
NOISEBELL_ENDPOINT_URL=https://noisebell.extremist.software/webhook
|
||||
NOISEBELL_BIND_ADDRESS=0.0.0.0
|
||||
NOISEBELL_ACTIVE_LOW=true
|
||||
RUST_LOG=info
|
||||
ENVEOF
|
||||
sudo chmod 600 /etc/noisebell/noisebell.env
|
||||
|
||||
sudo tee /etc/systemd/system/noisebell.service >/dev/null <<UNITEOF
|
||||
[Unit]
|
||||
Description=Noisebell GPIO door monitor
|
||||
After=network-online.target tailscaled.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
NotifyAccess=all
|
||||
EnvironmentFile=/etc/noisebell/noisebell.env
|
||||
ExecStart=/bin/bash -lc 'export NOISEBELL_API_KEY="$$(cat /etc/noisebell/pi-to-cache-key)"; export NOISEBELL_INBOUND_API_KEY="$$(cat /etc/noisebell/cache-to-pi-key)"; exec ${REMOTE_CURRENT_LINK}/noisebell'
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
WatchdogSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNITEOF
|
||||
|
||||
sudo ln -sfn "$REMOTE_RELEASE_DIR" "$REMOTE_CURRENT_LINK"
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now noisebell.service
|
||||
|
||||
sudo tailscale up --auth-key="$(sudo cat /etc/noisebell/tailscale-auth-key)" --hostname=noisebridge-pi || true
|
||||
|
||||
rmdir "$REMOTE_TMP_DIR" 2>/dev/null || true
|
||||
|
||||
echo "Noisebell deployed on Raspberry Pi OS."
|
||||
EOF
|
||||
|
||||
echo "Done."
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,8 +1,7 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 Ziw7aw W4nd317o0ON6n006hwUNIops3L7VngNqDqJ1bG7tCyg
|
||||
xyjvZOOzA0u3e+Cba99LR8J5JAkl6muHuspJ+b8dEog
|
||||
-> ssh-ed25519 uKftJg bX0GHUPdD4hR2yxLNx8ho689or7FNHXPA8iV9Af1q2s
|
||||
B91XIdsnHfAKdKfu8jmHKKptA5OomQ5sHvXfLLUeNbs
|
||||
--- 8zoH1w5ywsbDN5Xt50sE4BUgXoq12mr5b4rSVYMLEB8
|
||||
CŽb;*œã‚´*;í5œ#BL^ñƒT¯úE¢<45>%ÚÎçáZ8†r‰wݧ#‚r-îG¢éÖKÔ—t…|Ê
|
||||
pí³ÿÑòâÙ_ßÄÊ»\s»N´C‚UG
|º‡*i(6ÇÔA•k/µy
|
||||
-> ssh-ed25519 Ziw7aw 756HU1sPe5g3sa0YYfzMnXiToT5K+nfAPhfABEetgRI
|
||||
E8dqhn7hN77qM0PhHMEAsZySd1hfk0w1tlsiWj4aEOQ
|
||||
-> ssh-ed25519 uKftJg eGfmzvHseauAFPOR1QXfdmaQy5TjpNsoBWq27mbO50w
|
||||
KRuGUW65uQ5+IdREyg6X1oj0P5IkuuxFEl1WylGpAHc
|
||||
--- 2Ya08payqNiMCEqBXrbKEA53ETupxwgUNRcMNu9IP6k
|
||||
&°‰¢ÏvÄ<76>tBšL˜‚.¬HéÚ“Y P®Sç3Tó¹•4䱟D¬s,9Ñeã4TpåÌ,Gë?KtJm’wF' –®0³ÉXy»<á’÷<E28099>î>½\Áç^þ°m»ÜrÊÏùµŸÏü
|
||||
Binary file not shown.
|
|
@ -1,11 +1,12 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 Ziw7aw TMz4xBHmy5btMk5ETWyvjZcjj0QwF3F6Iqt2QmkwVwk
|
||||
KvcY409ZakEUO7OrmDrwseMQv/w7i3B7uDiXjls/5qE
|
||||
-> ssh-ed25519 jcT/MQ Y5TqVlHy3fM+CmQIBu1x18C3wjHtRZCHJ9dhFyVAR0I
|
||||
WPqxEZrpAm0wLYT4s9e1uxSuDjhwUwIOL4BvDtvqytU
|
||||
-> X25519 ZqSHgIStt6Ru3osVvMA1M5sydoY+CeZ56temQyCFIVY
|
||||
4tPA6VITlAzJxCFGVreKK1B6rrHm+ka4ELwnzYrMKbQ
|
||||
-> ssh-ed25519 uKftJg OR8VgPUuEvS/0Gc5c92IlAp4DKKYcRzBbSh1tX9ddzg
|
||||
DZYYx9ngwEUTmj/JaP2XnCQHjpPY8WYgOEDlOfZPLeA
|
||||
--- 4iDfaqdSLiW0doVijoZC5ckxiCmsmVWJi5Kvaxic2Ng
|
||||
è÷VV;2†h<>&e@sÏx#d=Y?iðw½´ÉÍF<15>OÝc×/ûš(™¨àœ›¼føkØí‹ƒ—¿}[¡¤'=B@ô‡¨Â!wÔ
|
||||
-> ssh-ed25519 Ziw7aw ZaLRvgj6V9ukim0lfxHftVUvCXi7tIXPn5O/2nzQqCE
|
||||
cem5AxKMkYOs8iifYP80hkbr5km7bFOdjCt7Ym6lQcs
|
||||
-> ssh-ed25519 NFB4qA ssMeOzGjehzTeppIGHpzPViIKObSwnXw6OZ1DfXs6Ew
|
||||
Y813udN4YGDMszEC8FVZz7Na6XQigVNFTdusLomMusg
|
||||
-> X25519 qmoLWSdRljn6daPlUyqk9TOOvBaUx42CvqcpXe/xUCE
|
||||
7xMN5RbYnpgw3+/pHyCiEyEhyUmQOwa1zSlAbuVwlQo
|
||||
-> ssh-ed25519 uKftJg Fv8M0RogkcYWd46bJY3OJCoCFAW8QMjzLueDZowylSA
|
||||
R3w6E2RvDmgaKKhxqWHjEeIQxNSCHzX7+nLb3Ls+iHs
|
||||
--- 13dp1N6I6pPdDx+FrxsT+ZS5rsFfrK3x0F7Rs6vN6/I
|
||||
ÖBI¬8Ż‹
|
||||
|đ9”<39>úŠ/tĆěšÂÚqXžŕÄÇ[𮠼™Í;ž§Ž†*Ć·¨»ńJ2|ŃS(đ‚’$Ó©ŘÎ<>SÁ*m
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
let
|
||||
jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu";
|
||||
pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E";
|
||||
pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEfZfAQEFy8QU5P7deC2vWPN76YpUKcBF8fiWwuANumG";
|
||||
server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB";
|
||||
piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862";
|
||||
in
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue