From 074d5f5f25cb05c951dc183a99d2aad2493304ca Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Thu, 22 Jan 2026 16:10:55 +0000 Subject: [PATCH 01/23] Change butter site location for kanglam --- ansible/deploy-butter-site.yml | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/ansible/deploy-butter-site.yml b/ansible/deploy-butter-site.yml index c926661..d31b261 100644 --- a/ansible/deploy-butter-site.yml +++ b/ansible/deploy-butter-site.yml @@ -33,8 +33,8 @@ - name: Download the butter-box UI zip file get_url: - url: "https://likebutter.gitlab.io/butter-box-ui/site-{{ butter_language }}.zip" - dest: /tmp/site.zip + url: "https://guardianproject.dev/api/packages/butter/generic/butter-kanglam-ui/latest/kanglam-ui.tar.gz" + dest: /tmp/site.tar.gz mode: '0644' - name: Ensure /tmp/butter-site directory exists @@ -43,17 +43,28 @@ state: directory mode: '0755' - - name: Unarchive site.zip to /tmp/butter-site + - name: Unarchive site.tar.gz to /tmp/butter-site unarchive: - src: /tmp/site.zip - dest: /tmp/butter-site + src: /tmp/site.tar.gz + dest: /var/www/html remote_src: yes + extra_opts: [--strip-components=1] - - name: Copy contents to /var/www/html/ - copy: - src: /tmp/butter-site/ - dest: /var/www/html/ + - name: Set permissions for /var/www/html/ + become: true + file: + path: /var/www/html/ owner: www-data group: www-data mode: '0755' - remote_src: yes + recurse: yes + +# - name: List files in remote directory +# ansible.builtin.find: +# paths: /var/www/html +# file_type: any +# register: dir_contents +# +# - name: Print directory contents +# ansible.builtin.debug: +# msg: "{{ dir_contents.files | map(attribute='path') | list }}" From 41e5269ae7943d5ef0254dd1a5b17dc1bb460b2c Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Fri, 23 Jan 2026 15:14:05 +0000 Subject: [PATCH 02/23] Add basic recipe for an amd64 machine --- vmdb2-recipes/amd64_trixie.yaml | 179 ++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 vmdb2-recipes/amd64_trixie.yaml diff --git a/vmdb2-recipes/amd64_trixie.yaml b/vmdb2-recipes/amd64_trixie.yaml new file mode 100644 index 0000000..d5e15ac --- /dev/null +++ b/vmdb2-recipes/amd64_trixie.yaml @@ -0,0 +1,179 @@ +--- +# See https://wiki.debian.org/RaspberryPi3 for known issues and more details. +# image.yml based on revision: ff7fdbf (Switch from qemu-debootstrap to debootstrap., 2024-01-01) + +steps: + - mkimg: "{{ output }}" + size: 3100M + + - mklabel: gpt + device: "{{ output }}" + +############ efi + - mkpart: primary + fs-type: 'fat32' + device: "{{ output }}" + start: 1MiB + end: 132MiB + tag: efi + + - set_part_flag: "{{ output }}" + tag: efi + flag: boot + state: enabled + + - set_part_flag: "{{ output }}" + tag: efi + flag: esp + state: enabled + +############ bios grub + - mkpart: primary + device: "{{ output }}" + start: 132MiB + end: 133MiB + tag: bios_grub + + - set_part_flag: "{{ output }}" + tag: bios_grub + flag: bios_grub + state: enabled +############ live + - mkpart: primary + device: "{{ output }}" + start: 133MiB + end: 100% + tag: tag-root + + - set_part_flag: "{{ output }}" + tag: tag-root + flag: legacy_boot + state: enabled + + - kpartx: "{{ output }}" + + - mkfs: vfat + partition: efi + label: EFI + options: -F32 + + - mkfs: ext4 + partition: tag-root + label: boot + + - mount: tag-root + + - shell: | + dd bs=440 count=1 conv=notrunc if=/usr/lib/syslinux/mbr/gptmbr.bin of="{{ output }}" + root-fs: tag-root + + - unpack-rootfs: tag-root + + - debootstrap: trixie + require_empty_target: false + mirror: http://deb.debian.org/debian + target: tag-root + components: + - main + - non-free-firmware + - non-free + unless: rootfs_unpacked + + - create-file: /etc/apt/sources.list + contents: |+ + deb http://deb.debian.org/debian trixie main non-free-firmware non-free + deb http://deb.debian.org/debian trixie-updates main non-free-firmware non-free + deb http://security.debian.org/debian-security trixie-security main non-free-firmware non-free + + unless: rootfs_unpacked + + - apt: install + packages: + - avahi-daemon + - curl + - udisks2 + - wget + - dhcpcd + - python3 + - lighttpd + - unzip + - sudo + - systemd-timesyncd + - ca-certificates + - dosfstools + - iw + - parted + - ssh + - wpasupplicant + - systemd + - systemd-sysv + - init-system-helpers + - syslinux + - linux-image-amd64 + tag: tag-root + unless: rootfs_unpacked + + - cache-rootfs: tag-root + unless: rootfs_unpacked + + - shell: | + echo "butterbox" > "${ROOT?}/etc/hostname" + + # Allow root logins locally with no password + sed -i 's,root:[^:]*:,root::,' "${ROOT?}/etc/shadow" + + install -m 644 -o root -g root image-specs/rootfs/etc/fstab "${ROOT?}/etc/fstab" + + install -m 644 -o root -g root image-specs/rootfs/etc/network/interfaces.d/eth0 "${ROOT?}/etc/network/interfaces.d/eth0" + install -m 600 -o root -g root image-specs/rootfs/etc/network/interfaces.d/wlan0 "${ROOT?}/etc/network/interfaces.d/wlan0" + root-fs: tag-root + + # Clean up archive cache (likely not useful) and lists (likely outdated) to + # reduce image size by several hundred megabytes. + - chroot: tag-root + shell: | + apt-get clean + rm -rf /var/lib/apt/lists + + - grub: bios + tag: tag-root + console: serial +# + - grub: uefi + tag: tag-root + efi: efi + console: serial + + - shell: | + rm "${ROOT?}/etc/resolv.conf" + root-fs: tag-root + + # Clear /etc/machine-id and /var/lib/dbus/machine-id, as both should + # be auto-generated upon first boot. From the manpage + # (machine-id(5)): + # + # For normal operating system installations, where a custom image is + # created for a specific machine, /etc/machine-id should be + # populated during installation. + # + # Note this will also trigger ConditionFirstBoot=yes for systemd. + # On Buster, /etc/machine-id should be an emtpy file, not an absent file + # On Bullseye, /etc/machine-id should not exist in an image + - chroot: tag-root + shell: | + rm -f /etc/machine-id /var/lib/dbus/machine-id + echo "uninitialized" > /etc/machine-id + echo "LABEL=BOOT / ext4 rw 0 1" > /etc/fstab + + + - virtual-filesystems: tag-root + + - ansible: tag-root + playbook: ../ansible/main.yml + config_file: ../ansible/ansible.cfg + extra_vars: + butter_language: en + butter_name: butterbox + tags: base,usb,matrix,keanu,website + butter_user: "amd" + ap_mode_supported: "false" From 0c0e770fca782e0b12c1275644e36cb8b09e1cc6 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Thu, 5 Mar 2026 09:24:59 +0000 Subject: [PATCH 03/23] Ensure hard drives are not mounted as storage media, add recipe for kanglam --- ansible/install-usb-viewer.yml | 2 +- ansible/templates/99-usb-butter.rules | 11 ++ vmdb2-recipes/kanglam_pi4.yaml | 197 ++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 ansible/templates/99-usb-butter.rules create mode 100644 vmdb2-recipes/kanglam_pi4.yaml diff --git a/ansible/install-usb-viewer.yml b/ansible/install-usb-viewer.yml index 820dc5f..8f84caa 100644 --- a/ansible/install-usb-viewer.yml +++ b/ansible/install-usb-viewer.yml @@ -36,7 +36,7 @@ - name: Install udev rule copy: - src: "{{ vmdb2_config_base_dir }}/99-usb-butter.rules" + src: "templates/99-usb-butter.rules" dest: "/etc/udev/rules.d/99-usb-butter.rules" - name: Install udev trigger script diff --git a/ansible/templates/99-usb-butter.rules b/ansible/templates/99-usb-butter.rules new file mode 100644 index 0000000..4f378a1 --- /dev/null +++ b/ansible/templates/99-usb-butter.rules @@ -0,0 +1,11 @@ +# Using udev to mount newly attached usb drives doesn't work. +# https://unix.stackexchange.com/a/507150/223286 +# So, we depend on udisks to mount the disk. *Then* we want to +# to setup the symlink and lighttpd config with our script. +# We can run the script immediately because it waits for the disk +# to be mounted. + +# Mount newly inserted drives, creating the directory if it doesn't exist +ACTION=="add", KERNEL=="sd[a-z][1-9]", ENV{ID_BUS}=="usb", TAG+="systemd", ENV{SYSTEMD_WANTS}+="udisks2-mount@%k.service", ENV{SYSTEMD_WANTS}+="serve-usb@%k.service" + +# When the disk is `unmounted, the symlink will just point to a now-empty directory. diff --git a/vmdb2-recipes/kanglam_pi4.yaml b/vmdb2-recipes/kanglam_pi4.yaml new file mode 100644 index 0000000..4cc244e --- /dev/null +++ b/vmdb2-recipes/kanglam_pi4.yaml @@ -0,0 +1,197 @@ +--- +# See https://wiki.debian.org/RaspberryPi3 for known issues and more details. +# image.yml based on revision: ff7fdbf (Switch from qemu-debootstrap to debootstrap., 2024-01-01) + +steps: + - mkimg: "{{ output }}" + size: 3100M + + - mklabel: msdos + device: "{{ output }}" + + - mkpart: primary + fs-type: 'fat32' + device: "{{ output }}" + start: 4MiB + end: 512MiB + tag: tag-firmware + + - mkpart: primary + device: "{{ output }}" + start: 512MiB + end: 100% + tag: tag-root + + - kpartx: "{{ output }}" + + - mkfs: vfat + partition: tag-firmware + label: RASPIFIRM + + - mkfs: ext4 + partition: tag-root + label: RASPIROOT + + - mount: tag-root + + - mount: tag-firmware + mount-on: tag-root + dirname: '/boot/firmware' + + - unpack-rootfs: tag-root + + - debootstrap: trixie + require_empty_target: false + mirror: http://deb.debian.org/debian + target: tag-root + arch: arm64 + components: + - main + - non-free-firmware + - non-free + unless: rootfs_unpacked + + - create-file: /etc/apt/sources.list + contents: |+ + deb http://deb.debian.org/debian trixie main non-free-firmware non-free + deb http://deb.debian.org/debian trixie-updates main non-free-firmware non-free + deb http://security.debian.org/debian-security trixie-security main non-free-firmware non-free + # Backports are _not_ enabled by default. + # Enable them by uncommenting the following line: + # deb http://deb.debian.org/debian trixie-backports main non-free-firmware + + unless: rootfs_unpacked + + - copy-file: /etc/initramfs-tools/hooks/rpi-resizerootfs + src: image-specs/rootfs/etc/initramfs-tools/hooks/rpi-resizerootfs + perm: 0755 + unless: rootfs_unpacked + + - copy-file: /etc/initramfs-tools/scripts/local-bottom/rpi-resizerootfs + src: image-specs/rootfs/etc/initramfs-tools/scripts/local-bottom/rpi-resizerootfs + perm: 0755 + unless: rootfs_unpacked + + - apt: install + packages: + - avahi-daemon + - curl + - udisks2 + - wget + - dhcpcd + - dnsmasq + - python3 + - lighttpd + - unzip + - sudo + - systemd-timesyncd + - ca-certificates + - dosfstools + - iw + - parted + - ssh + - wpasupplicant + - systemd-timesyncd + - linux-image-arm64 + - raspi-firmware + - firmware-brcm80211 + - bluez-firmware + tag: tag-root + unless: rootfs_unpacked + + - cache-rootfs: tag-root + unless: rootfs_unpacked + + - shell: | + echo "kanglam" > "${ROOT?}/etc/hostname" + sed -i "s,root:[^:]*:,root::," "${ROOT?}/etc/shadow" + + + install -m 644 -o root -g root image-specs/rootfs/etc/fstab "${ROOT?}/etc/fstab" + + install -m 644 -o root -g root image-specs/rootfs/etc/network/interfaces.d/eth0 "${ROOT?}/etc/network/interfaces.d/eth0" + install -m 600 -o root -g root image-specs/rootfs/etc/network/interfaces.d/wlan0 "${ROOT?}/etc/network/interfaces.d/wlan0" + + install -m 755 -o root -g root image-specs/rootfs/usr/local/sbin/rpi-set-sysconf "${ROOT?}/usr/local/sbin/rpi-set-sysconf" + install -m 644 -o root -g root image-specs/rootfs/etc/systemd/system/rpi-set-sysconf.service "${ROOT?}/etc/systemd/system/" + install -m 644 -o root -g root image-specs/rootfs/boot/firmware/sysconf.txt "${ROOT?}/boot/firmware/sysconf.txt" + mkdir -p "${ROOT?}/etc/systemd/system/basic.target.requires/" + ln -s /etc/systemd/system/rpi-set-sysconf.service "${ROOT?}/etc/systemd/system/basic.target.requires/rpi-set-sysconf.service" + + # Resize script is now in the initrd for first boot; no need to ship it. + rm -f "${ROOT?}/etc/initramfs-tools/hooks/rpi-resizerootfs" + rm -f "${ROOT?}/etc/initramfs-tools/scripts/local-bottom/rpi-resizerootfs" + + install -m 644 -o root -g root image-specs/rootfs/etc/systemd/system/rpi-reconfigure-raspi-firmware.service "${ROOT?}/etc/systemd/system/" + mkdir -p "${ROOT?}/etc/systemd/system/multi-user.target.requires/" + ln -s /etc/systemd/system/rpi-reconfigure-raspi-firmware.service "${ROOT?}/etc/systemd/system/multi-user.target.requires/rpi-reconfigure-raspi-firmware.service" + + install -m 644 -o root -g root image-specs/rootfs/etc/systemd/system/rpi-generate-ssh-host-keys.service "${ROOT?}/etc/systemd/system/" + ln -s /etc/systemd/system/rpi-generate-ssh-host-keys.service "${ROOT?}/etc/systemd/system/multi-user.target.requires/rpi-generate-ssh-host-keys.service" + rm -f "${ROOT?}"/etc/ssh/ssh_host_*_key* + + root-fs: tag-root + + # Copy the relevant device tree files to the boot partition + - chroot: tag-root + shell: | + install -m 644 -o root -g root /usr/lib/linux-image-*-arm64/broadcom/bcm*rpi*.dtb /boot/firmware/ + + # Clean up archive cache (likely not useful) and lists (likely outdated) to + # reduce image size by several hundred megabytes. + - chroot: tag-root + shell: | + apt-get clean + rm -rf /var/lib/apt/lists + + # Modify the kernel commandline we take from the firmware to boot from + # the partition labeled raspiroot instead of forcing it to mmcblk0p2. + # Also insert the serial console right before the root= parameter. + # + # These changes will be overwritten after the hardware is probed + # after dpkg reconfigures raspi-firmware (upon first boot), so make + # sure we don't lose label-based booting. + - chroot: tag-root + shell: | + sed -i 's/root=/console=ttyS1,115200 root=/' /boot/firmware/cmdline.txt + sed -i 's#root=/dev/mmcblk0p2#root=LABEL=RASPIROOT#' /boot/firmware/cmdline.txt + sed -i 's/^#ROOTPART=.*/ROOTPART=LABEL=RASPIROOT/' /etc/default/raspi*-firmware + + sed -i 's/cma=64M //' /boot/firmware/cmdline.txt + + # TODO(https://github.com/larswirzenius/vmdb2/issues/24): remove once vmdb + # clears /etc/resolv.conf on its own. + - shell: | + rm "${ROOT?}/etc/resolv.conf" + root-fs: tag-root + + # Clear /etc/machine-id and /var/lib/dbus/machine-id, as both should + # be auto-generated upon first boot. From the manpage + # (machine-id(5)): + # + # For normal operating system installations, where a custom image is + # created for a specific machine, /etc/machine-id should be + # populated during installation. + # + # Note this will also trigger ConditionFirstBoot=yes for systemd. + # On Buster, /etc/machine-id should be an emtpy file, not an absent file + # On Bullseye, /etc/machine-id should not exist in an image + - chroot: tag-root + shell: | + rm -f /etc/machine-id /var/lib/dbus/machine-id + echo "uninitialized" > /etc/machine-id + + # Create /etc/raspi-image-id to know, from what commit the image was built + - chroot: tag-root + shell: | + echo "image based on revision: ff7fdbf (Switch from qemu-debootstrap to debootstrap., 2024-01-01) and build on 2025-10-27 20:22 (UTC)" > "/etc/raspi-image-id" + + - virtual-filesystems: tag-root + + - ansible: tag-root + playbook: ../ansible/main.yml + config_file: ../ansible/ansible.cfg + extra_vars: + butter_language: bo + butter_name: kanglam + From 398685d9c2ddd8df42dd0146711a0fbf3f63dbe6 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Thu, 5 Mar 2026 09:28:23 +0000 Subject: [PATCH 04/23] Add some scripts for building and compressing images --- vmdb2-recipes/run_build_amd64.sh | 10 ++++++++++ vmdb2-recipes/run_build_amd64_kanglam.sh | 10 ++++++++++ vmdb2-recipes/run_build_raspi4.sh | 10 ++++++++++ vmdb2-recipes/run_build_raspi4_kanglam.sh | 10 ++++++++++ 4 files changed, 40 insertions(+) create mode 100755 vmdb2-recipes/run_build_amd64.sh create mode 100755 vmdb2-recipes/run_build_amd64_kanglam.sh create mode 100755 vmdb2-recipes/run_build_raspi4.sh create mode 100755 vmdb2-recipes/run_build_raspi4_kanglam.sh diff --git a/vmdb2-recipes/run_build_amd64.sh b/vmdb2-recipes/run_build_amd64.sh new file mode 100755 index 0000000..7b4d103 --- /dev/null +++ b/vmdb2-recipes/run_build_amd64.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +GIT_BRANCH=$(git branch --show-current 2>/dev/null) +GIT_TAG=$(git tag 2>/dev/null | head -n1) +BUILD_DATE=$(date +"%d%m%y") +SUFFIX="${GIT_BRANCH}_${GIT_TAG}_${BUILD_DATE}" +time vmdb2 --rootfs-tarball=64_$SUFFIX.tar.gz --output 64_butter_$SUFFIX.img --log 64_butter_$SUFFIX.log amd64_trixie.yaml +tar cvfz 64_butter_$SUFFIX.img.tar.gz 64_butter_$SUFFIX.img +#curl -H "Authorization: token" $CHURN_SECRET -X PUT --upload-file raspi4_butter_$SUFFIX.img.tar.gz https://guardianproject.dev/api/packages/butter/generic/churn/latest/raspi4_butter_$SUFFIX.img.tar.gz +#rm *img *tar.gz diff --git a/vmdb2-recipes/run_build_amd64_kanglam.sh b/vmdb2-recipes/run_build_amd64_kanglam.sh new file mode 100755 index 0000000..14c468f --- /dev/null +++ b/vmdb2-recipes/run_build_amd64_kanglam.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +GIT_BRANCH=$(git branch --show-current 2>/dev/null) +GIT_TAG=$(git tag 2>/dev/null | head -n1) +BUILD_DATE=$(date +"%d%m%y") +SUFFIX="${GIT_BRANCH}_${GIT_TAG}_${BUILD_DATE}" +time vmdb2 --rootfs-tarball=64_$SUFFIX.tar.gz --output 64_butter_$SUFFIX.img --log 64_butter_$SUFFIX.log amd64_trixie_kanglam.yaml +tar cvfz 64_butter_$SUFFIX.img.tar.gz 64_butter_$SUFFIX.img +#curl -H "Authorization: token" $CHURN_SECRET -X PUT --upload-file raspi4_butter_$SUFFIX.img.tar.gz https://guardianproject.dev/api/packages/butter/generic/churn/latest/raspi4_butter_$SUFFIX.img.tar.gz +#rm *img *tar.gz diff --git a/vmdb2-recipes/run_build_raspi4.sh b/vmdb2-recipes/run_build_raspi4.sh new file mode 100755 index 0000000..e0a9a07 --- /dev/null +++ b/vmdb2-recipes/run_build_raspi4.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +GIT_BRANCH=$(git branch --show-current 2>/dev/null) +GIT_TAG=$(git tag 2>/dev/null | head -n1) +BUILD_DATE=$(date +"%d%m%y") +SUFFIX="${GIT_BRANCH}_${GIT_TAG}_${BUILD_DATE}" +time vmdb2 --rootfs-tarball=raspi4_$SUFFIX.tar.gz --output raspi4_butter_NOAP_$SUFFIX.img --log raspi4_butter_$SUFFIX.log raspi_4_trixie.yaml +tar cvfz raspi4_butter_NOAP_$SUFFIX.img.tar.gz raspi4_butter_NOAP_$SUFFIX.img +#curl -H "Authorization: token" $CHURN_SECRET -X PUT --upload-file raspi4_butter_$SUFFIX.img.tar.gz https://guardianproject.dev/api/packages/butter/generic/churn/latest/raspi4_butter_$SUFFIX.img.tar.gz +#rm *img *tar.gz diff --git a/vmdb2-recipes/run_build_raspi4_kanglam.sh b/vmdb2-recipes/run_build_raspi4_kanglam.sh new file mode 100755 index 0000000..f27ff92 --- /dev/null +++ b/vmdb2-recipes/run_build_raspi4_kanglam.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +GIT_BRANCH=$(git branch --show-current 2>/dev/null) +GIT_TAG=$(git tag 2>/dev/null | head -n1) +BUILD_DATE=$(date +"%d%m%y") +SUFFIX="${GIT_BRANCH}_${GIT_TAG}_${BUILD_DATE}" +time vmdb2 --rootfs-tarball=raspi4_$SUFFIX.tar.gz --output raspi4_butter_$SUFFIX.img --log raspi4_butter_$SUFFIX.log kanglam_pi4.yaml +tar cvfz raspi4_butter_$SUFFIX.img.tar.gz raspi4_butter_$SUFFIX.img +#curl -H "Authorization: token" $CHURN_SECRET -X PUT --upload-file raspi4_butter_$SUFFIX.img.tar.gz https://guardianproject.dev/api/packages/butter/generic/churn/latest/raspi4_butter_$SUFFIX.img.tar.gz +#rm *img *tar.gz From dea506b562eca6afe570b3e37a914e2471d281ce Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Thu, 5 Mar 2026 09:31:14 +0000 Subject: [PATCH 05/23] Add amd64 recipe for kanglam --- vmdb2-recipes/amd64_trixie_kanglam.yaml | 178 ++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 vmdb2-recipes/amd64_trixie_kanglam.yaml diff --git a/vmdb2-recipes/amd64_trixie_kanglam.yaml b/vmdb2-recipes/amd64_trixie_kanglam.yaml new file mode 100644 index 0000000..f13c328 --- /dev/null +++ b/vmdb2-recipes/amd64_trixie_kanglam.yaml @@ -0,0 +1,178 @@ +--- +# See https://wiki.debian.org/RaspberryPi3 for known issues and more details. +# image.yml based on revision: ff7fdbf (Switch from qemu-debootstrap to debootstrap., 2024-01-01) + +steps: + - mkimg: "{{ output }}" + size: 3100M + + - mklabel: gpt + device: "{{ output }}" + +############ efi + - mkpart: primary + fs-type: 'fat32' + device: "{{ output }}" + start: 1MiB + end: 132MiB + tag: efi + + - set_part_flag: "{{ output }}" + tag: efi + flag: boot + state: enabled + + - set_part_flag: "{{ output }}" + tag: efi + flag: esp + state: enabled + +############ bios grub + - mkpart: primary + device: "{{ output }}" + start: 132MiB + end: 133MiB + tag: bios_grub + + - set_part_flag: "{{ output }}" + tag: bios_grub + flag: bios_grub + state: enabled +############ live + - mkpart: primary + device: "{{ output }}" + start: 133MiB + end: 100% + tag: tag-root + + - set_part_flag: "{{ output }}" + tag: tag-root + flag: legacy_boot + state: enabled + + - kpartx: "{{ output }}" + + - mkfs: vfat + partition: efi + label: EFI + options: -F32 + + - mkfs: ext4 + partition: tag-root + label: boot + + - mount: tag-root + + - shell: | + dd bs=440 count=1 conv=notrunc if=/usr/lib/syslinux/mbr/gptmbr.bin of="{{ output }}" + root-fs: tag-root + + - unpack-rootfs: tag-root + + - debootstrap: trixie + require_empty_target: false + mirror: http://deb.debian.org/debian + target: tag-root + components: + - main + - non-free-firmware + - non-free + unless: rootfs_unpacked + + - create-file: /etc/apt/sources.list + contents: |+ + deb http://deb.debian.org/debian trixie main non-free-firmware non-free + deb http://deb.debian.org/debian trixie-updates main non-free-firmware non-free + deb http://security.debian.org/debian-security trixie-security main non-free-firmware non-free + + unless: rootfs_unpacked + + - apt: install + packages: + - avahi-daemon + - curl + - udisks2 + - wget + - dhcpcd + - python3 + - lighttpd + - unzip + - sudo + - systemd-timesyncd + - ca-certificates + - dosfstools + - iw + - parted + - ssh + - wpasupplicant + - systemd + - systemd-sysv + - init-system-helpers + - syslinux + - linux-image-amd64 + tag: tag-root + unless: rootfs_unpacked + + - cache-rootfs: tag-root + unless: rootfs_unpacked + + - shell: | + echo "kanglam" > "${ROOT?}/etc/hostname" + + # Allow root logins locally with no password + sed -i 's,root:[^:]*:,root::,' "${ROOT?}/etc/shadow" + + install -m 644 -o root -g root image-specs/rootfs/etc/fstab "${ROOT?}/etc/fstab" + + install -m 644 -o root -g root image-specs/rootfs/etc/network/interfaces.d/eth0 "${ROOT?}/etc/network/interfaces.d/eth0" + install -m 600 -o root -g root image-specs/rootfs/etc/network/interfaces.d/wlan0 "${ROOT?}/etc/network/interfaces.d/wlan0" + root-fs: tag-root + + # Clean up archive cache (likely not useful) and lists (likely outdated) to + # reduce image size by several hundred megabytes. + - chroot: tag-root + shell: | + apt-get clean + rm -rf /var/lib/apt/lists + + - grub: bios + tag: tag-root + console: serial +# + - grub: uefi + tag: tag-root + efi: efi + console: serial + + - shell: | + rm "${ROOT?}/etc/resolv.conf" + root-fs: tag-root + + # Clear /etc/machine-id and /var/lib/dbus/machine-id, as both should + # be auto-generated upon first boot. From the manpage + # (machine-id(5)): + # + # For normal operating system installations, where a custom image is + # created for a specific machine, /etc/machine-id should be + # populated during installation. + # + # Note this will also trigger ConditionFirstBoot=yes for systemd. + # On Buster, /etc/machine-id should be an emtpy file, not an absent file + # On Bullseye, /etc/machine-id should not exist in an image + - chroot: tag-root + shell: | + rm -f /etc/machine-id /var/lib/dbus/machine-id + echo "uninitialized" > /etc/machine-id + echo "LABEL=BOOT / ext4 rw 0 1" > /etc/fstab + + - virtual-filesystems: tag-root + + - ansible: tag-root + playbook: ../ansible/main.yml + config_file: ../ansible/ansible.cfg + extra_vars: + butter_language: en + butter_name: kanglam + tags: base,usb,matrix,keanu,website + butter_user: "amd" + ap_mode_supported: "false" From c4ffbb00e22211a2e34e7800a6cc660b05b1e0fc Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Thu, 5 Mar 2026 09:33:02 +0000 Subject: [PATCH 06/23] Add delta chat ansible playbook --- ansible/delta-chat.yml | 31 +++++++++++++++++++++++++++++++ ansible/main.yml | 2 ++ 2 files changed, 33 insertions(+) create mode 100644 ansible/delta-chat.yml diff --git a/ansible/delta-chat.yml b/ansible/delta-chat.yml new file mode 100644 index 0000000..5f17f0b --- /dev/null +++ b/ansible/delta-chat.yml @@ -0,0 +1,31 @@ +--- +- name: Install madmail/deltachat + hosts: all + become: true + tasks: + - name: Create madmail directory + file: + path: "/home/{{ butter_user }}/madmail" + state: directory + owner: "{{ butter_user }}" + group: "{{ butter_user }}" + mode: "0755" + + - name: Download pre-built madmail archive + get_url: + url: "https://github.com/themadorg/madmail/releases/download/v0.12.7/madmail-linux-{{ go_arch_map[ansible_architecture] }}.tar.gz" + dest: "/tmp/madmail-linux-{{ go_arch_map[ansible_architecture] }}.tar.gz" + mode: '0644' + + - name: Untar madmail + unarchive: + src: "/tmp/madmail-linux-{{ go_arch_map[ansible_architecture] }}.tar.gz" + dest: "/home/{{ butter_user }}/madmail" + remote_src: yes + #extra_opts: [--strip-components=1] + + - name: Ensure butter_user owns madmail directory + file: + path: "/home/{{ butter_user }}/madmail" + state: directory + recurse: yes diff --git a/ansible/main.yml b/ansible/main.yml index 770412c..7687838 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -4,6 +4,8 @@ - "base" - "ap" - "matrix" +- import_playbook: delta-chat.yml + tags: "delta-chat" - import_playbook: install-rasp-ap.yml tags: "ap" when: ap_mode_supported | bool From 0fa18af48e78b20df2580769c98386a6b92d752b Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Fri, 6 Mar 2026 09:04:11 +0000 Subject: [PATCH 07/23] Initial changes to integrate new portal --- ansible/butter-base.yml | 34 +++---- ansible/cleanup.yml | 20 ++--- ansible/delta-chat.yml | 16 ++-- ansible/deploy-butter-portal.yml | 88 +++++++++++++++++++ ansible/deploy-butter-site.yml | 59 ------------- ansible/install-ap-optimized-firmware.yml | 2 +- ansible/install-chat.yml | 88 +++++++++---------- ansible/install-keanu-weblite.yml | 39 ++++---- ansible/install-rasp-ap.yml | 27 +++--- ansible/install-usb-viewer.yml | 38 ++------ ansible/main.yml | 12 +-- ansible/remove-wifi-creds.yml | 2 +- ansible/templates/99-usb-butter.rules | 2 +- .../templates/butterbox-dendrite.service.j2 | 14 +++ ansible/templates/butterbox-portal.service.j2 | 14 +++ ansible/templates/nginx-config.j2 | 22 +++++ ansible/templates/on-usb-drive-mounted.sh.j2 | 48 ++++++++++ vmdb2-recipes/amd64_trixie.yaml | 7 +- 18 files changed, 311 insertions(+), 221 deletions(-) create mode 100644 ansible/deploy-butter-portal.yml delete mode 100644 ansible/deploy-butter-site.yml create mode 100644 ansible/templates/butterbox-dendrite.service.j2 create mode 100644 ansible/templates/butterbox-portal.service.j2 create mode 100644 ansible/templates/nginx-config.j2 create mode 100644 ansible/templates/on-usb-drive-mounted.sh.j2 diff --git a/ansible/butter-base.yml b/ansible/butter-base.yml index 036e9f6..5a68064 100644 --- a/ansible/butter-base.yml +++ b/ansible/butter-base.yml @@ -13,28 +13,28 @@ name: "{{ butter_user }}" state: present - - name: Get supported interface modes - command: iw list - register: iw_list - ignore_errors: yes - when: not (is_vmdb2 | bool) + - name: Add butter user to sudo group + ansible.builtin.user: + name: "{{ butter_user }}" + groups: sudo + append: true - - name: Search for AP mode support - set_fact: - ap_mode_supported: "{{ 'AP' in iw_list.stdout }}" - when: not (is_vmdb2 | bool) - - - name: Show AP mode support result - debug: - msg: > - Wi-Fi AP mode supported: {{ ap_mode_supported }} - when: not (is_vmdb2 | bool) + - name: Allow passwordless sudo for butter user + ansible.builtin.lineinfile: + path: /etc/sudoers + state: present + regexp: "^{{ butter_user }}" + line: "{{ butter_user }} ALL=(ALL) NOPASSWD:ALL" + validate: '/usr/sbin/visudo -cf %s' - name: Make sure /etc/resolv.conf is populated - lineinfile: + ansible.builtin.lineinfile: path: /etc/resolv.conf regexp: '^nameserver 1.1.1.1' line: 'nameserver 1.1.1.1' state: present insertafter: EOF - create: yes + create: true + owner: root + group: root + mode: '0644' diff --git a/ansible/cleanup.yml b/ansible/cleanup.yml index c1ed62c..3c30381 100644 --- a/ansible/cleanup.yml +++ b/ansible/cleanup.yml @@ -4,11 +4,9 @@ become: true tasks: - name: Print Dendrite process info for debugging - become: yes ansible.builtin.shell: | echo "=== Dendrite PIDs ===" pgrep -u {{ butter_user }} -f dendrite || echo "No dendrite PIDs found" - echo echo "=== Full process tree of Dendrite ===" for pid in $(pgrep -u {{ butter_user }} -f dendrite); do @@ -16,34 +14,28 @@ pstree -p $pid || echo "pstree not available for PID $pid" echo done - - echo "=== Open files under VMDB mount ===" - lsof +D /tmp/tmpyu_8dsew || echo "No open files found" - - echo "=== Current working directories of processes in mount ===" - lsof +D /tmp/tmpyu_8dsew | awk '{print $2, $NF}' | sort | uniq register: dendrite_debug when: is_vmdb2 | bool + changed_when: false - name: Show debug output - debug: + ansible.builtin.debug: msg: "{{ dendrite_debug.stdout_lines }}" when: is_vmdb2 | bool - name: Kill any running Dendrite process - become: yes ansible.builtin.shell: | - pgrep -u {{ butter_user }} -f dendrite | xargs -r kill -9 + set -o pipefail && pgrep -u {{ butter_user }} -f dendrite | xargs -r kill -9 register: dendrite_cleanup changed_when: dendrite_cleanup.stdout != "" when: is_vmdb2 | bool - + - name: Show cleanup output ansible.builtin.debug: msg: "{{ dendrite_cleanup.stdout_lines }}" when: is_vmdb2 | bool - name: Give processes time to exit - become: yes - shell: sleep 5 + ansible.builtin.pause: + seconds: 5 when: is_vmdb2 | bool diff --git a/ansible/delta-chat.yml b/ansible/delta-chat.yml index 5f17f0b..c5561ac 100644 --- a/ansible/delta-chat.yml +++ b/ansible/delta-chat.yml @@ -4,7 +4,7 @@ become: true tasks: - name: Create madmail directory - file: + ansible.builtin.file: path: "/home/{{ butter_user }}/madmail" state: directory owner: "{{ butter_user }}" @@ -12,20 +12,22 @@ mode: "0755" - name: Download pre-built madmail archive - get_url: + ansible.builtin.get_url: url: "https://github.com/themadorg/madmail/releases/download/v0.12.7/madmail-linux-{{ go_arch_map[ansible_architecture] }}.tar.gz" dest: "/tmp/madmail-linux-{{ go_arch_map[ansible_architecture] }}.tar.gz" mode: '0644' - name: Untar madmail - unarchive: + ansible.builtin.unarchive: src: "/tmp/madmail-linux-{{ go_arch_map[ansible_architecture] }}.tar.gz" dest: "/home/{{ butter_user }}/madmail" - remote_src: yes - #extra_opts: [--strip-components=1] + remote_src: true + # extra_opts: [--strip-components=1] - name: Ensure butter_user owns madmail directory - file: + ansible.builtin.file: path: "/home/{{ butter_user }}/madmail" state: directory - recurse: yes + recurse: true + owner: "{{ butter_user }}" + group: "{{ butter_user }}" diff --git a/ansible/deploy-butter-portal.yml b/ansible/deploy-butter-portal.yml new file mode 100644 index 0000000..b913cb8 --- /dev/null +++ b/ansible/deploy-butter-portal.yml @@ -0,0 +1,88 @@ +--- +- name: Deploy butter portal + hosts: all + become: true + + tasks: + - name: "Ensure /tmp/butter-portal is absent" + ansible.builtin.file: + path: "/home/{{ butter_user }}/butter-portal" + state: absent + + - name: "Clone the portal repo" + ansible.builtin.git: + repo: "https://guardianproject.dev/butter/butter-portal" + dest: "/home/{{ butter_user }}/butter-portal" + version: main + + - name: Install requirements + ansible.builtin.pip: + requirements: "/home/{{ butter_user }}/butter-portal/requirements.txt" + virtualenv: "/home/{{ butter_user }}/portal_env" + virtualenv_python: python3 + + - name: Seed database + ansible.builtin.shell: | + echo "Starting db initialisation!" + source /home/{{ butter_user }}/portal_env/bin/activate + flask db init + flask db migrate + flask db upgrade + flask seed-settings + args: + chdir: "/home/{{ butter_user }}/butter-portal" + executable: /bin/bash + creates: "/home/{{ butter_user }}/butter-portal/app.db" + register: database_init + + - name: Template portal systemd service file + ansible.builtin.template: + src: templates/butterbox-portal.service.j2 + dest: /lib/systemd/system/butterbox-portal.service + owner: root + group: root + mode: '0644' + + - name: Template nginx config + ansible.builtin.template: + src: templates/nginx-config.j2 + dest: /etc/nginx/sites-available/default + owner: root + group: root + mode: '0644' + + - name: Enable portal by symlink + ansible.builtin.file: + src: /lib/systemd/system/butterbox-portal.service + dest: /etc/systemd/system/multi-user.target.wants/butterbox-portal.service + state: link + + - name: Ensure butter_user owns portal directory + ansible.builtin.file: + path: "/home/{{ butter_user }}/butter-portal" + state: directory + recurse: true + owner: "{{ butter_user }}" + group: "{{ butter_user }}" +# - name: Template portal reverse proxy config for Lighttpd +# ansible.builtin.get_url: +# src: templates/50-butter-portal-reverse-proxy.conf +# dest: /etc/lighttpd/conf-available/50-butter-portal-reverse-proxy.conf +# owner: root +# group: root +# mode: '0644' +# +# - name: Ensure old symlink is removed if it exists +# ansible.builtin.file: +# path: /etc/lighttpd/conf-enabled/50-butter-portal-reverse-proxy.conf +# state: absent +# force: true +# +# - name: Enable reverse proxy config for portal in Lighttpd +# ansible.builtin.file: +# src: /etc/lighttpd/conf-available/50-butter-portal-reverse-proxy.conf +# dest: /etc/lighttpd/conf-enabled/50-butter-portal-reverse-proxy.conf +# state: link +# force: true +# + # - debug: var=database_init.stdout_lines diff --git a/ansible/deploy-butter-site.yml b/ansible/deploy-butter-site.yml deleted file mode 100644 index c926661..0000000 --- a/ansible/deploy-butter-site.yml +++ /dev/null @@ -1,59 +0,0 @@ ---- -- name: Deploy butter site - hosts: all - become: true - - tasks: - - name: Install unzip - apt: - name: - - unzip - state: present - update_cache: yes - when: not ( is_vmdb2 | bool ) - - - name: Ensure /etc/resolv.conf contains nameserver 1.1.1.1 - copy: - dest: /etc/resolv.conf - content: "nameserver 1.1.1.1\n" - owner: root - group: root - mode: '0644' - when: is_vmdb2 | bool - - - name: Ensure /tmp/butter-site is absent - file: - path: /tmp/butter-site - state: absent - - - name: Ensure /tmp/site.zip is absent - file: - path: /tmp/site.zip - state: absent - - - name: Download the butter-box UI zip file - get_url: - url: "https://likebutter.gitlab.io/butter-box-ui/site-{{ butter_language }}.zip" - dest: /tmp/site.zip - mode: '0644' - - - name: Ensure /tmp/butter-site directory exists - file: - path: /tmp/butter-site - state: directory - mode: '0755' - - - name: Unarchive site.zip to /tmp/butter-site - unarchive: - src: /tmp/site.zip - dest: /tmp/butter-site - remote_src: yes - - - name: Copy contents to /var/www/html/ - copy: - src: /tmp/butter-site/ - dest: /var/www/html/ - owner: www-data - group: www-data - mode: '0755' - remote_src: yes diff --git a/ansible/install-ap-optimized-firmware.yml b/ansible/install-ap-optimized-firmware.yml index a76f6d4..556a309 100644 --- a/ansible/install-ap-optimized-firmware.yml +++ b/ansible/install-ap-optimized-firmware.yml @@ -10,4 +10,4 @@ register: firmware_update changed_when: firmware_update.rc == 0 failed_when: firmware_update.rc != 0 - ignore_errors: yes + ignore_errors: true diff --git a/ansible/install-chat.yml b/ansible/install-chat.yml index 4e887a8..10c2db1 100644 --- a/ansible/install-chat.yml +++ b/ansible/install-chat.yml @@ -4,18 +4,18 @@ become: true tasks: - name: Install deps - apt: + ansible.builtin.apt: name: - git - vim - lighttpd - sudo state: present - update_cache: yes - when: not ( is_vmdb2 | bool ) + update_cache: true + when: not (is_vmdb2 | bool) - name: Create dendrite directories - file: + ansible.builtin.file: path: "/home/{{ butter_user }}/dendrite/bin" state: directory owner: "{{ butter_user }}" @@ -23,36 +23,38 @@ mode: "0755" - name: Download pre-built dendrite archive - get_url: + ansible.builtin.get_url: url: "https://guardianproject.dev/api/packages/butter/generic/dendrite/latest/dendrite-{{ go_arch_map[ansible_architecture] }}.tar.gz" dest: /tmp mode: '0644' - name: Untar dendrite - unarchive: + ansible.builtin.unarchive: src: "/tmp/dendrite-{{ go_arch_map[ansible_architecture] }}.tar.gz" dest: "/home/{{ butter_user }}/dendrite/bin" - remote_src: yes + remote_src: true extra_opts: [--strip-components=2] - name: Ensure butter_user owns Dendrite directory - file: + ansible.builtin.file: path: "/home/{{ butter_user }}/dendrite" state: directory - recurse: yes + recurse: true - name: Generate Matrix signing key - command: ./bin/generate-keys --private-key matrix_key.pem + ansible.builtin.command: ./bin/generate-keys --private-key matrix_key.pem args: + creates: "/home/{{ butter_user }}/dendrite/matrix_key.pem" chdir: "/home/{{ butter_user }}/dendrite" - name: Generate self-signed TLS certificate (optional) - command: ./bin/generate-keys --tls-cert server.crt --tls-key server.key + ansible.builtin.command: ./bin/generate-keys --tls-cert server.crt --tls-key server.key args: chdir: "/home/{{ butter_user }}/dendrite" + creates: "/home/{{ butter_user }}/dendrite/server.key" - name: Download Dendrite config to target - get_url: + ansible.builtin.get_url: url: "{{ config_base_url }}/butterbox-dendrite.conf" dest: "/home/{{ butter_user }}/dendrite/butterbox-dendrite.conf" owner: "{{ butter_user }}" @@ -60,57 +62,45 @@ mode: '0644' - name: Replace REPLACEME with butter_name in config - replace: + ansible.builtin.replace: path: "/home/{{ butter_user }}/dendrite/butterbox-dendrite.conf" regexp: 'REPLACEME' replace: "{{ butter_name }}" - name: Replace /home/pi with /home/butter_user in config - replace: + ansible.builtin.replace: path: "/home/{{ butter_user }}/dendrite/butterbox-dendrite.conf" regexp: '/pi/' replace: "/{{ butter_user }}/" - name: Create log directory for Dendrite - file: + ansible.builtin.file: path: "/var/log/dendrite" state: directory owner: "{{ butter_user }}" group: "{{ butter_user }}" mode: '0755' - recurse: yes + recurse: true - - name: Download dendrite systemd service file - get_url: - url: "{{ config_base_url }}/butterbox-dendrite.service" + - name: template dendrite systemd service file + ansible.builtin.template: + src: templates/butterbox-dendrite.service.j2 dest: /lib/systemd/system/dendrite.service owner: root group: root mode: '0644' - - name: Replace /home/pi with /home/butter_user in service file - replace: - path: /lib/systemd/system/dendrite.service - regexp: '/pi/' - replace: "/{{ butter_user }}/" - - - name: Replace pi with butter_user in service file - replace: - path: /lib/systemd/system/dendrite.service - regexp: 'User=pi' - replace: "User={{ butter_user }}" - - name: Enable dendrite by symlink - file: + ansible.builtin.file: src: /lib/systemd/system/dendrite.service dest: /etc/systemd/system/multi-user.target.wants/dendrite.service state: link - name: Ensure butter_user owns Dendrite directory - file: + ansible.builtin.file: path: "/home/{{ butter_user }}/dendrite" state: directory - recurse: yes + recurse: true owner: "{{ butter_user }}" group: "{{ butter_user }}" mode: "0755" @@ -123,7 +113,7 @@ when: not (is_vmdb2 | bool) - name: Download Matrix reverse proxy config for Lighttpd - get_url: + ansible.builtin.get_url: url: "{{ config_base_url }}/50-matrix-reverse-proxy.conf" dest: /etc/lighttpd/conf-available/50-matrix-reverse-proxy.conf owner: root @@ -131,22 +121,22 @@ mode: '0644' - name: Ensure old symlink is removed if it exists - file: + ansible.builtin.file: path: /etc/lighttpd/conf-enabled/50-matrix-reverse-proxy.conf state: absent force: true - + - name: Enable reverse proxy config for Matrix in Lighttpd - file: + ansible.builtin.file: src: /etc/lighttpd/conf-available/50-matrix-reverse-proxy.conf dest: /etc/lighttpd/conf-enabled/50-matrix-reverse-proxy.conf state: link force: true - name: Start dendrite as user butter_user - become: yes + become: true become_user: "{{ butter_user }}" - shell: | + ansible.builtin.shell: | nohup /home/{{ butter_user }}/dendrite/bin/dendrite \ --config /home/{{ butter_user }}/dendrite/butterbox-dendrite.conf \ -really-enable-open-registration \ @@ -154,28 +144,30 @@ args: chdir: "/home/{{ butter_user }}" when: is_vmdb2 | bool + changed_when: false - name: Wait for Dendrite client API to be available - wait_for: + ansible.builtin.wait_for: host: "127.0.0.1" port: 8008 delay: 3 # wait a few seconds before first check timeout: 60 # give it up to a minute to start state: started when: is_vmdb2 | bool - + - name: Copy public room script - template: + ansible.builtin.template: src: templates/create_public_room.sh.j2 dest: "/home/{{ butter_user }}/create_public_room.sh" mode: '0755' - + - name: Run the create_public_room.sh script - command: "/home/{{ butter_user }}/create_public_room.sh" + ansible.builtin.command: "/home/{{ butter_user }}/create_public_room.sh" register: room_creation - ignore_errors: false - + ignore_errors: false + changed_when: false + - name: Show room creation output - debug: + ansible.builtin.debug: var: room_creation.stdout diff --git a/ansible/install-keanu-weblite.yml b/ansible/install-keanu-weblite.yml index bb3c288..960440a 100644 --- a/ansible/install-keanu-weblite.yml +++ b/ansible/install-keanu-weblite.yml @@ -4,20 +4,21 @@ become: true tasks: - name: Install Node.js 22 (needed for matrix-js-sdk) - shell: | - curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + ansible.builtin.shell: | + set -o pipefail curl -fsSL https://deb.nodesource.com/setup_22.x | bash - apt-get install -y nodejs args: executable: /bin/bash + creates: /bin/npm - name: Ensure previous keanu-weblite temp directory is removed - file: + ansible.builtin.file: path: /tmp/keanu-weblite state: absent delegate_to: localhost - name: Clone keanu-weblite repository (dev branch) - git: + ansible.builtin.git: repo: https://gitlab.com/keanuapp/keanuapp-weblite.git dest: /tmp/keanu-weblite version: dev @@ -25,51 +26,45 @@ delegate_to: localhost - name: Run npm install - shell: npm install + ansible.builtin.command: npm install args: chdir: /tmp/keanu-weblite delegate_to: localhost + changed_when: false - name: Download keanu-weblite config file - get_url: + ansible.builtin.get_url: url: "{{ config_base_url }}/keanu-weblite-config.json" dest: /tmp/keanu-weblite/src/assets/config.json mode: '0644' delegate_to: localhost - name: Replace REPLACEME with butter_name in config.json - replace: + ansible.builtin.replace: path: /tmp/keanu-weblite/src/assets/config.json regexp: 'REPLACEME' replace: "{{ butter_name }}" delegate_to: localhost - name: Run npm build with legacy OpenSSL option - shell: | + ansible.builtin.shell: | export NODE_OPTIONS=--openssl-legacy-provider npm run build args: chdir: /tmp/keanu-weblite delegate_to: localhost + changed_when: false - name: Copy build output to /var/www/html/chat - become: true - copy: - src: /tmp/keanu-weblite/dist/ - dest: /var/www/html/chat/ + ansible.builtin.copy: + src: /tmp/keanu-weblite/dist/ + dest: /var/www/html/chat/ + mode: '0755' - name: Set permissions for /var/www/html/chat - become: true - file: + ansible.builtin.file: path: /var/www/html/chat owner: www-data group: www-data mode: '0755' - recurse: yes - - - name: Restart lighttpd service - ansible.builtin.systemd: - name: lighttpd - state: restarted - when: not (is_vmdb2 | bool) - + recurse: true diff --git a/ansible/install-rasp-ap.yml b/ansible/install-rasp-ap.yml index b023df9..e44221c 100644 --- a/ansible/install-rasp-ap.yml +++ b/ansible/install-rasp-ap.yml @@ -11,19 +11,19 @@ tasks: - name: Check if RaspAP is already installed ansible.builtin.stat: - path: /var/www/html/admin + path: /var/www/html/raspap register: raspap_stat - name: Download RaspAP install script - get_url: + ansible.builtin.get_url: url: https://install.raspap.com dest: /tmp/raspap_install.sh mode: "0755" - when: not raspap_stat.stat.exists + when: not raspap_stat.stat.exists - name: Run RaspAP install script ansible.builtin.shell: | - pwd && ls -alh / && /usr/bin/bash /tmp/raspap_install.sh --yes --path /var/www/html/admin \ + pwd && ls -alh / && /usr/bin/bash /tmp/raspap_install.sh --yes --path /var/www/html/raspap \ --check 0 \ --wireguard {{ raspap_wireguard }} \ --openvpn {{ raspap_openvpn }} \ @@ -35,37 +35,36 @@ failed_when: raspap_install.rc != 0 - name: Remove /var/www/html.* directories if they exist - become: true ansible.builtin.shell: | find /var/www/html.* -maxdepth 0 -type d -exec rm -r {} \; || : changed_when: false - name: Ensure /etc/hostapd directory exists - file: + ansible.builtin.file: path: /etc/hostapd state: directory mode: '0755' - name: Template RaspAP network config to target - template: + ansible.builtin.template: src: "hostapd.conf.j2" dest: "/etc/hostapd/hostapd.conf" mode: '0644' - name: Copy hostapd set_hostapd_iface config script - template: + ansible.builtin.template: src: "set_hostapd_iface.py" dest: "/usr/local/bin/set_hostapd_iface.py" - mode: '0755' + mode: '0744' - name: Copy hostapd set_hostapd_iface service file - template: + ansible.builtin.template: src: "set-hostapd-iface.service.j2" dest: "/lib/systemd/system/set-hostapd-iface.service" - mode: '0755' + mode: '0644' - name: Download hostapd raspapd systemd service file - get_url: + ansible.builtin.get_url: url: "{{ config_base_url }}/raspapd.service" dest: "/lib/systemd/system/raspapd.service" owner: root @@ -73,7 +72,7 @@ mode: '0644' - name: Enable service raspapd, avahi-daemon, and set_hostapd_iface by symlink - file: + ansible.builtin.file: src: "/lib/systemd/system/{{ item }}" dest: "/etc/systemd/system/multi-user.target.wants/{{ item }}" state: link @@ -83,7 +82,7 @@ - "avahi-daemon.service" - name: Copy dnsmasq config - template: + ansible.builtin.template: src: "butterbox-dnsmasq.conf.j2" dest: /etc/dnsmasq.d/butterbox-dnsmasq.conf owner: root diff --git a/ansible/install-usb-viewer.yml b/ansible/install-usb-viewer.yml index 8f84caa..d965dee 100644 --- a/ansible/install-usb-viewer.yml +++ b/ansible/install-usb-viewer.yml @@ -4,51 +4,31 @@ become: true tasks: - name: Copy systemd services - copy: + ansible.builtin.copy: src: "{{ vmdb2_config_base_dir }}/{{ item }}" dest: "/etc/systemd/system/{{ item }}" + mode: '0644' loop: - udisks2-mount@.service - serve-usb@.service - name: Enable services by symlink - file: - src: "/etc/systemd/system/{{ item }}" + ansible.builtin.file: + src: "/etc/systemd/system/{{ item }}" dest: "/etc/systemd/system/multi-user.target.wants/{{ item }}" state: link loop: - udisks2-mount@.service - serve-usb@.service - - name: Copy web UI assets (remote to remote) - copy: - src: "/var/www/html/assets/{{ item.src }}" - dest: "/var/www/html/{{ item.dest }}" - remote_src: true - loop: - - { src: "css/butter-dir-listing.css", dest: "butter-dir-listing.css" } - - { src: "js/butter-dir-listing.js", dest: "butter-dir-listing.js" } - - - name: Install Lighttpd USB config - copy: - src: "{{ vmdb2_config_base_dir }}/50-usb-butter.conf" - dest: "/etc/lighttpd/conf-available/50-usb-butter.conf" - - name: Install udev rule - copy: + ansible.builtin.copy: src: "templates/99-usb-butter.rules" dest: "/etc/udev/rules.d/99-usb-butter.rules" + mode: '0644' - name: Install udev trigger script - copy: - src: "{{ vmdb2_script_base_dir }}/on-usb-drive-mounted.sh" + ansible.builtin.template: + src: templates/on-usb-drive-mounted.sh.j2 dest: /usr/bin/on-usb-drive-mounted.sh - mode: '0755' - - - name: Reload udev rules - command: udevadm control --reload-rules - when: not (is_vmdb2 | bool) - - - name: Reload systemd daemon - command: systemctl daemon-reload - when: not (is_vmdb2 | bool) + mode: '0744' diff --git a/ansible/main.yml b/ansible/main.yml index 7687838..f5bdc7c 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -4,15 +4,15 @@ - "base" - "ap" - "matrix" -- import_playbook: delta-chat.yml - tags: "delta-chat" -- import_playbook: install-rasp-ap.yml - tags: "ap" - when: ap_mode_supported | bool -- import_playbook: deploy-butter-site.yml +#- import_playbook: install-rasp-ap.yml +# tags: "ap" +# when: ap_mode_supported | bool +- import_playbook: deploy-butter-portal.yml tags: - "website" - "usb" +- import_playbook: delta-chat.yml + tags: "delta-chat" - import_playbook: install-chat.yml tags: "matrix" - import_playbook: cleanup.yml diff --git a/ansible/remove-wifi-creds.yml b/ansible/remove-wifi-creds.yml index 20acf08..80880be 100644 --- a/ansible/remove-wifi-creds.yml +++ b/ansible/remove-wifi-creds.yml @@ -4,7 +4,7 @@ become: true tasks: - name: Copy wpa_supplicant config - copy: + ansible.builtin.copy: src: "{{ vmdb2_config_base_dir }}/wpa_supplicant.conf" dest: /etc/wpa_supplicant/wpa_supplicant.conf force: true diff --git a/ansible/templates/99-usb-butter.rules b/ansible/templates/99-usb-butter.rules index 4f378a1..d9334c0 100644 --- a/ansible/templates/99-usb-butter.rules +++ b/ansible/templates/99-usb-butter.rules @@ -1,7 +1,7 @@ # Using udev to mount newly attached usb drives doesn't work. # https://unix.stackexchange.com/a/507150/223286 # So, we depend on udisks to mount the disk. *Then* we want to -# to setup the symlink and lighttpd config with our script. +# to setup the symlink. # We can run the script immediately because it waits for the disk # to be mounted. diff --git a/ansible/templates/butterbox-dendrite.service.j2 b/ansible/templates/butterbox-dendrite.service.j2 new file mode 100644 index 0000000..63a8b5c --- /dev/null +++ b/ansible/templates/butterbox-dendrite.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=Dendrite Service +After=network.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +Restart=always +RestartSec=1 +User={{ butter_user }} +ExecStart=/home/{{ butter_user }}/dendrite/bin/dendrite --config /home/{{ butter_user }}/dendrite/butterbox-dendrite.conf -really-enable-open-registration + +[Install] +WantedBy=multi-user.target diff --git a/ansible/templates/butterbox-portal.service.j2 b/ansible/templates/butterbox-portal.service.j2 new file mode 100644 index 0000000..4bb8cfa --- /dev/null +++ b/ansible/templates/butterbox-portal.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=Portal Service +After=network.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +Restart=always +RestartSec=1 +User={{ butter_user }} +ExecStart=/bin/bash -c 'source /home/{{ butter_user }}/portal_env/bin/activate && cd /home/{{ butter_user }}/butter-portal && flask --app butter-portal.py run' + +[Install] +WantedBy=multi-user.target diff --git a/ansible/templates/nginx-config.j2 b/ansible/templates/nginx-config.j2 new file mode 100644 index 0000000..846a1d9 --- /dev/null +++ b/ansible/templates/nginx-config.j2 @@ -0,0 +1,22 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name {{ butter_name }}.local; + + location ^~ /chat { + alias /var/www/html/chat; + } + + location ^~ /raspap { + alias /var/www/html/raspap; + } + + location ^~ /_matrix { + proxy_pass http://localhost:8008; + } + + location / { + proxy_pass http://localhost:5000; + } + + } diff --git a/ansible/templates/on-usb-drive-mounted.sh.j2 b/ansible/templates/on-usb-drive-mounted.sh.j2 new file mode 100644 index 0000000..ad456d6 --- /dev/null +++ b/ansible/templates/on-usb-drive-mounted.sh.j2 @@ -0,0 +1,48 @@ +#!/bin/bash + +# Run by udev when a USB drive is inserted +# usage: /usr/bin/on-usb-drive-mounted.sh /media/%k + +# If the drive inserted contains a directory named "butter", +# symlink it to /media/usb-butter +device="$1" + +# The device might not be mounted yet, so wait for it. +usb_mount_path="" +for ((i=0; i<10; i++)); do + usb_mount_path=$(findmnt -n -o TARGET --source "$device") + if [ -n "$usb_mount_path" ]; then + break + fi + sleep 1 +done + +# findmnt will briefly return 1, so don't set e until we're done with it. +set -e + +if [ -z "$usb_mount_path" ]; then + echo "Device $device is not mounted" + exit 1 +else + echo "Device $device mounted to: $usb_mount_path" +fi + +butter_dir="$usb_mount_path" +served_dir="/media/usb-butter" + +# make directory butter_dir world readable +sudo chmod -R a+rx "$butter_dir" +sudo chmod -R a+rx "/media/root/" + +if [ -d "$butter_dir" ]; then + # Delete served_dir if it exists + if [ -L "$served_dir" ]; then + sudo rm "$served_dir" + fi + echo "Linking $butter_dir to $served_dir" + ln -sf "$butter_dir" "$served_dir" + sudo chown -R {{ butter_user }}:{{ butter_user }} $served_dir +else + echo "No butter directory $butter_dir found on $device" + exit 1 +fi diff --git a/vmdb2-recipes/amd64_trixie.yaml b/vmdb2-recipes/amd64_trixie.yaml index d5e15ac..b9b6ae7 100644 --- a/vmdb2-recipes/amd64_trixie.yaml +++ b/vmdb2-recipes/amd64_trixie.yaml @@ -95,7 +95,9 @@ steps: - wget - dhcpcd - python3 - - lighttpd + - python3-packaging + - python3-virtualenv + - nginx - unzip - sudo - systemd-timesyncd @@ -110,6 +112,7 @@ steps: - init-system-helpers - syslinux - linux-image-amd64 + - git tag: tag-root unless: rootfs_unpacked @@ -174,6 +177,6 @@ steps: extra_vars: butter_language: en butter_name: butterbox - tags: base,usb,matrix,keanu,website + tags: delta-chat,ap,base,usb,matrix,keanu,website butter_user: "amd" ap_mode_supported: "false" From c22e6ed5579c274bba560ced7c2760e1bce0896d Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Tue, 24 Mar 2026 11:25:10 +0000 Subject: [PATCH 08/23] Make all templates local, ensure ssh keys for amd64 builds are generated on first boot --- ansible/deploy-butter-portal.yml | 14 + ansible/group_vars/all.yml | 2 - ansible/install-chat.yml | 38 +- ansible/install-keanu-weblite.yml | 11 +- ansible/install-rasp-ap.yml | 8 +- ansible/main.yml | 6 +- ansible/templates/butterbox-dendrite.conf.j2 | 380 ++++++++++++++++++ ansible/templates/butterbox-dnsmasq.conf.j2 | 2 +- ansible/templates/change-manager.service.j2 | 15 + .../templates/keanu-weblite-config.json.j2 | 10 + ansible/templates/raspapd.service.j2 | 15 + vmdb2-recipes/amd64_trixie.yaml | 8 +- vmdb2-recipes/raspi_4_trixie.yaml | 5 +- vmdb2-recipes/run_build_raspi4.sh | 4 +- 14 files changed, 459 insertions(+), 59 deletions(-) create mode 100644 ansible/templates/butterbox-dendrite.conf.j2 create mode 100644 ansible/templates/change-manager.service.j2 create mode 100644 ansible/templates/keanu-weblite-config.json.j2 create mode 100644 ansible/templates/raspapd.service.j2 diff --git a/ansible/deploy-butter-portal.yml b/ansible/deploy-butter-portal.yml index b913cb8..bc6e0b5 100644 --- a/ansible/deploy-butter-portal.yml +++ b/ansible/deploy-butter-portal.yml @@ -43,6 +43,14 @@ group: root mode: '0644' + - name: Template portal change manager service file + ansible.builtin.template: + src: templates/change-manager.service.j2 + dest: /lib/systemd/system/change-manager.service + owner: root + group: root + mode: '0644' + - name: Template nginx config ansible.builtin.template: src: templates/nginx-config.j2 @@ -57,6 +65,12 @@ dest: /etc/systemd/system/multi-user.target.wants/butterbox-portal.service state: link + - name: Enable change manager by symlink + ansible.builtin.file: + src: /lib/systemd/system/change-manager.service + dest: /etc/systemd/system/multi-user.target.wants/change-manager.service + state: link + - name: Ensure butter_user owns portal directory ansible.builtin.file: path: "/home/{{ butter_user }}/butter-portal" diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index e9768ea..46de7ba 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -4,8 +4,6 @@ go_version: "1.24.6" go_arch_map: x86_64: "amd64" aarch64: "arm64" -script_base_url: "https://gitlab.com/likebutter/butterbox-rpi/-/raw/main/scripts" -config_base_url: "https://gitlab.com/likebutter/butterbox-rpi/-/raw/main/configs" vmdb2_script_base_dir: "butterbox-rpi/scripts" vmdb2_config_base_dir: "butterbox-rpi/configs" diff --git a/ansible/install-chat.yml b/ansible/install-chat.yml index 10c2db1..2bea966 100644 --- a/ansible/install-chat.yml +++ b/ansible/install-chat.yml @@ -54,25 +54,13 @@ creates: "/home/{{ butter_user }}/dendrite/server.key" - name: Download Dendrite config to target - ansible.builtin.get_url: - url: "{{ config_base_url }}/butterbox-dendrite.conf" + ansible.builtin.template: + src: "templates/butterbox-dendrite.conf.j2" dest: "/home/{{ butter_user }}/dendrite/butterbox-dendrite.conf" owner: "{{ butter_user }}" group: "{{ butter_user }}" mode: '0644' - - name: Replace REPLACEME with butter_name in config - ansible.builtin.replace: - path: "/home/{{ butter_user }}/dendrite/butterbox-dendrite.conf" - regexp: 'REPLACEME' - replace: "{{ butter_name }}" - - - name: Replace /home/pi with /home/butter_user in config - ansible.builtin.replace: - path: "/home/{{ butter_user }}/dendrite/butterbox-dendrite.conf" - regexp: '/pi/' - replace: "/{{ butter_user }}/" - - name: Create log directory for Dendrite ansible.builtin.file: path: "/var/log/dendrite" @@ -112,27 +100,6 @@ name: dendrite when: not (is_vmdb2 | bool) - - name: Download Matrix reverse proxy config for Lighttpd - ansible.builtin.get_url: - url: "{{ config_base_url }}/50-matrix-reverse-proxy.conf" - dest: /etc/lighttpd/conf-available/50-matrix-reverse-proxy.conf - owner: root - group: root - mode: '0644' - - - name: Ensure old symlink is removed if it exists - ansible.builtin.file: - path: /etc/lighttpd/conf-enabled/50-matrix-reverse-proxy.conf - state: absent - force: true - - - name: Enable reverse proxy config for Matrix in Lighttpd - ansible.builtin.file: - src: /etc/lighttpd/conf-available/50-matrix-reverse-proxy.conf - dest: /etc/lighttpd/conf-enabled/50-matrix-reverse-proxy.conf - state: link - force: true - - name: Start dendrite as user butter_user become: true become_user: "{{ butter_user }}" @@ -146,7 +113,6 @@ when: is_vmdb2 | bool changed_when: false - - name: Wait for Dendrite client API to be available ansible.builtin.wait_for: host: "127.0.0.1" diff --git a/ansible/install-keanu-weblite.yml b/ansible/install-keanu-weblite.yml index 960440a..e3664f3 100644 --- a/ansible/install-keanu-weblite.yml +++ b/ansible/install-keanu-weblite.yml @@ -33,19 +33,12 @@ changed_when: false - name: Download keanu-weblite config file - ansible.builtin.get_url: - url: "{{ config_base_url }}/keanu-weblite-config.json" + ansible.builtin.template: + src: "templates/keanu-weblite-config.json.j2" dest: /tmp/keanu-weblite/src/assets/config.json mode: '0644' delegate_to: localhost - - name: Replace REPLACEME with butter_name in config.json - ansible.builtin.replace: - path: /tmp/keanu-weblite/src/assets/config.json - regexp: 'REPLACEME' - replace: "{{ butter_name }}" - delegate_to: localhost - - name: Run npm build with legacy OpenSSL option ansible.builtin.shell: | export NODE_OPTIONS=--openssl-legacy-provider diff --git a/ansible/install-rasp-ap.yml b/ansible/install-rasp-ap.yml index e44221c..fd537bb 100644 --- a/ansible/install-rasp-ap.yml +++ b/ansible/install-rasp-ap.yml @@ -57,15 +57,15 @@ dest: "/usr/local/bin/set_hostapd_iface.py" mode: '0744' - - name: Copy hostapd set_hostapd_iface service file + - name: Template hostapd set_hostapd_iface service file ansible.builtin.template: src: "set-hostapd-iface.service.j2" dest: "/lib/systemd/system/set-hostapd-iface.service" mode: '0644' - - name: Download hostapd raspapd systemd service file - ansible.builtin.get_url: - url: "{{ config_base_url }}/raspapd.service" + - name: Template hostapd raspapd systemd service file + ansible.builtin.template: + src: "templates/raspapd.service.j2" dest: "/lib/systemd/system/raspapd.service" owner: root group: root diff --git a/ansible/main.yml b/ansible/main.yml index f5bdc7c..4469f2a 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -4,9 +4,9 @@ - "base" - "ap" - "matrix" -#- import_playbook: install-rasp-ap.yml -# tags: "ap" -# when: ap_mode_supported | bool +- import_playbook: install-rasp-ap.yml + tags: "ap" + when: ap_mode_supported | bool - import_playbook: deploy-butter-portal.yml tags: - "website" diff --git a/ansible/templates/butterbox-dendrite.conf.j2 b/ansible/templates/butterbox-dendrite.conf.j2 new file mode 100644 index 0000000..c7507a3 --- /dev/null +++ b/ansible/templates/butterbox-dendrite.conf.j2 @@ -0,0 +1,380 @@ +# This is the Dendrite configuration file. +# +# The configuration is split up into sections - each Dendrite component has a +# configuration section, in addition to the "global" section which applies to +# all components. +# +# At a minimum, to get started, you will need to update the settings in the +# "global" section for your deployment, and you will need to check that the +# database "connection_string" line in each component section is correct. +# +# Each component with a "database" section can accept the following formats +# for "connection_string": +# SQLite: file:filename.db +# file:///path/to/filename.db +# PostgreSQL: postgresql://user:pass@hostname/database?params=... +# +# SQLite is embedded into Dendrite and therefore no further prerequisites are +# needed for the database when using SQLite mode. However, performance with +# PostgreSQL is significantly better and recommended for multi-user deployments. +# SQLite is typically around 20-30% slower than PostgreSQL when tested with a +# small number of users and likely will perform worse still with a higher volume +# of users. +# +# The "max_open_conns" and "max_idle_conns" settings configure the maximum +# number of open/idle database connections. The value 0 will use the database +# engine default, and a negative value will use unlimited connections. The +# "conn_max_lifetime" option controls the maximum length of time a database +# connection can be idle in seconds - a negative value is unlimited. + +# The version of the configuration file. +version: 2 + +# Global Matrix configuration. This configuration applies to all components. +global: + # The domain name of this homeserver. + server_name: {{ butter_name }}.local + + # The path to the signing private key file, used to sign requests and events. + # Note that this is NOT the same private key as used for TLS! To generate a + # signing key, use "./bin/generate-keys --private-key matrix_key.pem". + private_key: /home/{{ butter_user }}/dendrite/matrix_key.pem + + # The paths and expiry timestamps (as a UNIX timestamp in millisecond precision) + # to old signing private keys that were formerly in use on this domain. These + # keys will not be used for federation request or event signing, but will be + # provided to any other homeserver that asks when trying to verify old events. + # old_private_keys: + # - private_key: old_matrix_key.pem + # expired_at: 1601024554498 + + # How long a remote server can cache our server signing key before requesting it + # again. Increasing this number will reduce the number of requests made by other + # servers for our key but increases the period that a compromised key will be + # considered valid by other homeservers. + key_validity_period: 168h0m0s + + # The server name to delegate server-server communications to, with optional port + # e.g. localhost:443 + well_known_server_name: "" + + # Lists of domains that the server will trust as identity servers to verify third + # party identifiers such as phone numbers and email addresses. + trusted_third_party_id_servers: + - matrix.org + - vector.im + + # Disables federation. Dendrite will not be able to make any outbound HTTP requests + # to other servers and the federation API will not be exposed. + disable_federation: false + + # Configures the handling of presence events. + presence: + # Whether inbound presence events are allowed, e.g. receiving presence events from other servers + enable_inbound: false + # Whether outbound presence events are allowed, e.g. sending presence events to other servers + enable_outbound: false + + # Server notices allows server admins to send messages to all users. + server_notices: + enabled: false + # The server localpart to be used when sending notices, ensure this is not yet taken + local_part: "_server" + # The displayname to be used when sending notices + display_name: "Server alerts" + # The mxid of the avatar to use + avatar_url: "" + # The roomname to be used when creating messages + room_name: "Server Alerts" + + # Configuration for NATS JetStream + jetstream: + # A list of NATS Server addresses to connect to. If none are specified, an + # internal NATS server will be started automatically when running Dendrite + # in monolith mode. It is required to specify the address of at least one + # NATS Server node if running in polylith mode. + addresses: + # - localhost:4222 + + # Keep all NATS streams in memory, rather than persisting it to the storage + # path below. This option is present primarily for integration testing and + # should not be used on a real world Dendrite deployment. + in_memory: false + + # Persistent directory to store JetStream streams in. This directory + # should be preserved across Dendrite restarts. + storage_path: /home/{{ butter_user }}/dendrite/jetstream + + # The prefix to use for stream names for this homeserver - really only + # useful if running more than one Dendrite on the same NATS deployment. + topic_prefix: Dendrite + + # Configuration for Prometheus metric collection. + metrics: + # Whether or not Prometheus metrics are enabled. + enabled: false + + # HTTP basic authentication to protect access to monitoring. + basic_auth: + username: metrics + password: metrics + + # DNS cache options. The DNS cache may reduce the load on DNS servers + # if there is no local caching resolver available for use. + dns_cache: + # Whether or not the DNS cache is enabled. + enabled: false + + # Maximum number of entries to hold in the DNS cache, and + # for how long those items should be considered valid in seconds. + cache_size: 256 + cache_lifetime: "5m" # 5minutes; see https://pkg.go.dev/time@master#ParseDuration for more + +# Configuration for the Appservice API. +app_service_api: + internal_api: + listen: http://localhost:7777 # Only used in polylith deployments + connect: http://localhost:7777 # Only used in polylith deployments + database: + connection_string: file:///home/{{ butter_user }}/dendrite/appservice.db + max_open_conns: 10 + max_idle_conns: 2 + conn_max_lifetime: -1 + + # Disable the validation of TLS certificates of appservices. This is + # not recommended in production since it may allow appservice traffic + # to be sent to an unverified endpoint. + disable_tls_validation: false + + # Appservice configuration files to load into this homeserver. + config_files: [] + +# Configuration for the Client API. +client_api: + internal_api: + listen: http://localhost:7771 # Only used in polylith deployments + connect: http://localhost:7771 # Only used in polylith deployments + external_api: + listen: http://[::]:8071 + + # Prevents new users from being able to register on this homeserver, except when + # using the registration shared secret below. + registration_disabled: false + + # Prevents new guest accounts from being created. Guest registration is also + # disabled implicitly by setting 'registration_disabled' above. + guests_disabled: true + + # If set, allows registration by anyone who knows the shared secret, regardless of + # whether registration is otherwise disabled. + registration_shared_secret: "" + + # Whether to require reCAPTCHA for registration. + enable_registration_captcha: false + + # Settings for ReCAPTCHA. + recaptcha_public_key: "" + recaptcha_private_key: "" + recaptcha_bypass_secret: "" + recaptcha_siteverify_api: "" + + # TURN server information that this homeserver should send to clients. + turn: + turn_user_lifetime: "" + turn_uris: [] + turn_shared_secret: "" + turn_username: "" + turn_password: "" + + # Settings for rate-limited endpoints. Rate limiting will kick in after the + # threshold number of "slots" have been taken by requests from a specific + # host. Each "slot" will be released after the cooloff time in milliseconds. + rate_limiting: + enabled: true + threshold: 5 + cooloff_ms: 500 + +# Configuration for the Federation API. +federation_api: + internal_api: + listen: http://localhost:7772 # Only used in polylith deployments + connect: http://localhost:7772 # Only used in polylith deployments + external_api: + listen: http://[::]:8072 + database: + connection_string: file:///home/{{ butter_user }}/dendrite/federationapi.db + max_open_conns: 10 + max_idle_conns: 2 + conn_max_lifetime: -1 + + # How many times we will try to resend a failed transaction to a specific server. The + # backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. + send_max_retries: 16 + + # Disable the validation of TLS certificates of remote federated homeservers. Do not + # enable this option in production as it presents a security risk! + disable_tls_validation: false + + # Perspective keyservers to use as a backup when direct key fetches fail. This may + # be required to satisfy key requests for servers that are no longer online when + # joining some rooms. + key_perspectives: + - server_name: matrix.org + keys: + - key_id: ed25519:auto + public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw + - key_id: ed25519:a_RXGa + public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ + + # This option will control whether Dendrite will prefer to look up keys directly + # or whether it should try perspective servers first, using direct fetches as a + # last resort. + prefer_direct_fetch: false + +# Configuration for the Key Server (for end-to-end encryption). +key_server: + internal_api: + listen: http://localhost:7779 # Only used in polylith deployments + connect: http://localhost:7779 # Only used in polylith deployments + database: + connection_string: file:///home/{{ butter_user }}/dendrite/keyserver.db + max_open_conns: 10 + max_idle_conns: 2 + conn_max_lifetime: -1 + +# Configuration for the Media API. +media_api: + internal_api: + listen: http://localhost:7774 # Only used in polylith deployments + connect: http://localhost:7774 # Only used in polylith deployments + external_api: + listen: http://[::]:8074 + database: + connection_string: file:///home/{{ butter_user }}/dendrite/mediaapi.db + max_open_conns: 5 + max_idle_conns: 2 + conn_max_lifetime: -1 + + # Storage path for uploaded media. May be relative or absolute. + base_path: /home/{{ butter_user }}/dendrite/media_store + + # The maximum allowed file size (in bytes) for media uploads to this homeserver + # (0 = unlimited). If using a reverse proxy, ensure it allows requests at + # least this large (e.g. client_max_body_size in nginx.) + # 1GB = 1 048 576 000 (1024*1024*1000) + max_file_size_bytes: 1048576000 + + # Whether to dynamically generate thumbnails if needed. + dynamic_thumbnails: true + + # The maximum number of simultaneous thumbnail generators to run. + max_thumbnail_generators: 10 + + # A list of thumbnail sizes to be generated for media content. + thumbnail_sizes: + - width: 32 + height: 32 + method: crop + - width: 96 + height: 96 + method: crop + - width: 640 + height: 480 + method: scale + +# Configuration for experimental MSC's +mscs: + # A list of enabled MSC's + # Currently valid values are: + # - msc2836 (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836) + # - msc2946 (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946) + mscs: [] + database: + connection_string: file:///home/{{ butter_user }}/dendrite/mscs.db + max_open_conns: 5 + max_idle_conns: 2 + conn_max_lifetime: -1 + +# Configuration for the Room Server. +room_server: + internal_api: + listen: http://localhost:7770 # Only used in polylith deployments + connect: http://localhost:7770 # Only used in polylith deployments + database: + connection_string: file:///home/{{ butter_user }}/dendrite/roomserver.db + max_open_conns: 10 + max_idle_conns: 2 + conn_max_lifetime: -1 + +# Configuration for the Sync API. +sync_api: + internal_api: + listen: http://localhost:7773 # Only used in polylith deployments + connect: http://localhost:7773 # Only used in polylith deployments + external_api: + listen: http://[::]:8073 + database: + connection_string: file:///home/{{ butter_user }}/dendrite/syncapi.db + max_open_conns: 10 + max_idle_conns: 2 + conn_max_lifetime: -1 + + # This option controls which HTTP header to inspect to find the real remote IP + # address of the client. This is likely required if Dendrite is running behind + # a reverse proxy server. + # real_ip_header: X-Real-IP + +# Configuration for the User API. +user_api: + # The cost when hashing passwords on registration/login. Default: 10. Min: 4, Max: 31 + # See https://pkg.go.dev/golang.org/x/crypto/bcrypt for more information. + # Setting this lower makes registration/login consume less CPU resources at the cost of security + # should the database be compromised. Setting this higher makes registration/login consume more + # CPU resources but makes it harder to brute force password hashes. + # This value can be low if performing tests or on embedded Dendrite instances (e.g WASM builds) + # bcrypt_cost: 10 + internal_api: + listen: http://localhost:7781 # Only used in polylith deployments + connect: http://localhost:7781 # Only used in polylith deployments + account_database: + connection_string: file:///home/{{ butter_user }}/dendrite/userapi_accounts.db + max_open_conns: 10 + max_idle_conns: 2 + conn_max_lifetime: -1 + # The length of time that a token issued for a relying party from + # /_matrix/client/r0/user/{userId}/openid/request_token endpoint + # is considered to be valid in milliseconds. + # The default lifetime is 3600000ms (60 minutes). + # openid_token_lifetime_ms: 3600000 + +# Configuration for Opentracing. +# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on +# how this works and how to set it up. +tracing: + enabled: false + jaeger: + serviceName: "" + disabled: false + rpc_metrics: false + tags: [] + sampler: null + reporter: null + headers: null + baggage_restrictions: null + throttler: null + +# Logging configuration +logging: + - type: std + level: info + - type: file + # The logging level, must be one of debug, info, warn, error, fatal, panic. + level: info + params: + path: /var/log/dendrite/ + + +# Not part of the dendrite-sample file, but required by 0.13.7 +relay_api: + database: + connection_string: file:///home/{{ butter_user }}/dendrite/relay_api.db diff --git a/ansible/templates/butterbox-dnsmasq.conf.j2 b/ansible/templates/butterbox-dnsmasq.conf.j2 index 2a11557..b0f64a0 100644 --- a/ansible/templates/butterbox-dnsmasq.conf.j2 +++ b/ansible/templates/butterbox-dnsmasq.conf.j2 @@ -1,4 +1,4 @@ interface=wlan0 dhcp-range=10.3.141.50,10.3.141.255,255.255.255.0,12h dhcp-option=6,10.3.141.1 -address=/{{ butter_name }}.lan/10.3.141.1 +address=/{{ butter_name }}.local/10.3.141.1 diff --git a/ansible/templates/change-manager.service.j2 b/ansible/templates/change-manager.service.j2 new file mode 100644 index 0000000..9fbc53e --- /dev/null +++ b/ansible/templates/change-manager.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Butterbox setting management service +After=network.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +Restart=on-failure +RestartSec=1 +User=root +ExecStart=/bin/bash -c "chmod 600 /etc/ssh/*key && python3 change_manager.py" +WorkingDirectory=/home/{{ butter_user }}/butter-portal +Environment=PYTHONUNBUFFERED=1 +[Install] +WantedBy=multi-user.target diff --git a/ansible/templates/keanu-weblite-config.json.j2 b/ansible/templates/keanu-weblite-config.json.j2 new file mode 100644 index 0000000..27908bd --- /dev/null +++ b/ansible/templates/keanu-weblite-config.json.j2 @@ -0,0 +1,10 @@ +{ + "appName": "Keanu on Butter Box", + "appNames": {}, + "productLink": "{{ butter_name }}.local", + "defaultServer": "http://{{ butter_name }}.local", + "rtl": false, + "analytics": { + "enabled": false + } +} diff --git a/ansible/templates/raspapd.service.j2 b/ansible/templates/raspapd.service.j2 new file mode 100644 index 0000000..773a13e --- /dev/null +++ b/ansible/templates/raspapd.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=RaspAP Service Daemon +DefaultDependencies=no +# This line, which the default raspapd.service would use, results in a circular dependency +# And the symptom we see is that it doesn't boot. Instead, run this after network.target +# After=multi-user.target +After=network.target + +[Service] +Type=oneshot +ExecStart=/bin/bash /etc/raspap/hostapd/servicestart.sh --interface uap0 --seconds 3 +RemainAfterExit=no + +[Install] +WantedBy=multi-user.target diff --git a/vmdb2-recipes/amd64_trixie.yaml b/vmdb2-recipes/amd64_trixie.yaml index b9b6ae7..d3c7be9 100644 --- a/vmdb2-recipes/amd64_trixie.yaml +++ b/vmdb2-recipes/amd64_trixie.yaml @@ -120,6 +120,7 @@ steps: unless: rootfs_unpacked - shell: | + echo "butterbox" > "${ROOT?}/etc/hostname" # Allow root logins locally with no password @@ -149,6 +150,10 @@ steps: - shell: | rm "${ROOT?}/etc/resolv.conf" + mkdir -p "${ROOT?}/etc/systemd/system/multi-user.target.requires/" + install -m 644 -o root -g root image-specs/rootfs/etc/systemd/system/rpi-generate-ssh-host-keys.service "${ROOT?}/etc/systemd/system/" + ln -s "${ROOT?}/etc/systemd/system/rpi-generate-ssh-host-keys.service" "${ROOT?}/etc/systemd/system/multi-user.target.requires/rpi-generate-ssh-host-keys.service" + rm -f "${ROOT?}"/etc/ssh/ssh_host_*_key* root-fs: tag-root # Clear /etc/machine-id and /var/lib/dbus/machine-id, as both should @@ -162,13 +167,14 @@ steps: # Note this will also trigger ConditionFirstBoot=yes for systemd. # On Buster, /etc/machine-id should be an emtpy file, not an absent file # On Bullseye, /etc/machine-id should not exist in an image + # + - chroot: tag-root shell: | rm -f /etc/machine-id /var/lib/dbus/machine-id echo "uninitialized" > /etc/machine-id echo "LABEL=BOOT / ext4 rw 0 1" > /etc/fstab - - virtual-filesystems: tag-root - ansible: tag-root diff --git a/vmdb2-recipes/raspi_4_trixie.yaml b/vmdb2-recipes/raspi_4_trixie.yaml index cd9925b..d622a41 100644 --- a/vmdb2-recipes/raspi_4_trixie.yaml +++ b/vmdb2-recipes/raspi_4_trixie.yaml @@ -81,7 +81,10 @@ steps: - dhcpcd - dnsmasq - python3 - - lighttpd + - python3-packaging + - python3-virtualenv + - nginx + - git - unzip - sudo - systemd-timesyncd diff --git a/vmdb2-recipes/run_build_raspi4.sh b/vmdb2-recipes/run_build_raspi4.sh index e0a9a07..f09f05a 100755 --- a/vmdb2-recipes/run_build_raspi4.sh +++ b/vmdb2-recipes/run_build_raspi4.sh @@ -4,7 +4,7 @@ GIT_BRANCH=$(git branch --show-current 2>/dev/null) GIT_TAG=$(git tag 2>/dev/null | head -n1) BUILD_DATE=$(date +"%d%m%y") SUFFIX="${GIT_BRANCH}_${GIT_TAG}_${BUILD_DATE}" -time vmdb2 --rootfs-tarball=raspi4_$SUFFIX.tar.gz --output raspi4_butter_NOAP_$SUFFIX.img --log raspi4_butter_$SUFFIX.log raspi_4_trixie.yaml -tar cvfz raspi4_butter_NOAP_$SUFFIX.img.tar.gz raspi4_butter_NOAP_$SUFFIX.img +time vmdb2 --rootfs-tarball=raspi4_$SUFFIX.tar.gz --output raspi4_butter_$SUFFIX.img --log raspi4_butter_$SUFFIX.log raspi_4_trixie.yaml +tar cvfz raspi4_butter_NOAP_$SUFFIX.img.tar.gz raspi4_butter_$SUFFIX.img #curl -H "Authorization: token" $CHURN_SECRET -X PUT --upload-file raspi4_butter_$SUFFIX.img.tar.gz https://guardianproject.dev/api/packages/butter/generic/churn/latest/raspi4_butter_$SUFFIX.img.tar.gz #rm *img *tar.gz From bdddd161cd930c1ccfe833618bffb624731032e9 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Tue, 24 Mar 2026 12:44:03 +0000 Subject: [PATCH 09/23] Add fix from Fabby for the v6 upstream issue Closes #8 --- ansible/templates/nginx-config.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/templates/nginx-config.j2 b/ansible/templates/nginx-config.j2 index 846a1d9..5ef7794 100644 --- a/ansible/templates/nginx-config.j2 +++ b/ansible/templates/nginx-config.j2 @@ -12,11 +12,11 @@ server { } location ^~ /_matrix { - proxy_pass http://localhost:8008; + proxy_pass http://127.0.0.1:8008; } location / { - proxy_pass http://localhost:5000; + proxy_pass http://127.0.0.1:5000; } } From 65b712b4ef32cb623604ce096821dcc25202ca47 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Wed, 1 Apr 2026 11:12:54 +0100 Subject: [PATCH 10/23] Bring lighttpd back in raspap, disable raspap api service --- ansible/install-rasp-ap.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ansible/install-rasp-ap.yml b/ansible/install-rasp-ap.yml index fd537bb..e9e390a 100644 --- a/ansible/install-rasp-ap.yml +++ b/ansible/install-rasp-ap.yml @@ -9,6 +9,12 @@ raspap_adblock: 0 tasks: + - name: ensure lighttpd listens on port 8080 + ansible.builtin.lineinfile: + path: /etc/lighttpd/lighttpd.conf + regexp: '^server.port' + line: server.port=8080 + - name: Check if RaspAP is already installed ansible.builtin.stat: path: /var/www/html/raspap @@ -81,6 +87,13 @@ - "set-hostapd-iface.service" - "avahi-daemon.service" + - name: Disable service raspapd restapi service + ansible.builtin.file: + path: "/etc/systemd/system/multi-user.target.wants/{{ item }}" + state: absent + with_items: + - "restapi.service" + - name: Copy dnsmasq config ansible.builtin.template: src: "butterbox-dnsmasq.conf.j2" @@ -88,13 +101,3 @@ owner: root group: root mode: '0644' - - - name: Restart service raspapd, issue daemon-reload to pick up config changes - ansible.builtin.systemd_service: - state: restarted - daemon_reload: true - name: "{{ item }}" - when: not (is_vmdb2 | bool) - with_items: - - raspapd - - set-hostapd-iface From 0246e9a517bc620224807c0294af543d0fb7f564 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Wed, 1 Apr 2026 11:15:43 +0100 Subject: [PATCH 11/23] Bring in missing templates/files locally --- ansible/install-usb-viewer.yml | 4 ++-- ansible/remove-wifi-creds.yml | 4 ++-- ansible/templates/serve-usb@.service | 8 ++++++++ ansible/templates/udisks2-mount@.service | 8 ++++++++ ansible/templates/wpa_supplicant.conf | 3 +++ 5 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 ansible/templates/serve-usb@.service create mode 100644 ansible/templates/udisks2-mount@.service create mode 100644 ansible/templates/wpa_supplicant.conf diff --git a/ansible/install-usb-viewer.yml b/ansible/install-usb-viewer.yml index d965dee..9144fca 100644 --- a/ansible/install-usb-viewer.yml +++ b/ansible/install-usb-viewer.yml @@ -4,8 +4,8 @@ become: true tasks: - name: Copy systemd services - ansible.builtin.copy: - src: "{{ vmdb2_config_base_dir }}/{{ item }}" + ansible.builtin.template: + src: "{{ item }}" dest: "/etc/systemd/system/{{ item }}" mode: '0644' loop: diff --git a/ansible/remove-wifi-creds.yml b/ansible/remove-wifi-creds.yml index 80880be..e3184ab 100644 --- a/ansible/remove-wifi-creds.yml +++ b/ansible/remove-wifi-creds.yml @@ -4,8 +4,8 @@ become: true tasks: - name: Copy wpa_supplicant config - ansible.builtin.copy: - src: "{{ vmdb2_config_base_dir }}/wpa_supplicant.conf" + ansible.builtin.template: + src: "wpa_supplicant.conf" dest: /etc/wpa_supplicant/wpa_supplicant.conf force: true mode: '0644' diff --git a/ansible/templates/serve-usb@.service b/ansible/templates/serve-usb@.service new file mode 100644 index 0000000..4081b72 --- /dev/null +++ b/ansible/templates/serve-usb@.service @@ -0,0 +1,8 @@ +[Unit] +Description=Serve USB content +BindTo=dev-%i.device + +[Service] +ExecStart=/bin/sh -c '/usr/bin/on-usb-drive-mounted.sh /dev/%i' +ExecStop=/bin/sh -c 'rm /media/usb-butter/' +RemainAfterExit=yes \ No newline at end of file diff --git a/ansible/templates/udisks2-mount@.service b/ansible/templates/udisks2-mount@.service new file mode 100644 index 0000000..c87a591 --- /dev/null +++ b/ansible/templates/udisks2-mount@.service @@ -0,0 +1,8 @@ +[Unit] +Description=Mount service +BindTo=dev-%i.device + +[Service] +ExecStart=/bin/sh -c '/usr/bin/udisksctl mount -b /dev/%i' +ExecStop=/bin/sh -c '/usr/bin/udisksctl unmount -b /dev/%i' +RemainAfterExit=yes \ No newline at end of file diff --git a/ansible/templates/wpa_supplicant.conf b/ansible/templates/wpa_supplicant.conf new file mode 100644 index 0000000..5622f2e --- /dev/null +++ b/ansible/templates/wpa_supplicant.conf @@ -0,0 +1,3 @@ +country=US +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +ap_scan=1 From d8db95b5d596c1480fc3e81723eb1dccf63a2b39 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Wed, 1 Apr 2026 11:17:21 +0100 Subject: [PATCH 12/23] More lighttpd cleanup --- ansible/templates/nginx-config.j2 | 2 +- vmdb2-recipes/amd64_trixie.yaml | 1 + vmdb2-recipes/raspi_4_trixie.yaml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ansible/templates/nginx-config.j2 b/ansible/templates/nginx-config.j2 index 5ef7794..262ac5e 100644 --- a/ansible/templates/nginx-config.j2 +++ b/ansible/templates/nginx-config.j2 @@ -8,7 +8,7 @@ server { } location ^~ /raspap { - alias /var/www/html/raspap; + proxy_pass http://127.0.0.1:8080; } location ^~ /_matrix { diff --git a/vmdb2-recipes/amd64_trixie.yaml b/vmdb2-recipes/amd64_trixie.yaml index d3c7be9..65b186e 100644 --- a/vmdb2-recipes/amd64_trixie.yaml +++ b/vmdb2-recipes/amd64_trixie.yaml @@ -98,6 +98,7 @@ steps: - python3-packaging - python3-virtualenv - nginx + - lighttpd - unzip - sudo - systemd-timesyncd diff --git a/vmdb2-recipes/raspi_4_trixie.yaml b/vmdb2-recipes/raspi_4_trixie.yaml index d622a41..7c2b9ab 100644 --- a/vmdb2-recipes/raspi_4_trixie.yaml +++ b/vmdb2-recipes/raspi_4_trixie.yaml @@ -84,6 +84,7 @@ steps: - python3-packaging - python3-virtualenv - nginx + - lighttpd - git - unzip - sudo From c97a5fcfe35790249e0ac6d656c2fe56706501b4 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Wed, 1 Apr 2026 11:18:12 +0100 Subject: [PATCH 13/23] Trying out 802.11ac instead of 802.11n for better wifi speeds --- ansible/templates/hostapd.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/templates/hostapd.conf.j2 b/ansible/templates/hostapd.conf.j2 index 1f8b8e4..9e197c1 100644 --- a/ansible/templates/hostapd.conf.j2 +++ b/ansible/templates/hostapd.conf.j2 @@ -7,7 +7,7 @@ beacon_int=100 ssid={{ butter_name }} channel=1 hw_mode=g -ieee80211n=0 +ieee80211ac=1 interface=wlan0 wpa=none wpa_pairwise=CCMP From 37acdb1272d14f3081c4a23809dc6e4ccd12630a Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Fri, 3 Apr 2026 10:09:22 +0100 Subject: [PATCH 14/23] Add upgrade content pack instructions --- ansible/debian.md | 0 debian.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 ansible/debian.md create mode 100644 debian.md diff --git a/ansible/debian.md b/ansible/debian.md new file mode 100644 index 0000000..e69de29 diff --git a/debian.md b/debian.md new file mode 100644 index 0000000..70c652b --- /dev/null +++ b/debian.md @@ -0,0 +1,57 @@ +# Creating an upgrade content pack for the butterbox + +Butterboxes run [Debian OS](https://www.debian.org/) and can be upgraded using `apt update` and `apt upgrade` commands. +However, since they are often expected to operate in areas without Internet connectivity, +the following instructions detail how to create and use a content pack to allow local offline upgrades. + +## Requirements +- A large (min 250GB ) Hard drive or USB Stick, formatted as FAT32 or ext4 +- Internet connectivity +- A computer running Debian + +## Creating a Debian mirror + +- Plug in and mount the hard drive into a computer running Debian. The rest of the configuration assumes this is mounted at `/mnt`. +- Edit the Debian mirror configuration. For this, depending on what kind of butterbox you have (AMD64-based, like an old laptop, or +ARM-based, like a Raspberry Pi), edit the file "/etc/apt/mirror.list" (you may wish to back it up beforehand) +to contain the following, making sure to uncomment the line corresponding to your box. +```angular2html +############# config ################## +# + set base_path /mnt # This is where the mirror will be created, ensure it corresponds to where the hard drive is mounted +# + set mirror_path $base_path/mirror + set skel_path $base_path/skel + set var_path $base_path/var + set cleanscript $var_path/clean.sh + set postmirror_script $var_path/postmirror.sh + set run_postmirror 0 +set nthreads 20 +set _tilde 0 +# +############# end config ############## + +#deb [arch=arm64] http://ftp.us.debian.org/debian stable main contrib non-free # Uncomment for ARM64, e.g. Raspberry Pi 3 or 4 +#deb [arch=amd64] http://ftp.us.debian.org/debian stable main contrib non-free # Uncomment for AMD64 + +# Note the mirror location, ftp.us.debian.org; you can choose any other online debian mirror to download from, keep a note of its name +``` + +- Run the command `apt-mirror`. This will create a Debian mirror on your hard drive, that can then be used to upgrade your butterbox. +The size of the archive varies, but will utilise somewhere in the region of 150-200GB, depending on architecture. + + +> Careful running this command on any metered connection, as it will attempt download hundreds of GB worth of data! + +> Be patient. This command will take a while to run depending on the speed of your Internet connection. + +## Updating your Butterbox + +- Plug in the hard drive into your butterbox, and mount it. This assumes it is mounted at `/mnt`. +- Log in via the butterbox console, as the root user. +- ensure the date is set correctly by running `date -s "30/12/2026 10:29` +- edit file `/etc/apt/sources.list` to contain the following line: +` deb file:/mnt/mirror/ftp.us.debian.org/debian stable main contrib non-free` + +Note that the name of the directory might vary depending on the mirror you used in the previous step. +- Congrats! Now you can run `apt update` and `apt upgrade` to update the box. \ No newline at end of file From 2c103e8b00fb9af339a4f05b41a38fd68585db65 Mon Sep 17 00:00:00 2001 From: acute Date: Fri, 3 Apr 2026 10:10:09 +0000 Subject: [PATCH 15/23] Update debian.md --- debian.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian.md b/debian.md index 70c652b..7d8bb6f 100644 --- a/debian.md +++ b/debian.md @@ -49,7 +49,7 @@ The size of the archive varies, but will utilise somewhere in the region of 150- - Plug in the hard drive into your butterbox, and mount it. This assumes it is mounted at `/mnt`. - Log in via the butterbox console, as the root user. -- ensure the date is set correctly by running `date -s "30/12/2026 10:29` +- ensure the date is set correctly by running date, for example: `date -s "30/12/2026 10:29"` - edit file `/etc/apt/sources.list` to contain the following line: ` deb file:/mnt/mirror/ftp.us.debian.org/debian stable main contrib non-free` From 6a4822fcfbdd2e3310a369fa43df25944bf30134 Mon Sep 17 00:00:00 2001 From: irl Date: Mon, 6 Apr 2026 11:09:26 +0100 Subject: [PATCH 16/23] docs: add BSD licence and enhance README --- LICENCE | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 29 +++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 LICENCE diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..6a7670e --- /dev/null +++ b/LICENCE @@ -0,0 +1,56 @@ +Copyright © 2017, Michael Stapelberg and contributors +Copyright © 2021, guardianproject +Copyright © 2025-2026, SR2 Communications Limited +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Michael Stapelberg nor the + names of contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY Michael Stapelberg ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL Michael Stapelberg BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Additionally, we include here the original licence terms as required by the terms +of the Butter Box for Raspberry Pi project, which apply only to the code derived +from that project: + +MIT License + +Copyright (c) 2021 guardianproject + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md index 8bfa6a5..7537a00 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,28 @@ -# Churn +# Butter Churn -This is work in progress. +[![License](https://img.shields.io/badge/License-BSD_3--Clause-orange.svg)](https://opensource.org/licenses/BSD-3-Clause) -To build a butter image, run vmdb2 --rootfs-tarball=my_image.tar.gz --output my_image.img --log my_image.log \ No newline at end of file +Tool for building Butter Box disk images. + +## Usage + +To build a butter image, run: + +```sh +vmdb2 --rootfs-tarball=my_image.tar.gz --output my_image.img --log my_image.log +``` + +## Copyright and Licence + +© 2017, Michael Stapelberg and contributors +© 2021 guardianproject +© SR2 Communications Limited + +See [LICENCE](./LICENCE) for details of the BSD-3 clause licence. + +This project is a derivative work of: + +* [Butter Box for Raspberry Pi](https://gitlab.com/likebutter/butterbox-rpi) + licensed under the MIT licence. +* [Raspberry Pi Image Specs](https://salsa.debian.org/raspi-team/image-specs) + licensed under the 3-clause BSD licence. From 5f113c9cda5625d801e415075977d79c6cd3de6f Mon Sep 17 00:00:00 2001 From: irl Date: Mon, 6 Apr 2026 11:10:50 +0100 Subject: [PATCH 17/23] docs: lint README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7537a00..44a6627 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ vmdb2 --rootfs-tarball=my_image.tar.gz --output my_image.img --log my_image.log ## Copyright and Licence -© 2017, Michael Stapelberg and contributors -© 2021 guardianproject +© 2017, Michael Stapelberg and contributors
+© 2021 guardianproject
© SR2 Communications Limited See [LICENCE](./LICENCE) for details of the BSD-3 clause licence. From 70b267ccdc2660255146aa17aa7a8feb155b3b73 Mon Sep 17 00:00:00 2001 From: irl Date: Mon, 6 Apr 2026 11:12:41 +0100 Subject: [PATCH 18/23] docs: link advanced docs in README --- README.md | 8 ++++++-- debian.md => docs/upgrade_pack.md | 0 2 files changed, 6 insertions(+), 2 deletions(-) rename debian.md => docs/upgrade_pack.md (100%) diff --git a/README.md b/README.md index 44a6627..87b7d37 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,15 @@ To build a butter image, run: vmdb2 --rootfs-tarball=my_image.tar.gz --output my_image.img --log my_image.log ``` +## Advanced Documentation + +* [Creating an upgrade content pack for the Butter Box](./docs/upgrade_pack.md) + ## Copyright and Licence © 2017, Michael Stapelberg and contributors
-© 2021 guardianproject
-© SR2 Communications Limited +© 2021, guardianproject
+© 2025-2026, SR2 Communications Limited See [LICENCE](./LICENCE) for details of the BSD-3 clause licence. diff --git a/debian.md b/docs/upgrade_pack.md similarity index 100% rename from debian.md rename to docs/upgrade_pack.md From 40e05da66296ce9718a79c85ca652fcb121aebac Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Mon, 6 Apr 2026 13:24:37 +0100 Subject: [PATCH 19/23] Add multibox tutorial based on Nathans initial exploration --- README.md | 1 + docs/multibox.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 docs/multibox.md diff --git a/README.md b/README.md index 87b7d37..9357115 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ vmdb2 --rootfs-tarball=my_image.tar.gz --output my_image.img --log my_image.log ## Advanced Documentation * [Creating an upgrade content pack for the Butter Box](./docs/upgrade_pack.md) +* [Set up DeltaChat messaging between multiple boxes](./docs/multibox.md) ## Copyright and Licence diff --git a/docs/multibox.md b/docs/multibox.md new file mode 100644 index 0000000..a4a354c --- /dev/null +++ b/docs/multibox.md @@ -0,0 +1,53 @@ +## Multi-box Delta Chat setup + +This document details the requirements and setup needed to allow exchanging messages between Delta Chat instances +running on different boxes connected together over LAN. +This requires a few configuration changes. + +## Requirements + +- A way to access the boxes via console, either by using a keyboard and monitor, or by enabling +the SSH setting through the admin portal and connecting via SSH. +- The boxes must be configured with different hostnames, e.g., butterbox.local and jambox.local. +The hostname can only be changed at the time of the initial admin setup for a box. +- The boxes must be connected together in the same LAN, either by being plugged into the same router, or +by reconfiguring one of them to be a client of the hotspot provided by the other. +- The rest of the instructions assume the router provides the boxes with IP addresses via DHCP. + +## MDNS Setup +- After making sure the boxes are on the same network, check they can resolve each other's local names, +by running an MDNS lookup command (`avahi-resolve -n -4 `) on one of the boxes for the hostname of the others. The following +example shows the address of `jambox.local` being resolved on the console of the box named `butterbox.local`: +``` +root@butterbox:~# avahi-resolve -n -4 jambox.local + +jambox.local 192.168.1.2 +``` + +- If the above does not work, it could be due to a variety of reasons, the most likely being that the +router used to connect the boxes does not support forwarding MDNS queries. +In this case, run the command `ip a` to find the local `eth0` address for each box, and then add +that into the `/etc/hosts` file on the other boxes. This will allow them to find each other by hostname. +For example, if `jambox.local` had an address of `192.168.1.2`, you would need to add the following +line to `/etc/hosts` on `butterbox.local`: + +```192.168.1.2 jambox.local``` + +- In this example, the corresponding config change would also need to be made for any other connected boxes, e.g., jambox.local's `/etc/hosts` file. + +## Nginx setup +- Edit the `/etc/nginx/sites-enabled/default` file to add an entry for `/mxdeliv` that points to +localhost, port 8081, on all boxes participating in the setup: + +```angular2html + + location /mxdeliv { + proxy_pass http://127.0.0.1:8081; + } +``` + +## Testing + +With the changes above, each box should be able to find the other boxes for mail delivery, +with incoming mail routed by `nginx` to the `madmail` service. +To test, create Delta Chat accounts on each box, and send test messages between them. From d1f4f7b9adb7f6073f89d5f1ca49305513c5ad12 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Tue, 7 Apr 2026 21:27:09 +0100 Subject: [PATCH 20/23] Ensure no systemd-timesyncd, update templates to inlcude avahi-utils --- ansible/install-keanu-weblite.yml | 5 +++++ vmdb2-recipes/amd64_trixie.yaml | 1 + vmdb2-recipes/raspi_4_trixie.yaml | 1 + 3 files changed, 7 insertions(+) diff --git a/ansible/install-keanu-weblite.yml b/ansible/install-keanu-weblite.yml index e3664f3..9c1450d 100644 --- a/ansible/install-keanu-weblite.yml +++ b/ansible/install-keanu-weblite.yml @@ -61,3 +61,8 @@ group: www-data mode: '0755' recurse: true + + - name: "Ensure systemd-timesyncd is disabled" + ansible.builtin.file: + path: "/etc/systemd/system/sysinit.target.wants/systemd-timesyncd.service" + state: absent \ No newline at end of file diff --git a/vmdb2-recipes/amd64_trixie.yaml b/vmdb2-recipes/amd64_trixie.yaml index 65b186e..515fb86 100644 --- a/vmdb2-recipes/amd64_trixie.yaml +++ b/vmdb2-recipes/amd64_trixie.yaml @@ -90,6 +90,7 @@ steps: - apt: install packages: - avahi-daemon + - avahi-utils - curl - udisks2 - wget diff --git a/vmdb2-recipes/raspi_4_trixie.yaml b/vmdb2-recipes/raspi_4_trixie.yaml index 7c2b9ab..c8a04b9 100644 --- a/vmdb2-recipes/raspi_4_trixie.yaml +++ b/vmdb2-recipes/raspi_4_trixie.yaml @@ -75,6 +75,7 @@ steps: - apt: install packages: - avahi-daemon + - avahi-utils - curl - udisks2 - wget From 10d75e9d8c9de27e3cc42903ec361d4dbba81306 Mon Sep 17 00:00:00 2001 From: Ana Custura Date: Wed, 8 Apr 2026 10:30:22 +0100 Subject: [PATCH 21/23] Sync some fixes from production --- ansible/templates/nginx-config.j2 | 1 + vmdb2-recipes/amd64_trixie.yaml | 1 + vmdb2-recipes/raspi_4_trixie.yaml | 2 ++ vmdb2-recipes/run_build_amd64.sh | 2 +- vmdb2-recipes/run_build_raspi4.sh | 4 ++-- vmdb2-recipes/run_build_raspi4_kanglam.sh | 2 +- 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ansible/templates/nginx-config.j2 b/ansible/templates/nginx-config.j2 index 262ac5e..0f10094 100644 --- a/ansible/templates/nginx-config.j2 +++ b/ansible/templates/nginx-config.j2 @@ -2,6 +2,7 @@ server { listen 80 default_server; listen [::]:80 default_server; server_name {{ butter_name }}.local; + client_max_body_size 150M; location ^~ /chat { alias /var/www/html/chat; diff --git a/vmdb2-recipes/amd64_trixie.yaml b/vmdb2-recipes/amd64_trixie.yaml index 515fb86..23669fe 100644 --- a/vmdb2-recipes/amd64_trixie.yaml +++ b/vmdb2-recipes/amd64_trixie.yaml @@ -91,6 +91,7 @@ steps: packages: - avahi-daemon - avahi-utils + - locales - curl - udisks2 - wget diff --git a/vmdb2-recipes/raspi_4_trixie.yaml b/vmdb2-recipes/raspi_4_trixie.yaml index c8a04b9..ae8de0a 100644 --- a/vmdb2-recipes/raspi_4_trixie.yaml +++ b/vmdb2-recipes/raspi_4_trixie.yaml @@ -76,6 +76,8 @@ steps: packages: - avahi-daemon - avahi-utils + - locales + - fake-hwclock - curl - udisks2 - wget diff --git a/vmdb2-recipes/run_build_amd64.sh b/vmdb2-recipes/run_build_amd64.sh index 7b4d103..262d77f 100755 --- a/vmdb2-recipes/run_build_amd64.sh +++ b/vmdb2-recipes/run_build_amd64.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash GIT_BRANCH=$(git branch --show-current 2>/dev/null) -GIT_TAG=$(git tag 2>/dev/null | head -n1) +GIT_TAG=$(git tag 2>/dev/null | tail -n1) BUILD_DATE=$(date +"%d%m%y") SUFFIX="${GIT_BRANCH}_${GIT_TAG}_${BUILD_DATE}" time vmdb2 --rootfs-tarball=64_$SUFFIX.tar.gz --output 64_butter_$SUFFIX.img --log 64_butter_$SUFFIX.log amd64_trixie.yaml diff --git a/vmdb2-recipes/run_build_raspi4.sh b/vmdb2-recipes/run_build_raspi4.sh index f09f05a..83434b9 100755 --- a/vmdb2-recipes/run_build_raspi4.sh +++ b/vmdb2-recipes/run_build_raspi4.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash GIT_BRANCH=$(git branch --show-current 2>/dev/null) -GIT_TAG=$(git tag 2>/dev/null | head -n1) +GIT_TAG=$(git tag 2>/dev/null | tail -n1) BUILD_DATE=$(date +"%d%m%y") SUFFIX="${GIT_BRANCH}_${GIT_TAG}_${BUILD_DATE}" time vmdb2 --rootfs-tarball=raspi4_$SUFFIX.tar.gz --output raspi4_butter_$SUFFIX.img --log raspi4_butter_$SUFFIX.log raspi_4_trixie.yaml -tar cvfz raspi4_butter_NOAP_$SUFFIX.img.tar.gz raspi4_butter_$SUFFIX.img +tar cvfz raspi4_butter_$SUFFIX.img.tar.gz raspi4_butter_$SUFFIX.img #curl -H "Authorization: token" $CHURN_SECRET -X PUT --upload-file raspi4_butter_$SUFFIX.img.tar.gz https://guardianproject.dev/api/packages/butter/generic/churn/latest/raspi4_butter_$SUFFIX.img.tar.gz #rm *img *tar.gz diff --git a/vmdb2-recipes/run_build_raspi4_kanglam.sh b/vmdb2-recipes/run_build_raspi4_kanglam.sh index f27ff92..3940291 100755 --- a/vmdb2-recipes/run_build_raspi4_kanglam.sh +++ b/vmdb2-recipes/run_build_raspi4_kanglam.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash GIT_BRANCH=$(git branch --show-current 2>/dev/null) -GIT_TAG=$(git tag 2>/dev/null | head -n1) +GIT_TAG=$(git tag 2>/dev/null | tail -n1) BUILD_DATE=$(date +"%d%m%y") SUFFIX="${GIT_BRANCH}_${GIT_TAG}_${BUILD_DATE}" time vmdb2 --rootfs-tarball=raspi4_$SUFFIX.tar.gz --output raspi4_butter_$SUFFIX.img --log raspi4_butter_$SUFFIX.log kanglam_pi4.yaml From cb5970ea4331babc06f60e02502a75970fa6305d Mon Sep 17 00:00:00 2001 From: n8fr8 Date: Mon, 13 Apr 2026 22:35:32 -0400 Subject: [PATCH 22/23] add new "advanced wireless documentation" based on the post: https://www.sr2.uk/posts/2026-butter-box-connectivity --- README.md | 1 + docs/wireless_connectivity.md | 127 ++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 docs/wireless_connectivity.md diff --git a/README.md b/README.md index 9357115..5f9374d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ vmdb2 --rootfs-tarball=my_image.tar.gz --output my_image.img --log my_image.log * [Creating an upgrade content pack for the Butter Box](./docs/upgrade_pack.md) * [Set up DeltaChat messaging between multiple boxes](./docs/multibox.md) +* [Extending Butter Box connectivity with WiFi HaLow and LoRa](./docs/wireless_connectivity.md) ## Copyright and Licence diff --git a/docs/wireless_connectivity.md b/docs/wireless_connectivity.md new file mode 100644 index 0000000..9804941 --- /dev/null +++ b/docs/wireless_connectivity.md @@ -0,0 +1,127 @@ +# Extending ButterBox connectivity with WiFi HaLow and LoRa + +ButterBoxes are designed to operate in areas with limited or no Internet connectivity. In some +deployments, it is useful to extend the range over which boxes can communicate with each other, +or over which client devices can reach a box, beyond what conventional WiFi allows. This document +describes two long-range wireless options that have been evaluated for use with the ButterBox, + **WiFi HaLow** (IEEE 802.11ah) and **LoRa**, and explains how to set each one up as a bridge in front of a box. + +Both approaches let you keep the ButterBox itself unchanged: the long-range radio acts as a +transparent link between the box and the remote client (or another box), so the existing portal, +Delta Chat relay, and update mechanisms continue to work as normal. + +## Choosing between HaLow and LoRa + +| | WiFi HaLow | LoRa | +| --- | --- | --- | +| Typical range | ~400 m (modeled), usable up to ~500 m | Greater than HaLow | +| Throughput | Up to ~8.9 Mbps (Region 1/3), ~43 Mbps (Region 2); often much lower in practice | ~2680 bits per second | +| Cost per node | ~£155 (Morse Micro evaluation kit) | ~£26 (USR-LG206-P RS232 bridge) | +| Good for | Portal access, multi-box networks, software updates | Very low bandwidth messaging, store-and-forward | + +In short: pick **HaLow** if you need clients to reach the ButterBox portal or to push updates +between boxes at reasonable speeds. Pick **LoRa** if you only need to move very small amounts +of data over a longer distance, and you are prepared to use protocols designed for slow, +high-latency links. + +> WiFi HaLow operates in sub-1GHz ISM bands. The exact frequency allocation depends on your +> region: 863–870 MHz in ITU Regions 1 and 3, and 902–928 MHz in Region 2. Make sure +> the hardware you buy is approved for use where you intend to deploy it. + +## Option 1: WiFi HaLow bridge + +This setup uses a pair of Morse Micro HaLow evaluation kits to create a long-range bridge in +front of the ButterBox. Conventional WiFi clients connect to the mobile end of the bridge and +reach the box transparently over HaLow. + +### Requirements + +- 2 × Morse Micro **MM8108-EKH19** evaluation kits (available from Mouser Electronics, + approximately £155 each). +- A ButterBox running the standard image, with its portal and Delta Chat relay configured. +- A power source for the mobile end of the bridge - a USB powerbank is sufficient. +- Antennas appropriate for the band allowed in your region. + +### Setup + +1. Place the first HaLow kit alongside the ButterBox and connect it to the box's LAN port. + This unit acts as the **stationary bridge** at the box's location. +2. Configure the second HaLow kit as the **mobile bridge**. Power it from a powerbank so it + can be carried or positioned away from the box. +3. Both kits must be configured to use the same HaLow channel within the band approved for + your region (863–870 MHz or 902–928 MHz). Refer to the Morse Micro evaluation + kit documentation for the exact channel selection commands. +4. The mobile bridge exposes a conventional 2.4 GHz WiFi network. Client devices (phones, + laptops) join this network in the normal way; their traffic is then relayed over HaLow to + the stationary bridge and on to the ButterBox. +5. Because the bridge is transparent at the IP layer, no changes are needed on the ButterBox + itself. Clients reach the portal at the usual `butterbox.local` address. + +### Notes on performance + +- Coverage modelling suggests reliable operation out to roughly 400 metres, and the ButterBox + portal remained reachable in tests at around 500 metres. +- Link speed is volatile: even within 50 metres, throughput can drop to around 0.3 Mbps before + recovering. Expect the portal to feel slower than over conventional WiFi. +- HaLow operates in an ISM band and must implement "listen before send". Heavily congested + environments will reduce effective throughput further. +- For multi-box deployments (for example, several ButterBoxes across a school, refugee camp, + or evacuation centre) HaLow has enough bandwidth to also push updates between boxes. + See [upgrade_pack.md](upgrade_pack.md) for the offline upgrade workflow this complements. + +## Option 2: LoRa serial bridge + +LoRa offers significantly greater range than HaLow but at a fraction of the throughput. It is +not suitable for portal access, but can be used for very low bandwidth messaging or +store-and-forward style workflows between boxes. + +### Requirements + +- 2 × **USR-LG206-P** RS232-to-LoRa bridges (available from AliExpress, approximately + £26 each). +- A USB-to-RS232 adapter for each ButterBox the bridge is attached to, unless the box already + has a serial port. +- Antennas appropriate for the band approved in your region (typically 868 MHz or 915 MHz; the + 433 MHz band is also available in some regions but overlaps with amateur radio allocations). + +### Setup + +1. Connect a USR-LG206-P to each ButterBox you want to link, using the USB-to-RS232 adapter. +2. Configure both LoRa bridges with matching frequency, spreading factor, and network ID + settings. The factory defaults will work for a point-to-point link, but the frequency must + be set to a band approved in your region. +3. The bridge presents itself as a serial device on the ButterBox (typically `/dev/ttyUSB0`). + Any application that can speak over a serial link can now exchange data between the two + boxes. +4. For practical use, layer a protocol designed for slow, high-latency links on top of the + serial connection. A reasonable starting point is **UUCP**, which is packaged in the Debian + repositories the ButterBox already uses and is well suited to store-and-forward delivery + over unreliable links. + +### Notes on performance + +- The advertised throughput of the USR-LG206-P is **2680 bits per second** ... bits, not + kilobits. Plan accordingly: this is enough for small text messages and metadata, but not for + interactive use of the portal. +- LoRa's physical layer is a proprietary Semtech standard. There is currently no fully open + source implementation of the radio itself, although the application protocols you run over + it can be open. +- Because the link is so slow, generic IP-over-serial approaches will be unsatisfactory. + Protocols specifically designed for constrained links (for example tinySSB, which uses + aggressive compression and small packet sizes) are a better fit if UUCP does not meet your + needs. + +## Testing + +Once a bridge is in place, the simplest end-to-end test is the same one used in the +[multi-box Delta Chat setup](multibox.md): create a Delta Chat account on each ButterBox and +exchange a test message. If the message is delivered, the link is working at the application +layer. + +For HaLow setups, you can additionally browse to the ButterBox admin portal from a client +device connected to the mobile bridge, and confirm that pages load (slowly is fine). + +For LoRa setups, do not expect the portal to be usable. Instead, verify the serial link with +a tool such as `minicom` or `screen` on each box, then test your chosen application protocol +(for example UUCP) by queueing a small file for delivery in one direction and confirming it +arrives at the other end. From 68783945f6c1959f9749d34c6491499e243d2b20 Mon Sep 17 00:00:00 2001 From: n8fr8 Date: Mon, 13 Apr 2026 22:43:45 -0400 Subject: [PATCH 23/23] add in link to original blog post --- docs/wireless_connectivity.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/wireless_connectivity.md b/docs/wireless_connectivity.md index 9804941..1e00297 100644 --- a/docs/wireless_connectivity.md +++ b/docs/wireless_connectivity.md @@ -1,5 +1,7 @@ # Extending ButterBox connectivity with WiFi HaLow and LoRa +*This documentation is entirely based upon the original blog post ["Butter Box Connectivity"](https://www.sr2.uk/posts/2026-butter-box-connectivity).* + ButterBoxes are designed to operate in areas with limited or no Internet connectivity. In some deployments, it is useful to extend the range over which boxes can communicate with each other, or over which client devices can reach a box, beyond what conventional WiFi allows. This document