4 Commits
Author SHA1 Message Date
devandClaude Opus 5 8b79b8d929 docs: Note commit trailer and body conventions
CI / Check, build and cache nixfiles (push) Failing after 8m39s
Update docs / update (push) Successful in 1m15s
Co-Authored-By is the only trailer wanted here; session links are not.
Also spell out that bodies should stay concise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 01:17:11 +01:00
devandClaude Opus 5 98162ea989 openwrt: Build fergal's firmware in the flake
fergal is an 8-port SFP+ switch on a Realtek RTL9303, running OpenWrt
rather than RouterOS or UniFi. It is not part of the fabric yet, but
its firmware is now built here via astro's nix-openwrt-imagebuilder.
Packages are baked into the image: OpenWrt's package server keeps only
the current build of each feed, so installing at runtime stops working
as soon as the feed moves past the running firmware.

Those feed indexes rotate constantly, and upstream pins only the
indexes -- a mismatch drops evaluation into import-from-derivation,
putting this flake's eval on the network. The openwrt-feeds input pins
expanded per-package hashes instead, in a repository of its own
because they run to hundreds of thousands of generated lines.

Flashing gets a procedure doc and a thin skill pointing at it, the
same split as the box installation and nixpkgs upgrade procedures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 01:16:53 +01:00
jackos1998andClaude Opus 5 e7122b8862 docs: Add box installation procedure
CI / Check, build and cache nixfiles (push) Successful in 56m36s
Update docs / update (push) Successful in 1m11s
Canonical, agent-agnostic procedure for bringing a new box into the
flake, from a booted installer through to a deployable system, plus a
thin Claude Code skill pointing at it -- same split as the nixpkgs
upgrade procedure.

Records the conventions that were not written down anywhere: sgdisk
plus an LVM PV for the nix and persist volumes, adopting the
installer's SSH host keys so secrets can be encrypted before first
boot, and taking whatever show-hw-config emits that the flake's own
modules do not already set.

Also notes in AGENTS.md that a changed recipient list should be
re-encrypted per file with ragenix --rekey-one; --rekey rewrites every
secret in secrets/ and buries the actual change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:30:59 +01:00
jackos1998andClaude Opus 5 0aade09d7e nixos/portcullis: Add initial config
New bare-metal box headed for Nikhef, intended to take over most of
estuary's colony edge routing. This is the bootstrap config only: the
hardware, the single-NVMe ESP + LVM layout, and enough networking to
boot and be reachable.

It is being staged at home before it is racked, so it has no colony
assignments yet. Every 2.5G port takes DHCP and whichever one is
patched in brings the box up; kea registers the DHCP hostname, so the
deploy node points at portcullis.dyn.h.nul.ie until there is a real
colony FQDN for it.

The host key was adopted from the installer session and seeded onto
the persist volume before first boot, so my.secrets.key could be set
up front -- which makes portcullis a recipient of the user-passwd
secret that my.user declares for every box.

Documented with a box page, a row in the colony site index, and a note
in the colony section of networking.md that the topology is expected
to change once portcullis takes over from estuary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:30:59 +01:00
17 changed files with 907 additions and 88 deletions
+37
View File
@@ -0,0 +1,37 @@
---
name: flash-openwrt
description: >-
Flash a flake-built OpenWrt image onto one of the OpenWrt boxes (currently fergal): build the
image, pre-flight the box, back up its config, validate and stage the image, run sysupgrade, and
verify what came back. Use when the user wants to flash, reflash, upgrade or sysupgrade an OpenWrt
box, or after changing its baked-in package list.
---
# Flash an OpenWrt box
The canonical, agent-agnostic procedure lives in the repo at
[`docs/openwrt-flash.md`](../../../docs/openwrt-flash.md). Read it and follow the phases in order.
Key reminders (see the doc for the full steps):
- **Stop at the ⏸ before Phase 5.** Flashing reboots the box and cannot be interrupted partway.
Confirm with the user, and confirm a serial console is reachable, *before* writing anything.
- **Packages are baked into the image**, so a package change means a reflash. Edit the box's list in
[`openwrt/default.nix`](../../../openwrt/default.nix), rebuild, and check the built `.manifest` —
a package name that doesn't exist is not a build error, it just isn't in the image.
- **`scp` does not work** on these boxes (no `sftp-server`). Move files with
`ssh <box> 'cat > /dev/…' < file` and `ssh <box> 'cat …' > file`.
- **Detach the upgrade with `setsid`**, not `nohup` (absent on busybox). `sysupgrade` kills the SSH
session mid-run, and an attached run dies with it — possibly after the firmware is erased.
- **Never reach for `sysupgrade -c`.** It needs `/overlay/upper/etc` and aborts *after* erasing the
firmware when that is missing, which is exactly the initramfs case. Plain `sysupgrade` already
keeps everything in `/lib/upgrade/keep.d/`.
- **Poll SSH to detect the reboot, never ping.** Successful pings return in milliseconds, so a
"wait for down" loop completes instantly and reports nonsense. Sleep between probes; expect about
three minutes.
- **Verify after**, don't assume: revision, management address, package count against the manifest,
and that the new packages are present and running.
The images are declared in [`openwrt/default.nix`](../../../openwrt/default.nix); background on the
outputs and the pinned package feeds is in
[`docs/deployment.md`](../../../docs/deployment.md#openwrt-images).
+40
View File
@@ -0,0 +1,40 @@
---
name: install-box
description: >-
Install a new NixOS box into this flake, from bare hardware booted into the custom installer
through to a deployable system: probe the hardware, partition and format the disks, write the box
config and flake entry, run do-install, and document the box. Use when the user wants to install,
bootstrap, provision or add a new box/host/machine.
---
# Install a box
The canonical, agent-agnostic procedure lives in the repo at
[`docs/install-box.md`](../../../docs/install-box.md). Read it and follow the phases in order.
Key reminders (see the doc for the full steps):
- It is **guided, not automated** — stop at the ⏸ points: settling what the box actually is
(Phase 1), wiping and partitioning disks (Phase 3), and running `do-install` (Phase 6). The user
often wants to do the install step by hand.
- **Phase 1 is not derivable from the hardware.** Name, site, role, channel and whether the box gets
assignments now all have to come from the user. Ask before writing files.
- **`show-hw-config` is a shell alias**, so it needs `installer-shell bash -lic show-hw-config`.
Run it twice: once early for the kernel-module lists, once after mounting for the filesystems.
- **`git add` the new box directory before evaluating** — the flake reads through git, and an
untracked path fails as "Path … is not tracked by Git" rather than as a Nix error.
- **Validate with `check-system <host>`**, not `build-system` — evaluation catches module and option
errors cheaply.
- **Seed the SSH host key from the installer** (Phase 3) by copying `/etc/ssh/ssh_host_*` onto the
persist volume. The installer regenerates them each boot, so they are safe to adopt, and it means
`my.secrets.key` can be set and secrets encrypted before the install rather than after first boot.
- **Every box declares a secret even when its own config declares none** — `my.user` pulls in
`user-passwd.txt` by default — so setting `my.secrets.key` always requires
`ragenix --rekey-one secrets/user-passwd.txt.age`. Check with
`nix eval .#nixosConfigurations.<host>.config.age.secrets --apply builtins.attrNames` rather than
assuming there is nothing to do. Re-encrypt selectively; `ragenix --rekey` rewrites every secret
in `secrets/` and drowns the real change in churn.
- Take **everything** useful out of `show-hw-config`, not just the modules and filesystems — drop an
option only when a nixfiles module already sets it.
- Finish with Phase 8: box page, site index row, `networking.md` prose. Don't hand-edit anything
between `<!-- ... -->` markers.
+10 -1
View File
@@ -61,6 +61,9 @@ Common ones:
`SSH_AUTH_SOCK= ssh-machine …` (or add `-o IdentityAgent=none` to a raw `ssh`). `SSH_AUTH_SOCK= ssh-machine …` (or add `-o IdentityAgent=none` to a raw `ssh`).
- `ragenix` — edit age secrets using `.keys/dev.key` as identity (see Secrets). - `ragenix` — edit age secrets using `.keys/dev.key` as identity (see Secrets).
- `repl` — `nix repl .#`. - `repl` — `nix repl .#`.
- `installer-shell` / `do-install <system>` — drive an install against a booted installer at
`$INSTALLER`. For bringing up a new box end to end follow the guided procedure in
[`docs/install-box.md`](docs/install-box.md).
- `update-nixpkgs` / `update-home-manager` — bump pinned inputs. For the full periodic upgrade - `update-nixpkgs` / `update-home-manager` — bump pinned inputs. For the full periodic upgrade
(rebasing the `devplayer0` nixpkgs fork, stable-release bumps, version-gate sweep, input review) (rebasing the `devplayer0` nixpkgs fork, stable-release bumps, version-gate sweep, input review)
follow the guided procedure in [`docs/nixpkgs-upgrade.md`](docs/nixpkgs-upgrade.md). follow the guided procedure in [`docs/nixpkgs-upgrade.md`](docs/nixpkgs-upgrade.md).
@@ -177,6 +180,10 @@ recipient key list (always including `.keys/dev.pub`). Edit secrets with the `ra
command, which supplies `.keys/dev.key` as the identity. The `.keys/` directory (dev + deploy command, which supplies `.keys/dev.key` as the identity. The `.keys/` directory (dev + deploy
private keys) is required for editing secrets, deploying, and running dev VMs. private keys) is required for editing secrets, deploying, and running dev VMs.
When a recipient list changes, re-encrypt selectively with `ragenix --rekey-one <file>` for each
affected secret. `ragenix --rekey` rewrites **every** secret in `secrets/`, burying the real change
in churn.
## Conventions ## Conventions
- Format with `nixpkgs-fmt` (`fmt`). 2-space indent, `inherit (...)` blocks at the top of `let` — - Format with `nixpkgs-fmt` (`fmt`). 2-space indent, `inherit (...)` blocks at the top of `let` —
@@ -205,7 +212,9 @@ private keys) is required for editing secrets, deploying, and running dev VMs.
- Commit subjects follow `area/scope: Capitalized summary` (e.g. `nixos/home: ...`); keep logically - Commit subjects follow `area/scope: Capitalized summary` (e.g. `nixos/home: ...`); keep logically
distinct changes in separate commits. Aim for 50-character subjects and do not exceed 72 distinct changes in separate commits. Aim for 50-character subjects and do not exceed 72
characters. Wrap commit bodies at 72 columns. A concise body describing the change and its characters. Wrap commit bodies at 72 columns. A concise body describing the change and its
rationale is welcome when the subject alone does not provide enough context. rationale is welcome when the subject alone does not provide enough context — keep it to the
essentials rather than restating the diff. `Co-Authored-By` is the only trailer used here; do
**not** add a `Claude-Session` link (or any other session/tooling trailer).
## Documentation ## Documentation
+6
View File
@@ -27,6 +27,10 @@ Not every box fits this pattern, but **colony** and **home** are organised this
- [`deployment.md`](deployment.md) — deploy-rs, devshell commands, secrets workflow, CI. - [`deployment.md`](deployment.md) — deploy-rs, devshell commands, secrets workflow, CI.
- [`nixpkgs-upgrade.md`](nixpkgs-upgrade.md) — guided procedure for the periodic upgrade of the four - [`nixpkgs-upgrade.md`](nixpkgs-upgrade.md) — guided procedure for the periodic upgrade of the four
nixpkgs channels and home-manager (fork rebase, stable bumps, input review). nixpkgs channels and home-manager (fork rebase, stable bumps, input review).
- [`install-box.md`](install-box.md) — guided procedure for installing a new box, from the booted
installer through partitioning, the box config, `do-install` and documentation.
- [`openwrt-flash.md`](openwrt-flash.md) — guided procedure for flashing a flake-built image onto an
OpenWrt box, from the build through pre-flight, `sysupgrade` and verification.
- [`reference/dns.md`](reference/dns.md) — generated forward and reverse DNS record reference. - [`reference/dns.md`](reference/dns.md) — generated forward and reverse DNS record reference.
- [`reference/nixos-options.md`](reference/nixos-options.md) — generated per-option reference for - [`reference/nixos-options.md`](reference/nixos-options.md) — generated per-option reference for
the custom `my.*` NixOS modules. the custom `my.*` NixOS modules.
@@ -53,6 +57,8 @@ colony (physical VM host, ams1)
├── git ────── Gitea + Gitea Actions runner ├── git ────── Gitea + Gitea Actions runner
├── mail ───── Debian VM running mailcow (not NixOS) ├── mail ───── Debian VM running mailcow (not NixOS)
└── darts ──── third-party/customer VM (opaque, not NixOS) └── darts ──── third-party/customer VM (opaque, not NixOS)
portcullis (bare-metal edge box for Nikhef — staged, not yet in service)
``` ```
## Site: home ## Site: home
+37
View File
@@ -171,6 +171,43 @@ default `/tmp/xchg/dev.key`), so dev VMs can decrypt the boxes' secrets without
keys. Dev VMs also get DHCP on `eth0`, an SSH port forward (host 2222 → guest 22), and are keys. Dev VMs also get DHCP on `eth0`, an SSH port forward (host 2222 → guest 22), and are
automatically excluded from deploy targets. automatically excluded from deploy targets.
## OpenWrt images
The OpenWrt boxes are not NixOS and are not deployed by this flake, but their firmware is built
here. [`openwrt/default.nix`](../openwrt/default.nix) declares one image per box and packages it
through [`astro/nix-openwrt-imagebuilder`](https://github.com/astro/nix-openwrt-imagebuilder),
which drives OpenWrt's official ImageBuilder — prebuilt target packages assembled into a sysupgrade
image, with no cross-toolchain involved.
| Output | Box | Release |
|---|---|---|
| `openwrt-fergal` | [fergal](sites/home/switches.md#fergal-the-openwrt-switch) | `snapshot` |
| `openwrt-fergal-release` | The same, on the release branch | pinned in `openwrt/default.nix` |
Both are in `ci`, so images are built and pushed to the Harmonia cache like everything else. Build
one with `nix build .#openwrt-fergal`; the result holds the `-squashfs-sysupgrade.bin` to flash,
plus a package manifest and an SBOM. Getting it onto the box is a guided procedure of its own —
see [`openwrt-flash.md`](openwrt-flash.md).
Packages are baked into the image rather than installed on the box. OpenWrt's package server keeps
only the current build of each feed, so a box that installs packages at runtime stops being able to
do so as soon as the feed moves on from the firmware it is running. Adding a package means editing
the image's `packages` list and reflashing.
### The feed pin
OpenWrt's download server is never at rest: snapshot is rebuilt daily, and
`releases/<version>/packages/` is a symlink to the rolling `packages-<major>` feed shared by every
point release. Building straight against it fails on hash mismatches and, worse, resolves the
package list by import-from-derivation — which would drag *evaluation* of this flake onto the
network and let an OpenWrt feed rebuild break `check-system` for unrelated boxes.
The `openwrt-feeds` input exists to stop that. It holds expanded per-package hashes, so every `.apk`
is a plain pinned `fetchurl` and no import-from-derivation is involved. Its generated files run to
hundreds of thousands of lines and are rewritten wholesale on each refresh, which is why they live
in their own repository rather than here. Refresh the pin with `nix flake update openwrt-feeds`;
adding a release or target means adding it to that repo's `pins` and regenerating there first.
## CI ## CI
GitHub/Gitea Actions workflows live in [`.gitea/workflows/`](../.gitea/workflows). GitHub/Gitea Actions workflows live in [`.gitea/workflows/`](../.gitea/workflows).
+213
View File
@@ -0,0 +1,213 @@
# Installing a box
Procedure for bringing a new NixOS box into this flake, from bare hardware booted into the custom
installer through to a deployable system. Written to be followed by a person or any coding agent; a
Claude Code entry point exists at `.claude/skills/install-box/` but the steps below are the
canonical source.
The install is **guided, not automated**: the mechanical steps (probing hardware, partitioning,
writing the config, evaluating it) can be done straight through, but stop at the judgment points
(marked ⏸) — what the box actually is, wiping disks, and running `do-install` itself. Keep a running
summary and present it before any destructive step.
For the installer image itself — what it contains, how it is built and released — see
[`misc/installer.md`](misc/installer.md).
## Setup facts
- **Installer access:** the box boots the custom installer (ISO, kexec or netboot) and is reached
over SSH as `root` with `.keys/deploy.key`. The devshell sets
`INSTALLER_SSH_OPTS = "-i .keys/deploy.key"`; set `INSTALLER` to the address, and
`INSTALLER_SSH_PORT` if it is not 22.
- **Devshell commands** (from [`devshell/install.nix`](../devshell/install.nix)):
`installer-shell [cmd]` and `do-install [--no-bootloader] [--no-substitute] <system>`.
- **`INSTALL_ROOT`** is `/mnt` in the installer's environment — everything is mounted under it and
`do-install` reads it from the installer rather than assuming.
- **`show-hw-config`** is a shell *alias* in the installer (wrapping
`nixos-generate-config --show-hardware-config --root $INSTALL_ROOT`), so it needs an interactive
shell: `installer-shell bash -lic show-hw-config`. A plain `installer-shell show-hw-config` will
not find it.
- **Validation:** `check-system <host>` evaluates a system without building it — use it while
iterating. Only `build-system` when you need the artifact.
- Nix reads the flake through git, so **`git add` new files before evaluating** — an untracked
box directory fails with "Path … is not tracked by Git", not a Nix error.
## Phase 1 — Establish what the box is
⏸ Settle these before writing anything; they decide where every file goes and they are not
recoverable from the hardware:
1. **Name and site** — the box name doubles as the `nixos.systems.<name>` attribute, the deploy node
name and the docs page name. The site decides the directory (`nixos/boxes/<site>/`), the
constants block in [`lib/constants.nix`](../lib/constants.nix) it draws prefixes from, and the
docs directory (`docs/sites/<site>/`, `docs/remote/`, `docs/mobile/`).
2. **Role** — what it does, which decides its modules, networking and firewall config.
3. **nixpkgs channel** — `unstable` / `stable` / `mine` / `mine-stable`; match the site's other
boxes unless there is a reason not to.
4. **Networking** — whether it gets `assignments` now, or bootstraps on DHCP because it is being
staged somewhere other than its final home. A box with no assignment still needs a reachable
`my.deploy.node.hostname`, since the default (`config.networking.fqdn`) will not resolve.
## Phase 2 — Reach the installer and inventory the hardware
With the box booted into the installer and `INSTALLER` set:
1. Confirm you are talking to the right thing — `installer-shell hostname` reports `installer`, and
`/etc/os-release` carries `VARIANT_ID=installer`.
2. Collect the inventory you will need for both the config and the docs page: `lscpu`, `free -h`,
`lsblk -o NAME,SIZE,TYPE,FSTYPE,MODEL,SERIAL`, `ip -br link`, `ip -br addr`,
`lspci -nn | grep -Ei 'ethernet|network|nvme|sata|raid'`, and whether `/sys/firmware/efi` exists.
3. Record every NIC's **permanent MAC** against its PCI address — interface naming in Phase 5 pins
names to MACs, and the PCI order tells you which physical port is which.
4. Run `installer-shell bash -lic show-hw-config` now for the kernel-module lists. Filesystems are
not mounted yet, so run it again in Phase 4 for those.
## Phase 3 — Partition, format and mount
⏸ Destructive. Check the target disks are the ones you think they are and that nothing on them is
wanted, then show the exact command sequence and get confirmation before running it.
The house layout is a tmpfs root (`my.tmproot`) with three mounts: an ESP at `/boot`, `/nix`, and
`/persist` (`neededForBoot = true`). Use **`sgdisk`** for partitioning and put `/nix` and `/persist`
on **LVM** so they can be resized later:
```sh
sgdisk -Z /dev/<disk>
sgdisk \
-n 1:0:+2G -t 1:ef00 -c 1:esp \
-n 2:0:0 -t 2:8e00 -c 2:lvm \
/dev/<disk>
partprobe /dev/<disk>
pvcreate /dev/<disk>p2
vgcreate main /dev/<disk>p2
lvcreate -L 48G -n <host>-nix main
lvcreate -l 100%FREE -n <host>-persist main
mkfs.vfat -n ESP /dev/<disk>p1
mkfs.ext4 -L nix /dev/main/<host>-nix
mkfs.ext4 -L persist /dev/main/<host>-persist
```
Conventions worth keeping: volume group `main`, logical volumes `<host>-nix` / `<host>-persist`,
and ext4 filesystem labels `nix` and `persist`. Size the ESP and `/nix` to the box — 2 GiB and
48 GiB suit a small single-disk box.
Then mount everything under `$INSTALL_ROOT`, with a tmpfs standing in for the eventual tmpfs root:
```sh
mount -t tmpfs -o size=2G tmpfs "$INSTALL_ROOT"
mkdir -p "$INSTALL_ROOT"/{nix,persist,boot}
mount /dev/main/<host>-nix "$INSTALL_ROOT/nix"
mount /dev/main/<host>-persist "$INSTALL_ROOT/persist"
mount /dev/<disk>p1 "$INSTALL_ROOT/boot"
```
### Seed the SSH host key
The installer generates fresh host keys on every boot, so adopt them as the box's own rather than
letting it generate another set on first boot. Copy them onto the persist volume now:
```sh
install -d -m 0755 "$INSTALL_ROOT/persist/etc/ssh"
for t in ed25519 rsa; do
install -m 0600 "/etc/ssh/ssh_host_${t}_key" "$INSTALL_ROOT/persist/etc/ssh/ssh_host_${t}_key"
install -m 0644 "/etc/ssh/ssh_host_${t}_key.pub" "$INSTALL_ROOT/persist/etc/ssh/ssh_host_${t}_key.pub"
done
```
`my.tmproot` persists `services.openssh.hostKeys` at exactly those paths, so the installed system
picks them up. This means the box's key is known **before** it first boots, so `my.secrets.key` can
be set and its secrets encrypted as part of the same pass — no install, boot, re-encrypt, re-deploy
round trip. (For a box already up, the `ssh-get-ed25519 <host>` devshell command prints the same
value in the form `my.secrets.key` wants.)
## Phase 4 — Capture the hardware config
Re-run `installer-shell bash -lic show-hw-config` with the filesystems mounted.
Read the whole generated file and carry over **anything** in it that the flake does not already
provide — it reflects what was actually detected on this hardware, and the list below is just what
usually shows up, not a limit:
- `boot.initrd.availableKernelModules` and `boot.initrd.kernelModules` (LVM adds `dm-snapshot`)
- `boot.kernelModules` (`kvm-intel` / `kvm-amd`) and the microcode attribute
- the ESP's `by-uuid` device, and the device paths for `/nix` and `/persist`
- anything else it emits — `boot.extraModulePackages`, `hardware.*` attributes, `swapDevices`,
additional detected filesystems, `imports` such as `not-detected.nix`
The test is conflict, not familiarity: drop an option only when a nixfiles module already sets it,
and keep it otherwise. The flake's own modules cover the bootloader, `initrd.systemd`,
`initrd.services.lvm`, the kernel package and `nixpkgs.hostPlatform` (see
[`nixos/modules/common.nix`](../nixos/modules/common.nix) and
[`nixos/default.nix`](../nixos/default.nix)), so those are the ones to leave out. Don't paste the
file in wholesale either — translate it into the box's own style, and reference LVM volumes as
`/dev/main/<host>-nix` rather than the generated `/dev/mapper/main-<host>--nix`.
## Phase 5 — Write the box config
Create `nixos/boxes/<site>/<host>/default.nix` (a directory, so per-topic files can be added
alongside it later) declaring `nixos.systems.<host>`, and add its path to the `configs` list in
[`flake.nix`](../flake.nix). Then `git add` it.
The minimum is `system`, `nixpkgs`, `home-manager` and a `configuration` with the hardware from
Phase 4, the three filesystems, and networking. Beyond that:
- **Interface naming:** pin names to hardware with `.link` files matching `PermanentMACAddress`,
named for speed and index — `et1g0`, `et2g5-0`, `et10g-1`. Never rely on predictable-interface
names in the `.network` files.
- **Servers** set `my.server.enable = true`.
- **Secrets:** set `my.secrets.key` to the ed25519 public key seeded in Phase 3 (the key only, no
`root@installer` comment). Note that **every box declares at least one secret** even if its own
config declares none: [`nixos/modules/user.nix`](../nixos/modules/user.nix) adds
`user-passwd.txt` whenever `my.user.enable` is on, which is the default. So setting
`my.secrets.key` always adds the box to that file's recipients, and
`ragenix --rekey-one secrets/user-passwd.txt.age` is required — skip it and the box cannot
decrypt its user password on first boot. Confirm what the box actually declares with
`nix eval .#nixosConfigurations.<host>.config.age.secrets --apply builtins.attrNames`, and
re-encrypt each of those files the same way. Create any new secrets with `ragenix -e <path>`.
Never use `--rekey`, which rewrites every secret in `secrets/`.
- **A box staged away from its final home** gets a bootstrap `.network` taking DHCP, plus
`systemd.network.wait-online.anyInterface = true` so boot does not block on unpatched ports, and
an explicit `my.deploy.node.hostname`. Comment it as temporary and say what replaces it.
Validate with `check-system <host>` and fix eval errors before going near the target.
## Phase 6 — Install
⏸ The maintainer may want to run this step themselves; ask rather than assume.
`do-install <host>` builds the system's `toplevel`, `nix copy`s the closure into the installer's
`$INSTALL_ROOT` store, points `/nix/var/nix/profiles/system` at it, touches `/etc/NIXOS`, and runs
`switch-to-configuration boot` with `NIXOS_INSTALL_BOOTLOADER=1`. It prompts for confirmation and
prints the target it resolved.
- `--no-bootloader` skips the bootloader install (for a box that boots by other means).
- `--no-substitute` copies everything from the local store instead of letting the target substitute.
## Phase 7 — First boot and post-install
1. Reboot the box off the installer and confirm it comes up: it should get its address, and
`hostname` should be the system name. Its SSH host key is the one seeded in Phase 3, so it
presents the same fingerprint the installer did.
2. **Secrets.** If Phase 5 set `my.secrets.key`, they already decrypt. [`secrets.nix`](../secrets.nix)
computes the ragenix recipient list from that key at evaluation time, so nothing needs
regenerating — but any secret added to the box later must be re-encrypted for the new recipient
list with `ragenix --rekey-one <path>`, one file at a time. Never reach for `ragenix --rekey`:
it rewrites every secret in `secrets/` and buries the actual change in churn.
3. **Deploy.** `deploy .#<host>` should now work over the `deploy` user. If the box is staged
somewhere without its final DNS name, `deploy --hostname <address> .#<host>` overrides the node
hostname for one run.
## Phase 8 — Document it
Per [`AGENTS.md`](../AGENTS.md), a new box means:
- a box page under the right docs directory, following the standard layout (H1 + one-line intro;
`Source` / `Host` / `nixpkgs` bullets; hardware inventory; `## Role`; `## Network assignments`
linking to [`networking.md#box-assignments`](networking.md#box-assignments), or a short
explanation if it has none yet; one `##` per topic; `## Notable config files` last);
- a row in the site index `README.md` boxes table;
- affected prose in [`networking.md`](networking.md) — the assignment tables themselves are
CI-generated, so write the prose and leave the tables alone;
- the site diagram in [`README.md`](README.md) if the box changes its layout.
+4 -2
View File
@@ -34,8 +34,10 @@ The custom NixOS installer image used to bootstrap new boxes.
## Installing a box ## Installing a box
The devshell's installer commands ([`devshell/install.nix`](../../devshell/install.nix)) drive The end-to-end procedure — hardware inventory, partitioning, writing the box config, installing and
an install over SSH against a booted installer reachable at `$INSTALLER`: documenting it — is in [`install-box.md`](../install-box.md). The devshell's installer commands
([`devshell/install.nix`](../../devshell/install.nix)) drive an install over SSH against a booted
installer reachable at `$INSTALLER`:
- `installer-shell` — get a shell on the installer. - `installer-shell` — get a shell on the installer.
- `do-install <system>` — builds the system's toplevel, `nix copy`s the closure to the - `do-install <system>` — builds the system's toplevel, `nix copy`s the closure to the
+4
View File
@@ -266,6 +266,10 @@ On top of that: `p2pTunnels` (`10.100.5.0/24`) holds point-to-point tunnel /30s
public blocks and the per-customer `mail` / `darts` / `jam` prefixes carry customer-facing public blocks and the per-customer `mail` / `darts` / `jam` prefixes carry customer-facing
services with their own public addresses (announced by BGP, routed via the host). services with their own public addresses (announced by BGP, routed via the host).
This layout is expected to change: [`portcullis`](sites/colony/portcullis.md) is bare-metal edge
hardware headed for Nikhef that will take over most of `estuary`'s routing. It has no assignments
yet and the replacement topology is still being designed.
## home ## home
The home site prefixes (`lib.my.c.home.prefixes`) come from `192.168.64.0/18` and The home site prefixes (`lib.my.c.home.prefixes`) come from `192.168.64.0/18` and
+103
View File
@@ -0,0 +1,103 @@
# Flashing an OpenWrt box
Guided procedure for putting a flake-built OpenWrt image onto a box. The images themselves are
declared in [`openwrt/default.nix`](../openwrt/default.nix) and described in
[`deployment.md`](deployment.md#openwrt-images); the boxes are listed on their site pages (today
that is [fergal](sites/home/switches.md#fergal-the-openwrt-switch)).
Packages are baked into the image, so this runs whenever the package list changes — not only for
version upgrades. Work through the phases in order; ⏸ marks the point to stop and confirm.
## Phase 1 — Build
```sh
nix build .#openwrt-<box>
```
The result holds the `-squashfs-sysupgrade.bin` to flash, plus a `.manifest` listing every package
in the image and an SBOM. Check the manifest for the packages the change was meant to add — an
unknown package name is not an error at build time, it just silently isn't there.
## Phase 2 — Pre-flight
Confirm on the box:
```sh
grep -E 'RELEASE|REVISION' /etc/openwrt_release # what is running now
mount | grep -E ' / | /overlay | /rom ' # flash or RAM? (see below)
uci get network.lan.ipaddr # will it come back reachable?
cat /lib/upgrade/keep.d/* # what survives the flash
df -h /tmp # room for the image
```
**Flash or RAM matters.** A box booted normally shows a squashfs `/rom` plus a jffs2 `/overlay`;
one booted from an initramfs has `/` on tmpfs. The initramfs case has its own hazards — see
[Flashing from an initramfs](sites/home/switches.md#flashing-notes).
**Check the address is in UCI**, not just present on the interface. An address added by hand with
`ip` disappears on reboot and the box comes back unreachable.
`keep.d` normally lists `/etc/config/`, `/etc/dropbear/authorized_keys` and the dropbear host keys,
so an ordinary flash preserves both access and identity. Verify rather than assume — losing
`authorized_keys` on a box reachable only over SSH means a serial console recovery.
## Phase 3 — Back up
```sh
sysupgrade -b /tmp/<box>-config-backup.tar.gz
```
Fetch it with `ssh <box> 'cat /tmp/…' > local.tar.gz`. **`scp` does not work** — these boxes have no
`/usr/libexec/sftp-server`, so it fails with `Connection closed`. (`scp -O` forces the legacy
protocol if you prefer it.)
For a box being flashed off its **vendor** firmware for the first time, back up the whole flash
first — the vendor partitions hold per-unit MAC addresses and licence data that cannot be
regenerated. See [fergal's flash layout](sites/home/switches.md#flash-layout).
## Phase 4 — Stage and validate
```sh
ssh <box> 'cat > /tmp/sysupgrade.bin' < <image>.bin
ssh <box> 'sha256sum /tmp/sysupgrade.bin; sysupgrade -T /tmp/sysupgrade.bin'
```
Compare the sha256 against the local file, and require `sysupgrade -T` to exit 0. `-T` validates the
image and its device-compatibility metadata without writing anything, which is the last cheap chance
to catch a wrong-profile image.
## Phase 5 — Flash ⏸
Confirm before this point. It reboots the box and is not interruptible.
```sh
ssh <box> 'setsid sh -c "sleep 2; sysupgrade -v /tmp/sysupgrade.bin" \
</dev/null >/tmp/upgrade.log 2>&1 & echo detached'
```
**Detaching matters.** `sysupgrade` kills the SSH session partway through; without `setsid` the
upgrade dies with it, potentially after the flash has been erased. `nohup` is not available on these
boxes' busybox — use `setsid`.
Plain `sysupgrade` keeps the config in `keep.d`. Do not reach for `-c` out of caution: it needs
`/overlay/upper/etc` and aborts *after* erasing the firmware if that is missing.
## Phase 6 — Wait and verify
Poll SSH, not ping. A successful ping returns in milliseconds, so a naive "wait for it to go down"
loop finishes before the box has even started rebooting. Sleep between probes and wait on something
that only succeeds once userspace is up:
```sh
for i in $(seq 1 40); do
sleep 15
ssh -o ConnectTimeout=5 -o BatchMode=yes <box> 'grep REVISION /etc/openwrt_release' && break
done
```
Expect roughly three minutes. Then confirm the revision changed, the management address returned,
the package count matches the manifest, and the new packages are actually present and running.
Connecting without host-key overrides also confirms the host keys survived.
If the box does not return, it needs the serial console — have that confirmed as reachable *before*
Phase 5, not after.
+4
View File
@@ -24,9 +24,13 @@ prefixes and routing overview are in the [`colony` section of networking.md](../
| [`git`](git.md) | Gitea + Gitea Actions runner | | [`git`](git.md) | Gitea + Gitea Actions runner |
| [`mail`](mail.md) | Debian VM running mailcow (not NixOS) | | [`mail`](mail.md) | Debian VM running mailcow (not NixOS) |
| [`darts`](darts.md) | Third-party/customer VM (not NixOS) | | [`darts`](darts.md) | Third-party/customer VM (not NixOS) |
| [`portcullis`](portcullis.md) | Bare-metal edge box for Nikhef; being staged, not yet in service |
The applications running on `shill` are listed on its own page — see The applications running on `shill` are listed on its own page — see
[shill/README.md](shill/README.md#containers). [shill/README.md](shill/README.md#containers).
`mail` and `darts` are host-defined VMs whose guest operating systems are managed out of band; their `mail` and `darts` are host-defined VMs whose guest operating systems are managed out of band; their
pages document only what this repository controls. pages document only what this repository controls.
`portcullis` is new hardware headed for Nikhef that will take over most of `estuary`'s edge routing.
It is not deployed yet and the resulting topology is still being worked out.
+60
View File
@@ -0,0 +1,60 @@
# portcullis
A bare-metal box destined for Nikhef, intended to take over most of the colony edge
routing currently done by the [`estuary`](estuary.md) VM.
- **Source:** [`nixos/boxes/colony/portcullis/`](../../../nixos/boxes/colony/portcullis)
- **Host:** bare metal
- **nixpkgs:** `mine-stable`
## Hardware
| Component | Inventory |
|---|---|
| Platform | Mini PC (no vendor DMI strings) |
| CPU | Intel N150 (4 cores / 4 threads) |
| Memory | 8 GiB |
| Storage | One 128 GB NVMe SSD (`nvme0n1`), partitioned as a 2 GiB ESP plus an LVM PV holding the `nix` and `persist` volumes |
| Network | Four Intel I226-V 2.5 GbE ports (`et2g5-0`…`et2g5-3`) and one dual-port Intel 82599ES 10 GbE SFP+ card (`et10g-0`, `et10g-1`) |
| Management | JetKVM (HDMI/USB KVM with virtual media) |
## Role
Not yet in service. The eventual job is to be the physical edge for the colony site at Nikhef,
taking over most of what `estuary` does today — WAN termination, firewalling and NAT, BGP for
AS211024 and DNS. Some of that functionality stays on `estuary`, and the surrounding network
topology will change with the move, so the split is not settled yet. Until it is, the config in
this repository covers only what is needed to boot and reach the box.
## Network assignments
`portcullis` has no static assignments yet. It is being staged at home before it is racked, so it
takes DHCP on the home `lo` VLAN; the colony assignments land alongside the routing config once the
topology is decided.
## Networking
- The four I226-V ports are named `et2g5-0`…`et2g5-3` and the 82599ES SFP+ ports `et10g-0` /
`et10g-1`, pinned by permanent MAC address in `.link` files.
- Bootstrap only: a single `.network` matches every `et2g5-*` port and takes DHCP, so whichever
port happens to be patched in brings the box up. `wait-online.anyInterface` keeps boot from
blocking on the unpatched ports.
- kea registers the DHCP hostname, so while staged the box answers to `portcullis.dyn.h.nul.ie` —
which is also what `my.deploy.node.hostname` points at, since there is no colony FQDN for it yet.
## Storage
A single NVMe SSD, following the usual tmpfs-root layout: a 2 GiB ESP at `/boot`, then one LVM PV
in volume group `main` carrying `portcullis-nix` (48 GiB, `/nix`) and `portcullis-persist` (the
remainder, `/persist`).
## Secrets
`my.secrets.key` is the SSH host key adopted from the installer session at install time (seeded onto
the persist volume before first boot), so secrets could be encrypted for the box without waiting for
it to come up. The box declares nothing of its own yet — only the default `user-passwd.txt` that
`my.user` brings in.
## Notable config files
- [`nixos/boxes/colony/portcullis/default.nix`](../../../nixos/boxes/colony/portcullis/default.nix) — hardware, filesystems and bootstrap networking.
+59 -1
View File
@@ -11,7 +11,9 @@ carried untranslated because a single ONT makes it unique on the fabric — see
[the WAN path](#the-digiweb-wan-path-trunked-vlan-10--pvid-140) and [the WAN path](#the-digiweb-wan-path-trunked-vlan-10--pvid-140) and
[why not translation](#why-not-translation-for-one-ont). The router side lives in [why not translation](#why-not-translation-for-one-ont). The router side lives in
[river.md](river.md); the logical network map in [networking.md](../../networking.md). The Wi-Fi [river.md](river.md); the logical network map in [networking.md](../../networking.md). The Wi-Fi
APs that hang off these switches are in [aps.md](aps.md). APs that hang off these switches are in [aps.md](aps.md). A fourth switch, **fergal**, runs OpenWrt
and is on the bench rather than in the production path — see
[fergal](#fergal-the-openwrt-switch).
## The switches ## The switches
@@ -205,6 +207,62 @@ two ISP sessions never mix.
**CRS305-1G-4S+** (4×SFP+, same Marvell rule support) to land multiple ONTs and do the per-port **CRS305-1G-4S+** (4×SFP+, same Marvell rule support) to land multiple ONTs and do the per-port
translation there, feeding distinct fabric VLANs up to dave. translation there, feeding distinct fabric VLANs up to dave.
## fergal, the OpenWrt switch
An 8-port SFP+ switch — **XikeStor SKS8300-8X**, the board itself branded **ONTi ONT-S508CL-8S** —
on a Realtek RTL9303 (MIPS 34Kc, 512 MB RAM, 32 MiB SPI NOR). Unlike jim, dave and brian it runs
**OpenWrt**, so it is configured through UCI rather than RouterOS or a UniFi controller.
fergal is **not yet part of the fabric**: it sits at `192.168.64.30` on core (no DNS record yet),
still has the stock single-VLAN bridge with all eight ports untagged, and only one SFP+ cage is
populated. Treat it as bench equipment until that changes.
Its firmware *is* built by this flake — see
[OpenWrt images](../../deployment.md#openwrt-images) for the outputs and the feed pin. Packages are
baked into the image, so adding tooling means editing
[`openwrt/default.nix`](../../../openwrt/default.nix) and reflashing rather than installing on the
box.
### Flash layout
A single 32 MiB SPI NOR chip (`spi0.0`, 64 KiB erase blocks). `kernel` and `rootfs` are
sub-partitions of `firmware`, and OpenWrt adds `rootfs_data` as the JFFS2 overlay after a real
flash.
| Partition | Device | Offset | Size |
|---|---|---|---|
| `u-boot` | `mtd0` | `0x000000` | 1 MiB |
| `board-info` | `mtd1` | `0x100000` | 192 KiB |
| `syslog` | `mtd2` | `0x130000` | 832 KiB |
| `firmware` | `mtd3` | `0x200000` | 30 MiB |
**`board-info` is irreplaceable.** It holds the unit's MAC addresses (`[vlanmac]` / `[cpumac]`), its
`[license]` hash, the stock boot pointers and an SSH host key — only about 1.3 KiB of it is
non-blank, and none of it can be regenerated. A full dump of all four partitions, taken before
OpenWrt was flashed, is kept outside this repo — 33 MB of images, with per-partition checksums and
restore notes. Never write `u-boot` or `board-info` without a confirmed serial/TFTP recovery path.
### Flashing notes
The procedure itself is in [`openwrt-flash.md`](../../openwrt-flash.md); what follows is specific to
this board.
Stock u-boot boots `flash:/nos.img` from a JFFS2 filesystem, so OpenWrt's sysupgrade image is
itself a JFFS2 image containing `nos.img` rather than a raw kernel + squashfs. Two things bite when
flashing from an initramfs, as during the initial install:
- **`sysupgrade -c` does not work.** It needs `/overlay/upper/etc`, which doesn't exist when running
from RAM, and it aborts *after* `mtd erase firmware` has already run — leaving the box with no
bootable firmware until the job is finished. Pass the config as an explicit tarball instead
(`tar czf`, then `sysupgrade -f <tarball> …`).
- **The working management address may not be in UCI.** If it was set by hand with `ip` while UCI
still held the stock address, the box comes back unreachable. Write it into `network.lan` and
commit before flashing.
Neither applies to an ordinary flash-to-flash upgrade, where `sysupgrade` keeps `/etc/config` and
the files listed in `/lib/upgrade/keep.d/` by default. Dropbear host keys are regenerated by a flash
that doesn't preserve them, so clear the old `known_hosts` entry afterwards.
## Accessing the switches ## Accessing the switches
The switches resolve by **short hostname** on the home network — the home routers serve their The switches resolve by **short hostname** on the home network — the home routers serve their
Generated
+92 -9
View File
@@ -8,7 +8,7 @@
"ragenix", "ragenix",
"nixpkgs" "nixpkgs"
], ],
"systems": "systems_7" "systems": "systems_8"
}, },
"locked": { "locked": {
"lastModified": 1761656077, "lastModified": 1761656077,
@@ -315,6 +315,27 @@
"url": "https://flakehub.com/f/hercules-ci/flake-parts/0.1" "url": "https://flakehub.com/f/hercules-ci/flake-parts/0.1"
} }
}, },
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"openwrt-imagebuilder",
"nixpkgs"
]
},
"locked": {
"lastModified": 1772408722,
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
@@ -335,7 +356,7 @@
}, },
"flake-utils_10": { "flake-utils_10": {
"inputs": { "inputs": {
"systems": "systems_9" "systems": "systems_10"
}, },
"locked": { "locked": {
"lastModified": 1709126324, "lastModified": 1709126324,
@@ -353,7 +374,7 @@
}, },
"flake-utils_11": { "flake-utils_11": {
"inputs": { "inputs": {
"systems": "systems_10" "systems": "systems_11"
}, },
"locked": { "locked": {
"lastModified": 1705309234, "lastModified": 1705309234,
@@ -503,7 +524,7 @@
}, },
"flake-utils_9": { "flake-utils_9": {
"inputs": { "inputs": {
"systems": "systems_8" "systems": "systems_9"
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1731533236,
@@ -910,6 +931,51 @@
"type": "github" "type": "github"
} }
}, },
"openwrt-feeds": {
"inputs": {
"nixpkgs": [
"nixpkgs-unstable"
],
"openwrt-imagebuilder": [
"openwrt-imagebuilder"
]
},
"locked": {
"lastModified": 1787353688,
"narHash": "sha256-YDEm+ev3BpDS9Sq1ByVv0i5QQCE1yezpjWVhcNBBjCE=",
"owner": "devplayer0",
"repo": "openwrt-feeds",
"rev": "a30b2b5f83c7d1fffca2146453e8d5866b882da4",
"type": "github"
},
"original": {
"owner": "devplayer0",
"repo": "openwrt-feeds",
"type": "github"
}
},
"openwrt-imagebuilder": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": [
"nixpkgs-unstable"
],
"systems": "systems_7"
},
"locked": {
"lastModified": 1787302424,
"narHash": "sha256-fg9pKzO6OeQhe/bY2CpHb6QnHq57P/icwQz35GF/R/8=",
"owner": "astro",
"repo": "nix-openwrt-imagebuilder",
"rev": "276c1dd6346f50231392e97b3a9987c9dd57da28",
"type": "github"
},
"original": {
"owner": "astro",
"repo": "nix-openwrt-imagebuilder",
"type": "github"
}
},
"pyproject-nix": { "pyproject-nix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -976,6 +1042,8 @@
"nixpkgs-mine-stable": "nixpkgs-mine-stable", "nixpkgs-mine-stable": "nixpkgs-mine-stable",
"nixpkgs-stable": "nixpkgs-stable", "nixpkgs-stable": "nixpkgs-stable",
"nixpkgs-unstable": "nixpkgs-unstable", "nixpkgs-unstable": "nixpkgs-unstable",
"openwrt-feeds": "openwrt-feeds",
"openwrt-imagebuilder": "openwrt-imagebuilder",
"ragenix": "ragenix", "ragenix": "ragenix",
"sharry": "sharry" "sharry": "sharry"
} }
@@ -1073,6 +1141,21 @@
"type": "github" "type": "github"
} }
}, },
"systems_11": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": { "systems_2": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
@@ -1150,16 +1233,16 @@
}, },
"systems_7": { "systems_7": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1680978846,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=",
"owner": "nix-systems", "owner": "nix-systems",
"repo": "default", "repo": "x86_64-linux",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-systems", "owner": "nix-systems",
"repo": "default", "repo": "x86_64-linux",
"type": "github" "type": "github"
} }
}, },
+13 -1
View File
@@ -58,6 +58,15 @@
# harmonia.url = "github:devplayer0/harmonia/cache-config-daemon-store"; # harmonia.url = "github:devplayer0/harmonia/cache-config-daemon-store";
harmonia.inputs.nixpkgs.follows = "nixpkgs-unstable"; harmonia.inputs.nixpkgs.follows = "nixpkgs-unstable";
# Firmware building for the OpenWrt boxes, which aren't managed by this flake otherwise.
# `openwrt-feeds` pins the package feeds; without it, evaluation would reach the OpenWrt
# download server over import-from-derivation and break on hashes that upstream rotates daily.
openwrt-imagebuilder.url = "github:astro/nix-openwrt-imagebuilder";
openwrt-imagebuilder.inputs.nixpkgs.follows = "nixpkgs-unstable";
openwrt-feeds.url = "github:devplayer0/openwrt-feeds";
openwrt-feeds.inputs.nixpkgs.follows = "nixpkgs-unstable";
openwrt-feeds.inputs.openwrt-imagebuilder.follows = "openwrt-imagebuilder";
# Packages not in nixpkgs # Packages not in nixpkgs
sharry.url = "github:eikek/sharry"; sharry.url = "github:eikek/sharry";
sharry.inputs.nixpkgs.follows = "nixpkgs-unstable"; sharry.inputs.nixpkgs.follows = "nixpkgs-unstable";
@@ -181,6 +190,7 @@
# Systems # Systems
nixos/installer.nix nixos/installer.nix
nixos/boxes/colony nixos/boxes/colony
nixos/boxes/colony/portcullis
nixos/boxes/tower nixos/boxes/tower
nixos/boxes/home/stream.nix nixos/boxes/home/stream.nix
nixos/boxes/home/palace nixos/boxes/home/palace
@@ -258,7 +268,9 @@
deploy = recurseIntoAttrs (pkgs.deploy-rs.lib.deployChecks self.deploy); deploy = recurseIntoAttrs (pkgs.deploy-rs.lib.deployChecks self.deploy);
}; };
packages = flattenTree (import ./pkgs { inherit lib pkgs; }); packages = flattenTree (
(import ./pkgs { inherit lib pkgs; }) //
(import ./openwrt { inherit pkgs inputs; }));
devShells.default = shell; devShells.default = shell;
+115
View File
@@ -0,0 +1,115 @@
{ lib, ... }:
let
inherit (lib.my.c.colony) domain;
in
{
nixos.systems.portcullis = {
system = "x86_64-linux";
nixpkgs = "mine-stable";
home-manager = "mine-stable";
configuration = { lib, pkgs, config, ... }:
{
hardware = {
enableRedistributableFirmware = true;
cpu = {
intel.updateMicrocode = true;
};
};
boot = {
kernelModules = [ "kvm-intel" ];
kernelParams = [ "intel_iommu=on" ];
initrd = {
availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "usbhid" "sd_mod" "sr_mod" ];
kernelModules = [ "dm-snapshot" ];
};
};
fileSystems = {
"/boot" = {
device = "/dev/disk/by-uuid/1A70-EBCB";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
"/nix" = {
device = "/dev/main/portcullis-nix";
fsType = "ext4";
};
"/persist" = {
device = "/dev/main/portcullis-persist";
fsType = "ext4";
neededForBoot = true;
};
};
networking = { inherit domain; };
environment.systemPackages = with pkgs; [
pciutils
usbutils
ethtool
lm_sensors
smartmontools
];
systemd.network = {
# Only one port is patched in while the box is being staged, so don't block
# boot on the others coming up.
wait-online.anyInterface = true;
links = {
"10-et2g5-0" = {
matchConfig.PermanentMACAddress = "00:d0:b4:05:ed:48";
linkConfig.Name = "et2g5-0";
};
"10-et2g5-1" = {
matchConfig.PermanentMACAddress = "00:d0:b4:05:ed:49";
linkConfig.Name = "et2g5-1";
};
"10-et2g5-2" = {
matchConfig.PermanentMACAddress = "00:d0:b4:05:ed:4a";
linkConfig.Name = "et2g5-2";
};
"10-et2g5-3" = {
matchConfig.PermanentMACAddress = "00:d0:b4:05:ed:4b";
linkConfig.Name = "et2g5-3";
};
"11-et10g-0" = {
matchConfig.PermanentMACAddress = "60:be:b4:2e:9b:a2";
linkConfig.Name = "et10g-0";
};
"11-et10g-1" = {
matchConfig.PermanentMACAddress = "60:be:b4:2e:9b:a3";
linkConfig.Name = "et10g-1";
};
};
networks = {
# TODO: replace with the colony assignments and routing config once portcullis is
# racked at Nikhef. Until then it is staged on the home lo VLAN, so every 2.5G port
# takes DHCP and whichever one is patched in provides connectivity. kea registers
# the DHCP hostname, making the box reachable as `portcullis.dyn.h.nul.ie`.
"80-bootstrap" = {
matchConfig.Name = "et2g5-*";
DHCP = "yes";
networkConfig.IPv6PrivacyExtensions = "no";
linkConfig.RequiredForOnline = "routable";
};
};
};
my = {
# As above: no colony assignment yet, so point deploy at the staging DHCP name.
deploy.node.hostname = "portcullis.dyn.${lib.my.c.home.domain}";
secrets = {
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAUolR93Byg+Daw8pUYHVpQ34ioxSc2C8vzj9F4KbqMs";
};
server.enable = true;
};
};
};
}
+32
View File
@@ -0,0 +1,32 @@
{ pkgs, inputs }:
# Firmware for the OpenWrt boxes. They are not NixOS and are not deployed by this flake - these
# outputs only build a sysupgrade image, which is then flashed by hand (see the box's docs page).
#
# Baking packages into the image is the only reliable way to have them: OpenWrt's package server
# keeps just the current build of each feed, so a box installing packages at runtime is broken as
# soon as the feed moves on from the firmware it is running.
let
inherit (inputs) openwrt-imagebuilder openwrt-feeds;
# Fallback to the release branch. Snapshot tracks OpenWrt main, which is where the rtl930x target
# is actually being developed; the release runs a much older kernel. Both are pinned by
# `openwrt-feeds`, so neither moves until that input is updated.
release = "25.12.5";
mkImage = args: openwrt-imagebuilder.lib.build (args // {
inherit pkgs;
cachePath = openwrt-feeds.cachePaths.${args.release};
});
# fergal, the 8-port SFP+ switch (XikeStor SKS8300-8X, board-branded ONTi ONT-S508CL-8S)
fergal = {
target = "realtek";
variant = "rtl930x";
profile = "xikestor_sks8300-8x";
packages = [ "luci" "ip-full" "ip-bridge" "ethtool-full" "luci-app-sfp-info" ];
};
in
{
openwrt-fergal = mkImage (fergal // { release = "snapshot"; });
openwrt-fergal-release = mkImage (fergal // { inherit release; });
}
+78 -74
View File
@@ -1,76 +1,80 @@
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IHNqUFR5ZyBkWHB2 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IHNqUFR5ZyArcmVy
MHNUSnRIQkc4OENhN2dVdVlFZFRlWW5oVW9WbENaTGcwK2ZnQVFRCkQyYjdNaEtK aWRwblUvTDlpdzVjTy9GdmsxUSswVlBoa01WU0J0WjVPNTlaazJZCnpjYW14dkU5
SXE5RExTMm9EZC9GSEJGcWNabUgyOERBZmFmV0VqRFVXWTgKLT4gc3NoLWVkMjU1 RFBZS1B2V0J3U2ZkUzJCZlN3WUZ0QzUvRGc3QkUyL3VXRWsKLT4gc3NoLWVkMjU1
MTkgRExNZUZnIEhJNHN3c0lGL1lrMTA0ZHV2bHdPZ0dveDNBNVlzazEwbU9NcVZl MTkgRExNZUZnIGFFanNQbkFRQzJxcU5heE8xRlM2clZTaldSR3M2SzVyMHJDakFt
Z3RKelUKaWEwTTZvSTA0T2pKSGVoc3gwL3VPWjNPOFk3dTNjYVo5S2tPWUNtV3Fw eWNHU1EKcWFka3hQajV5d1NiaENUNFhOUlpoSFlJUm8xSWNXVE5HaGM3blp0OVAy
dwotPiBzc2gtZWQyNTUxOSAzYkIzWmcgMEN2dDJiRHYrQzRIb1JJVXp1V0ZyMkdu TQotPiBzc2gtZWQyNTUxOSAzYkIzWmcgdFNqcnJoWjh1SDN5Vml5UC8rZ1NXSi82
RWNtKy93QVR4SVJkK1VXTlUyQQpNdUU1c1NOdi9ZbmFTNHJBWmNTZFp2NDZORWp1 Sm11QXJKUXI5Ulc5Mi9rL3UxawpzSFVXQzBQbUZFM3l2aHlIU1dzREMvRXdrMWFL
ZzZzMzU3ZWFVYU1Lc1k0Ci0+IHNzaC1lZDI1NTE5IHErMFhjdyBiVS9ZeVE5Vytp cEptWW4yVWF6aTJTVUdBCi0+IHNzaC1lZDI1NTE5IHErMFhjdyBCTnhBMG5RcTdt
a3p2c2xYM28rM0Q1UWMyNkQxYWRScStGRGVhTTYvQWc4ClNvSksydmhid20yTzNC MHg5aVN2ZmZQR3VTcFo1K3lBMTh3c2dBRjlzRWdjM2pvCkpwZTFZVExJc25aLzFy
ZHF2b252MWwzRG9Zdi9uRVpqL1BacGRaemo5OEkKLT4gc3NoLWVkMjU1MTkgWkIz OWZsNjVzMmNRREJ4ME56TVRneEZIdUdqODVZZnMKLT4gc3NoLWVkMjU1MTkgWkIz
ZTZRIFVZUTVjNS9pak9JSnF4N2JOMEZINmtKTzU5OUxHbHRKeng2cldvK2NXU2sK ZTZRIDlvYURqSFRVNUdEM1lIV3oxQ00zMG5CNXIyNnhrVXpFd3VhS0IwTDhqRlkK
T0M3QTZJU2lqMk9nRGl2c3QwNTlWOEwyYVJUK3pleEtMZ0lYbm9TSmVUZwotPiBz UzhsN3hLSUpQZ2lrNHNVd0s1aXBIY3ZaVm0rZjlXU0RSbU1Bb2h2NlBNawotPiBz
c2gtZWQyNTUxOSBqNjdGWFEgRzRXYTBxQWduN2QvZk84NXVWVHlQN2RiS0pkYXM2 c2gtZWQyNTUxOSBqNjdGWFEgaUsxSzNGS09nMGo0eXlsUVdDL2R5UjVXYm9PZ3R1
U2cvM0JKRXZvTjAxdwpCbWdMZVpMaXBBNHRRZjN4aU5lNmhRNmMzSnBIWUNtbW1w R0cra1JWbldnREJ3UQozeVYveGNUTExDUEJjT254NGlzTGxLSkl2akpqRnVmWU0y
ZStLaXlUL09ZCi0+IHNzaC1lZDI1NTE5IGMwVE5hUSBxK09aSTRaUzZ4VnArMlF4 Z09oZFQ0YnFzCi0+IHNzaC1lZDI1NTE5IGMwVE5hUSBIQlZSU3VOYythUHo2NERV
ZFZvNmUrR1hPKzB1SUdRS2Z2dmgyVlROOUVZCllUcDNCSEpVUmVUN2RSYjZ5aDdp cE1rbDNlazMzZk1CMlJaM295K09POUZUc1dJCkZ1Ui9DZ2JsUm9FWithQmMwcHpm
SWhCVFlwcWxJMkxodEwrQXNDOTh0TlUKLT4gc3NoLWVkMjU1MTkgbjhDcFV3IHJ4 SkNTcEtpSWVJdjJoZG1KY29hSEIvdDQKLT4gc3NoLWVkMjU1MTkgbjhDcFV3ICtP
Y1krN2hHTjhOakZSZ3VNNEZnYVVLb1BTZ01iT2dyRjdUdkRodit0QUkKRitpVFJB dUUwMGx4WEtkQ21ySEFMREVXMHJaSitPalkxdXpPZTJJczZsUEI1MWMKVzRaOE10
WDU2eGw2aUhQZFcrMTB6MU5VTURPNE5HbUFYendOMnNDRXRQcwotPiBzc2gtZWQy MEhjSHFVZW1RKzVDNnBYV2EyQXFTc2ozcVZlb0g4Z20zV3hROAotPiBzc2gtZWQy
NTUxOSBWN2xnR3cgcUNRZkp6R0llR0o0RmhvanBoamdoNVBKRWl0Sm1sQVhBRGJw NTUxOSBWN2xnR3cgSmY1VEE1TjVROTU2U3lvMFNuaVhKNXlrNm1GV3NPSGtIZzhJ
MFFDcmdUbwpENlEvaTh4OUhVQ0VTeXBGMTBoajd1eE42YzYvc0ZvcWlVYU1HWnkr YkVNV1hXSQpZVzhlMnd4OTViN1ZmZlI2TkRMZ29sSU8rY0ZxcUhxY1U3UlM5MHRi
R0FnCi0+IHNzaC1lZDI1NTE5IGpJOFJBZyBFdEpIT3F1MGVtZ0ppN05hVDBLRjZz N0VBCi0+IHNzaC1lZDI1NTE5IGpJOFJBZyAybmhuVGIzd1V3cCtoS2UxdVJ5S1p6
eTRWdjZkMGM0SWpqeVJpRXRGc2pRClVpTlB6Q3lCZ3FXd1g1eXU3Y3grRVBJRjBC UmowTEpvSDA2Mjl3Q0dBeENhUXlJCmFCWkZ5VVBKUnRtTGlvcUtMblJWNlVPTHRa
aERTanp4dDhGRFdteThNNTgKLT4gc3NoLWVkMjU1MTkgVCtzYkdBIEpselBValht RWdJY3kyL2dyUTV4Y09CWTgKLT4gc3NoLWVkMjU1MTkgVCtzYkdBIFE0R1hTaVAr
TTNtY2lTYWg3VHBUTWNIemdiQXpHd01jMS9BeERMclQ0RGcKYU85dnN3ZEJyQmZZ aVBObnNVdkx2YVJ4TTJKYk9QUVhEbXlFQW1ESXVmWTBSbDAKVkJwRlZNVlBwNVJx
ZE9ZYXl4Ym1BZlBkcm9jMXBhZ1J3aUlxR1dPNm96dwotPiBzc2gtZWQyNTUxOSBo WE9vVjl2OXFQcnZUTCtSS0NVQ1dFUmJXaDlhcVYwSQotPiBzc2gtZWQyNTUxOSBo
TWE0bncgcXdIK21EVDVMZGhMMEltQ3RNbkx5NDhGVWRVdU4wZlhUNDY0bEZTcEZG TWE0bncgc0o4b2VsUDZsRDFlaFlJWEpkNU5jRERnWGJ0blJMbXhkR3RWQ1gxNkpB
QQpUSGZOb0FoSTgxckp0R3dxVjVPZkQ0b3Z5WXpjU1Q1Qy8zaGNVNzh4ZGJvCi0+ dwp4YU1WdGpJNUtQc09lRDdFVXNZdThWUEVnZDFvdVhUaW1JZUdQaGlId0NRCi0+
IHNzaC1lZDI1NTE5IGV5cTNkZyBwaGF3NXVNWUVQUUpuUm9pVHVRK1NoV1FmVFMz IHNzaC1lZDI1NTE5IGV5cTNkZyBJTzdrSFdVTERDVk5hNHZsL1N6U010Y1ltZkpY
dys1bE9WaDdNV0RXZFRVCmVJSUx0VkQvbDRjOWdvNlhCNm9RSnhnUHFpYnp0ZjVM VS9vUXl6N1FDV3pvWUhNCm1UUHZNTUlrMjdybkVEaWV1NmVlY2hFeklJZndFQUpP
UG82UElhM3R6MFkKLT4gc3NoLWVkMjU1MTkgN1dROVBBIERKYnFGQm9pVlIzYWxu VGRNbzZ2SXVPVncKLT4gc3NoLWVkMjU1MTkgN1dROVBBIDdzKzBGUGNXZTYramx2
M3Fza0RucE9SczQzRk5ialU2R215L1NwcVU2bW8Ka2hCL2JSU1c1bEhFS0t3VnEy VnAzYlcvOTZ6MTRtSGZTRFY0SFluQWMwTVdsMncKTG83dWdTbldMbnRBMVkwSnBu
YWtaMG5mZHBxYjNkK0JQUjlmVHJYSmNUMAotPiBzc2gtZWQyNTUxOSBnU3hQMFEg QkxDTUhhUERXeUl2UXVEaldvRjZtQXZsbwotPiBzc2gtZWQyNTUxOSBnU3hQMFEg
VHhRN0VEV0xGL0hJUHd1V0drWVJzaWtucXJ2c2xMUVpYMFc2TklLUGdYVQp0VWZj SnBrUFhPa3dmMC84V1E0VVlyUC9VME1HWThPaDNxKzZLZC9Ybnd5aHdsYwpxQk1m
UEZGeGhaRDh4SmFsek93ejBUM1A3OVorTWFmcWt0QkVCZmV0QU5ZCi0+IHNzaC1l SW9TY2NOSExZeVJGY3NUbVcvMm5OUytjUnhJR1ZFV1NZUDRqdnRrCi0+IHNzaC1l
ZDI1NTE5IFZGY3c1ZyBmOHgraG50b2NiREZIcjRacTUwZ1Z3R2h0QXlHMTl6SFNJ ZDI1NTE5IFZGY3c1ZyBac29qNmN5aGhpNDVVRHNsc1NIYzViZUdZK2tnSzFTc3pr
Qlc4MTFtT2k4CkVSdzY3cEVpR3J3L0szMXBVdmd2OUFsWGJwanRtSGl3QmRyREhJ SWxFcXFONzNrCmxRK1haWkJ6aXU3amNPZ2hlMnovUDllTGMvSGZiTnNJWjhZN2k3
WjRwS2sKLT4gc3NoLWVkMjU1MTkgaGtidHZnIGVVNDZzenBDRlRmeW4ybUJqamRD VnFmMzgKLT4gc3NoLWVkMjU1MTkgaGtidHZnIG41VkQyQW9rcmY2N2E2Qmc2bUds
UFFpWDY1ZjJuK1VQNWJJSGIyaFJnbmcKcmg3b284YmZQUWt0clBjVDZVZk5CUUlo bWpUSWsxVHMvS2ZGUXhvSjNnanA5d0EKRE1IQ3UzNWE4VFdaQXZGakNscEE3RkQ3
aFRWWUwzVmVPcFBDaW1xKzRqSQotPiBzc2gtZWQyNTUxOSBldDJ6cFEgWEZpOXdx V2FESTdIWGRFOWV2QmpuaWZOYwotPiBzc2gtZWQyNTUxOSBldDJ6cFEgM1NwL3Jt
bWo3NnZYSjFTdldoSDBBMVVobHRXYWJjZEd2RVBIanRrQUZROApkZG82RUZHSkRH WDBHQmErN2JzNUVTelVqb1dRMzlha3NYVEVvRGRrMW9jN0ZoWQpkZ3N1TUExRVhz
TGkyTG1tRUlRMEg5MVRwRHNjTE9UL1BMRUpDdXVLZ2tVCi0+IHNzaC1lZDI1NTE5 aEE1QXFEbytySkdlcmpyV0JvRzFpRUZJc0VlenhBNDg0Ci0+IHNzaC1lZDI1NTE5
IFpiTEpXQSB0ZWtnRFVWKzRkSU45enFadkx6N3FpRmE1MnByZEljd3FyWGFyd2Ja IENrT1RXUSBCSU8xbTBNaXRqK0kzZVZOUGo3ZmZYd09sMTd6UHJvU2t5SUJBams1
QVNvClpiZkxsQ04zR2pzOXBtODdnbjdSaHBtSVFVT3V2aFdyZ2FoaytISmNzR0UK R0FjCjEzSEN0ZXd2NmI1M2xvWG1CRTNtcUZZZHhjOVpqemNXS3ByQWtmSy9MdmcK
LT4gc3NoLWVkMjU1MTkgWk5xSW9nIHMvWElOMTgvR0ZveHZLSlVqNW9PSmNvdm92 LT4gc3NoLWVkMjU1MTkgWmJMSldBIEx6TUdIT1I4VEIzbXFBdWh0eFZKSnN5R2pZ
ZW92dHhvbG8vRzl2NFdqMFEKUktLWEJuQ25LM1J6OXBvRlRlMEFEeXlhV3M1Yk85 VjhYejVtczZSTko5Ty93M2cKNUlBUHFKd0JwNFFHYk9pcnpTcVYvWmZrL3BIWDUx
Wk0rZWJMQ3U2SVYrcwotPiBzc2gtZWQyNTUxOSBxTGpxeVEgODlpYkhNQmFkNjlp cW9vOEpkM1J1emorSQotPiBzc2gtZWQyNTUxOSBaTnFJb2cgY1VUU3BJeFBRYkN4
ZUYyZ0VDdzBsTmk5TGVPU0VTRnZlSUdMazN3cXB4awpGbEhWMVR0N0NzTGljeEpw aWFsWEVWL2NHenYxODcvcnJHZXhRbERFU3YzQlZ4UQo4Z000SGJia01MdHpQU1kw
dWtrTXR0QW5CVHE5enA2dXZZNm55ajVSa3NVCi0+IHNzaC1lZDI1NTE5IEJhUWxS VjRLWXhGczJNWWNMdVhWV09TUXB2UE1PejZZCi0+IHNzaC1lZDI1NTE5IHFManF5
ZyBJc2xSZUJkaEd5U1EyV0J6T2pUU1ZWMnRPSzYwNFRERndsdThYeFFQSUVJCmlT USAzOWRKUTZDVTVpVlFuWGgrdlNYN2RBQXArbFNoN1k4OWxQR0VIZHRUWXlvCi9Y
OWh1dnpNRDU5WlNSaW5FTUZ5QVVHSmw0NUpQMklOb3JYU3FSM3ZTWEUKLT4gc3No blkrellUVVROYUMyWHdBK1NHdjQ5YWs2blpvbS9JZVkybUVPbFRxb2sKLT4gc3No
LWVkMjU1MTkgcytxUmZnIERvYU1PNXJkWEs0VkI5YWNuY3ZGK2xpVkpxN01zbHA2 LWVkMjU1MTkgQmFRbFJnIDhoR0lXTlNLQ3Z2WTZXQUFlQ25odmJPeFI1TWIyWUlv
cm9DUzk1WHIrRWsKZVRGWUwrdmVnOTh4clFHdm1yL0JuSVRpVldjWkMwUkdFTk5J SG02SjFYR29HQ1EKKzlqaTdMWmpwVCtQWDFDZlk3aXQ0L2t3YkZudHAzSC9WK2Vr
LzNlT0dZUQotPiBzc2gtZWQyNTUxOSA2MkpjY0EgTXI5QVZySXpsQlVoTE51c29j L3RXbW44SQotPiBzc2gtZWQyNTUxOSBzK3FSZmcgYzFia3NxQWJvOFFqa3g2ZkFL
MWltaUZHZUlPUEo1WXZNdUUzSWdWRU94MApMVUYzYlhNZDc5RTRzc0NxNW5PblU0 YVlXOTRzdTFUZWIvV1piM2RDSDh0Z21nUQptQ1dSSC83Wnd4cnJQcDNPRlhtV24v
WndIV1FZTm51ZlhQdWFQa2NNS25FCi0+IHNzaC1lZDI1NTE5IC9oeC9kQSBWUkJS THZ6bUd6Q25SU0Q4b3R6Y0d3dkQ0Ci0+IHNzaC1lZDI1NTE5IDYySmNjQSBCcFhE
ZmR2a3BDdHRrVUhqejFjZFI4cWw1MEkrRWUwdHZHZEloemtUT2pBCmFYU21lRllo SUVnRzZCQitpNjd1S0h4VmxWSzBDMlkrbURLVlZ5ZzIzWE1TUlZrClF0bVh3UU5k
MTlic204cU41Vi95dFBMSSs2eWtVSzJndG1keWdVeUgxNFEKLT4gc3NoLWVkMjU1 TFRvWlc2Z2pXMzhiY3ZyN2g3akhQa25zY3NhSEhWV05DUFEKLT4gc3NoLWVkMjU1
MTkgSEovSjdBIGhhck53d1ZzYVY5ZjF5VHpXYVBDMGZ5SGdTL3B4NHQ2a3lENGti MTkgL2h4L2RBIHc0UWp1WEdNWnJsaUpVTmU5Q2tITGNNYkRFcEllUENTMDgyOCtG
NXhZVGMKV3B0d0IwNm5qM0xDUWdOTEZ3Q3ErWGdNRmtIeTBMSjV1eDYzMUVYVFpY NCtIM1kKVmV2MjB1WlAva0xKMDdrWE43NUV1YXNOc0FTV0NNbnZuaXpVTWhvc2ZL
cwotPiBzc2gtZWQyNTUxOSBPRXFNc2cgcm9ZOS81emFxd2toNTRFUW1LRi9jU3FF bwotPiBzc2gtZWQyNTUxOSBXekxHSEEgdmJRZU5SVCsxelMxOXIwZ3FLeEhlYURX
VUdYRzRWTm5uV0ZjclJPZmsyQQoxZnRtTzZ5YzRJTVRGalU4NFZhQmZlMjhtY2Nv cmptKzQvZkZUZVJzM1h0UENYbwpTempycGIrU3J0b0FvaTJOek5VZ2RzeTA5NGRC
Q1oyZURhQUpjR2dvRVNRCi0+IHNzaC1lZDI1NTE5IC9FSlh2ZyA5aUNiQk1FZUtU N1JYY3hMYVVLM1diZE44Ci0+IHNzaC1lZDI1NTE5IEhKL0o3QSBSRkZyTXJ1djhJ
S0hta1lOMVlWL1RwdjUvQnl2MTg5VWQrMnNCVERkVkZrCkFraTR4UmFBbXpOR1lq aWt4Mk9iaitSSTdtWk9Eeklvb1VLU2oxVmQ1L0NDYUZzCkdIcmJBQ0RqbFlDdHh4
TTFCSmZmV0R1VjhWb2V0RXdiYkpaek0rdGVsRE0KLT4gWDI1NTE5IEpZSlNOZk5D a0o3Ujd2ZEZPbFJKOFozS3UxR3lldERYaVY5S0EKLT4gc3NoLWVkMjU1MTkgT0Vx
WlZ3VFFpVUx2RHlwblVEZHZyVVNGbHNrZ3hUY1FYQUJNMWMKYlo4dFVnS2hhYk1m TXNnIHgwbTFxV3ZrR0h2SHBocEluZTZmSnBmUStKMkNtRVhTZTJNeVM3akZjVUEK
THZXMktudExKdEE1enlGWUgyM3FiMGpFbmR6RkNyawotPiBGImFlO2V5Ky1ncmVh V3FKZGlxMmw3QzB3VnAxUXBnK2RzdUNUU3MwSHVIcXg3UHFVSyt1QklhSQotPiBz
c2UKdTZXVjRtcTR2TDFITzB1d3J3RG1hejk1am15SEswR05PMFdoTXR2UVpoRE9H c2gtZWQyNTUxOSAvRUpYdmcgSVdJOVdaNW9PdVY5TE9iNjFlY0ZQN001dnNENWR6
dkMvQldlc1FPWlRFSURXN3ppSQoyU0pSRHFIS2I3d1dtTE5OTFFXSTYyR2tTbTZB cU5DSTVpcEMvRHpSUQo5SnZmMmRCdldKQ2VCSC8zaG1yTVMra2p3QkZHNVF3WmNR
RDVROStRCi0tLSAvWEcyR09pQnRhSzN4d2kzSzduOUVtTXd2U25CWmZJdWt5NnNl a2VMNlJpRXNJCi0+IFgyNTUxOSBYU3VVMkttYUc5NVpBU0FSZk5MNXIvRVIxd0w5
RHVieGNBCi411IjgmUKttjX6ljaZGWivstOajx2pkTVLV/zFiEj3jv+KDGy1psZQ RHZxcmdLb2dmaVlTWGxZCm9lNUloSGJxcmYvcFVkc3dDNXNmUkZNbXB0elpwMVBB
no+eatGMO8LeJhGJ6H7TBKOmJhFMfoQp1XKJA8OGY+FGZ98bit04djo3jqbVSOms dWdpSTJZQlgwUk0KLT4gPipeLWdyZWFzZSA8ZCBNcmxSP1BrCjZpcVoxcFNmV2Zi
JSPRTvTxxQx+40yO+ETV+2qkRU1OdJTobz9YvuqGlHrJS8UNN30QMPT0ienu3QTY K0pxeHlKSmJ0ZWNIdm5mYk1Wano5VHZBb2wxTExxT0dKajFNMnZGU1JYODhXT3Jy
Tbo= MElGNG4KUEh2V0MvZkg5UmN3SVJEajQ3NVhXUk5YWFZPVnFyZkpsRjhnCi0tLSBS
Ulh4WkhVY3BuYXFicjhoN29DbFJwemlrczFQcUxpMTNaRXFLOWc2YTJFCvATVcNx
h0TZBbnm6QBWZVNDRiU8yHFAgaS/25pRvcaixnji3NkeKYYuEEnVSw6oUthhVSSg
g222QeHfXortX7m+/zTD0uIhdVm7e+emA8LBxsQEOgeDy33XA3Hi9yX2BFGV7l82
NTBlLbCiH0mlFZ6ZO8rtA/nMcriCQUb2QZ+TaSGAop4RHObEeFw=
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----