diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..82c2d60 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "vmdb2-recipes/ansible/scripts/butterbox-rpi"] + path = vmdb2-recipes/ansible/scripts/butterbox-rpi + url = https://gitlab.com/likebutter/butterbox-rpi/ +[submodule "ansible/butterbox-rpi"] + path = ansible/butterbox-rpi + url = https://gitlab.com/likebutter/butterbox-rpi +[submodule "vmdb2-recipes/image-specs"] + path = vmdb2-recipes/image-specs + url = https://salsa.debian.org/raspi-team/image-specs.git diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..c5f098a --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,2 @@ +[defaults] +verbosity=1 diff --git a/ansible/butter-base.yml b/ansible/butter-base.yml new file mode 100644 index 0000000..036e9f6 --- /dev/null +++ b/ansible/butter-base.yml @@ -0,0 +1,40 @@ +--- +- name: Butter Base + hosts: all + become: true + tasks: + - name: Ensure butter_user user exists + ansible.builtin.user: + name: "{{ butter_user }}" + state: present + + - name: Ensure butter_user group exists + ansible.builtin.group: + 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: 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: Make sure /etc/resolv.conf is populated + lineinfile: + path: /etc/resolv.conf + regexp: '^nameserver 1.1.1.1' + line: 'nameserver 1.1.1.1' + state: present + insertafter: EOF + create: yes diff --git a/ansible/butterbox-rpi b/ansible/butterbox-rpi new file mode 160000 index 0000000..04cfede --- /dev/null +++ b/ansible/butterbox-rpi @@ -0,0 +1 @@ +Subproject commit 04cfede53c1b2d4887ce06e988b14c178aeb7a08 diff --git a/ansible/cleanup.yml b/ansible/cleanup.yml new file mode 100644 index 0000000..c1ed62c --- /dev/null +++ b/ansible/cleanup.yml @@ -0,0 +1,49 @@ +--- +- name: Cleanup + hosts: all + 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 + echo "Process tree for PID $pid:" + 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 + + - name: Show debug output + 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 + 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 + when: is_vmdb2 | bool diff --git a/ansible/deploy-butter-site.yml b/ansible/deploy-butter-site.yml new file mode 100644 index 0000000..c926661 --- /dev/null +++ b/ansible/deploy-butter-site.yml @@ -0,0 +1,59 @@ +--- +- 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/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000..641772e --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,14 @@ +butter_language: en +butter_name: butter-box +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" +is_vmdb2: "true" +butter_user: "pi" +ap_mode_supported: "true" diff --git a/ansible/install-ap-optimized-firmware.yml b/ansible/install-ap-optimized-firmware.yml new file mode 100644 index 0000000..a76f6d4 --- /dev/null +++ b/ansible/install-ap-optimized-firmware.yml @@ -0,0 +1,13 @@ +--- +- name: Install chat + hosts: all + become: true + tasks: + - name: Set minimal firmware for cyfmac43455-sdio.bin + # This loads a firmware that takes up less of the SRAM used by the WiFi chip, which allows more simultaneous connections (~19 instead of 4–8). + ansible.builtin.command: + cmd: update-alternatives --set cyfmac43455-sdio.bin /lib/firmware/cypress/cyfmac43455-sdio-minimal.bin + register: firmware_update + changed_when: firmware_update.rc == 0 + failed_when: firmware_update.rc != 0 + ignore_errors: yes diff --git a/ansible/install-chat.yml b/ansible/install-chat.yml new file mode 100644 index 0000000..0440297 --- /dev/null +++ b/ansible/install-chat.yml @@ -0,0 +1,213 @@ +--- +- name: Install chat + hosts: all + become: true + tasks: + - name: Install deps + apt: + name: + - git + - vim + - lighttpd + - sudo + state: present + update_cache: yes + when: not ( is_vmdb2 | bool ) + + # install Go +# # - name: Download Go tarball +# get_url: +# url: "https://go.dev/dl/go{{ go_version }}.linux-{{ go_arch_map[ansible_architecture] }}.tar.gz" +# dest: /tmp/go.tar.gz +# mode: '0644' +# +# - name: Extract Go to /usr/local +# unarchive: +# src: /tmp/go.tar.gz +# dest: /usr/local +# remote_src: yes +# creates: /usr/local/go +# +# - name: Ensure Go path is in .profile +# lineinfile: +# path: "/home/{{ butter_user }}/.profile" +# line: 'PATH=$PATH:/usr/local/go/bin' +# insertafter: EOF +# state: present +# create: yes +# +# - name: Remove existing dendrite directory if it exists +# file: +# path: "/home/{{ butter_user }}/dendrite" +# state: absent +# +# - name: Clone dendrite repo +# git: +# repo: https://github.com/matrix-org/dendrite +# dest: "/home/{{ butter_user }}/dendrite" +# version: v0.13.7 +# force: yes +# update: no +# depth: 1 +# +# - name: Build dendrite +# command: /usr/local/go/bin/go build -o bin/ ./cmd/... +# args: +# chdir: "/home/{{ butter_user }}/dendrite" +# +# + + - name: copy Dendrite dir to target + copy: + src: "dendrite/" + dest: "/home/{{ butter_user }}/dendrite" + owner: "{{ butter_user }}" + group: "{{ butter_user }}" + + - name: Ensure butter_user owns Dendrite directory + file: + path: "/home/{{ butter_user }}/dendrite" + state: directory + recurse: yes + owner: "{{ butter_user }}" + group: "{{ butter_user }}" + mode: "0755" + + - name: Generate Matrix signing key + command: ./bin/generate-keys --private-key matrix_key.pem + args: + chdir: "/home/{{ butter_user }}/dendrite" + + - name: Generate self-signed TLS certificate (optional) + command: ./bin/generate-keys --tls-cert server.crt --tls-key server.key + args: + chdir: "/home/{{ butter_user }}/dendrite" + + - name: Download Dendrite config to target + get_url: + url: "{{ config_base_url }}/butterbox-dendrite.conf" + dest: "/home/{{ butter_user }}/dendrite/butterbox-dendrite.conf" + owner: "{{ butter_user }}" + group: "{{ butter_user }}" + mode: '0644' + + - name: Replace REPLACEME with butter_name in config + 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: + path: "/home/{{ butter_user }}/dendrite/butterbox-dendrite.conf" + regexp: '/pi/' + replace: "/{{ butter_user }}/" + + - name: Create log directory for Dendrite + file: + path: "/var/log/dendrite" + state: directory + owner: "{{ butter_user }}" + group: "{{ butter_user }}" + mode: '0755' + recurse: yes + + - name: Download dendrite systemd service file + get_url: + url: "{{ config_base_url }}/butterbox-dendrite.service" + 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: + 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: + path: "/home/{{ butter_user }}/dendrite" + state: directory + recurse: yes + owner: "{{ butter_user }}" + group: "{{ butter_user }}" + mode: "0755" + + - name: Restart service dendrite, issue daemon-reload to pick up config changes + ansible.builtin.systemd_service: + state: restarted + daemon_reload: true + name: dendrite + when: not (is_vmdb2 | bool) + + - name: Download Matrix reverse proxy config for Lighttpd + 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 + 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: + 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_user: "{{ butter_user }}" + shell: | + nohup /home/{{ butter_user }}/dendrite/bin/dendrite \ + --config /home/{{ butter_user }}/dendrite/butterbox-dendrite.conf \ + -really-enable-open-registration \ + > /var/log/dendrite/dendrite-provision.log 2>&1 & + args: + chdir: "/home/{{ butter_user }}" + when: is_vmdb2 | bool + + + - name: Wait for Dendrite client API to be available + 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: + src: templates/create_public_room.sh.j2 + dest: /home/pi/create_public_room.sh + mode: '0755' + + - name: Run the create_public_room.sh script + command: /home/pi/create_public_room.sh + register: room_creation + ignore_errors: false + + - name: Show room creation output + debug: + var: room_creation.stdout diff --git a/ansible/install-keanu-weblite.yml b/ansible/install-keanu-weblite.yml new file mode 100644 index 0000000..bb3c288 --- /dev/null +++ b/ansible/install-keanu-weblite.yml @@ -0,0 +1,75 @@ +--- +- name: Install keanu weblite + hosts: all + become: true + tasks: + - name: Install Node.js 22 (needed for matrix-js-sdk) + shell: | + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + args: + executable: /bin/bash + + - name: Ensure previous keanu-weblite temp directory is removed + file: + path: /tmp/keanu-weblite + state: absent + delegate_to: localhost + + - name: Clone keanu-weblite repository (dev branch) + git: + repo: https://gitlab.com/keanuapp/keanuapp-weblite.git + dest: /tmp/keanu-weblite + version: dev + depth: 1 + delegate_to: localhost + + - name: Run npm install + shell: npm install + args: + chdir: /tmp/keanu-weblite + delegate_to: localhost + + - name: Download keanu-weblite config file + 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: + 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: | + export NODE_OPTIONS=--openssl-legacy-provider + npm run build + args: + chdir: /tmp/keanu-weblite + delegate_to: localhost + + - name: Copy build output to /var/www/html/chat + become: true + copy: + src: /tmp/keanu-weblite/dist/ + dest: /var/www/html/chat/ + + - name: Set permissions for /var/www/html/chat + become: true + 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) + diff --git a/ansible/install-rasp-ap.yml b/ansible/install-rasp-ap.yml new file mode 100644 index 0000000..b023df9 --- /dev/null +++ b/ansible/install-rasp-ap.yml @@ -0,0 +1,101 @@ +--- +- name: Install rasp-ap + hosts: all + become: true + vars: + raspap_branch: "3.4.3" + raspap_wireguard: 0 + raspap_openvpn: 0 + raspap_adblock: 0 + + tasks: + - name: Check if RaspAP is already installed + ansible.builtin.stat: + path: /var/www/html/admin + register: raspap_stat + + - name: Download RaspAP install script + get_url: + url: https://install.raspap.com + dest: /tmp/raspap_install.sh + mode: "0755" + 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 \ + --check 0 \ + --wireguard {{ raspap_wireguard }} \ + --openvpn {{ raspap_openvpn }} \ + --adblock {{ raspap_adblock }} \ + --branch {{ raspap_branch }} + when: not raspap_stat.stat.exists + register: raspap_install + changed_when: raspap_install.rc == 0 + 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: + path: /etc/hostapd + state: directory + mode: '0755' + + - name: Template RaspAP network config to target + template: + src: "hostapd.conf.j2" + dest: "/etc/hostapd/hostapd.conf" + mode: '0644' + + - name: Copy hostapd set_hostapd_iface config script + template: + src: "set_hostapd_iface.py" + dest: "/usr/local/bin/set_hostapd_iface.py" + mode: '0755' + + - name: Copy hostapd set_hostapd_iface service file + template: + src: "set-hostapd-iface.service.j2" + dest: "/lib/systemd/system/set-hostapd-iface.service" + mode: '0755' + + - name: Download hostapd raspapd systemd service file + get_url: + url: "{{ config_base_url }}/raspapd.service" + dest: "/lib/systemd/system/raspapd.service" + owner: root + group: root + mode: '0644' + + - name: Enable service raspapd, avahi-daemon, and set_hostapd_iface by symlink + file: + src: "/lib/systemd/system/{{ item }}" + dest: "/etc/systemd/system/multi-user.target.wants/{{ item }}" + state: link + with_items: + - "raspapd.service" + - "set-hostapd-iface.service" + - "avahi-daemon.service" + + - name: Copy dnsmasq config + template: + src: "butterbox-dnsmasq.conf.j2" + dest: /etc/dnsmasq.d/butterbox-dnsmasq.conf + 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 diff --git a/ansible/install-usb-viewer.yml b/ansible/install-usb-viewer.yml new file mode 100644 index 0000000..820dc5f --- /dev/null +++ b/ansible/install-usb-viewer.yml @@ -0,0 +1,54 @@ +--- +- name: Install usb viewer + hosts: all + become: true + tasks: + - name: Copy systemd services + copy: + src: "{{ vmdb2_config_base_dir }}/{{ item }}" + dest: "/etc/systemd/system/{{ item }}" + loop: + - udisks2-mount@.service + - serve-usb@.service + + - name: Enable services by symlink + 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: + src: "{{ vmdb2_config_base_dir }}/99-usb-butter.rules" + dest: "/etc/udev/rules.d/99-usb-butter.rules" + + - name: Install udev trigger script + copy: + src: "{{ vmdb2_script_base_dir }}/on-usb-drive-mounted.sh" + 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) diff --git a/ansible/inventory/host_vars/butter-pc-1.local b/ansible/inventory/host_vars/butter-pc-1.local new file mode 100644 index 0000000..480772c --- /dev/null +++ b/ansible/inventory/host_vars/butter-pc-1.local @@ -0,0 +1,5 @@ +ansible_host: 192.168.2.104 +ansible_user: ana +ansible_password: ana +is_vmdb2: "false" +butter_user: "butter" diff --git a/ansible/inventory/host_vars/butter-pi-1.local b/ansible/inventory/host_vars/butter-pi-1.local new file mode 100644 index 0000000..97afb37 --- /dev/null +++ b/ansible/inventory/host_vars/butter-pi-1.local @@ -0,0 +1,4 @@ +ansible_host: 188.40.241.21 +ansible_user: root +is_vmdb2: "false" +butter_user: "butter" diff --git a/ansible/inventory/host_vars/butter.sr2hosted.app b/ansible/inventory/host_vars/butter.sr2hosted.app new file mode 100644 index 0000000..e69de29 diff --git a/ansible/inventory/production b/ansible/inventory/production new file mode 100644 index 0000000..e69de29 diff --git a/ansible/main.yml b/ansible/main.yml new file mode 100644 index 0000000..770412c --- /dev/null +++ b/ansible/main.yml @@ -0,0 +1,28 @@ +# main.yml +- import_playbook: butter-base.yml + tags: + - "base" + - "ap" + - "matrix" +- import_playbook: install-rasp-ap.yml + tags: "ap" + when: ap_mode_supported | bool +- import_playbook: deploy-butter-site.yml + tags: + - "website" + - "usb" +- import_playbook: install-chat.yml + tags: "matrix" +- import_playbook: cleanup.yml +- import_playbook: install-keanu-weblite.yml + tags: + - "matrix" + - "keanu" +- import_playbook: install-usb-viewer.yml + tags: "usb" +- import_playbook: install-ap-optimized-firmware.yml + tags: "ap" + when: ap_mode_supported | bool +- import_playbook: remove-wifi-creds.yml + tags: "ap" + when: ap_mode_supported | bool diff --git a/ansible/production b/ansible/production new file mode 100644 index 0000000..e69de29 diff --git a/ansible/remove-wifi-creds.yml b/ansible/remove-wifi-creds.yml new file mode 100644 index 0000000..20acf08 --- /dev/null +++ b/ansible/remove-wifi-creds.yml @@ -0,0 +1,11 @@ +--- +- name: Remove wifi creds + hosts: all + become: true + tasks: + - name: Copy wpa_supplicant config + copy: + src: "{{ vmdb2_config_base_dir }}/wpa_supplicant.conf" + dest: /etc/wpa_supplicant/wpa_supplicant.conf + force: true + mode: '0644' diff --git a/ansible/templates/butterbox-dnsmasq.conf.j2 b/ansible/templates/butterbox-dnsmasq.conf.j2 new file mode 100644 index 0000000..2a11557 --- /dev/null +++ b/ansible/templates/butterbox-dnsmasq.conf.j2 @@ -0,0 +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 diff --git a/ansible/templates/create_public_room.sh.j2 b/ansible/templates/create_public_room.sh.j2 new file mode 100644 index 0000000..02a3dec --- /dev/null +++ b/ansible/templates/create_public_room.sh.j2 @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# +# butterbox-setup.sh +# Registers a Matrix user and creates a public room. Fails on any non-200 response. + +#set -e # exit if any command fails + +MATRIX_HOMESERVER="http://localhost:8008" + +echo "Registering user {{ butter_name }}-admin..." + + +USERNAME="{{ butter_name }}-admin" +PASSWORD="{{ butter_name}}-admin" + + +HTTP_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST \ + "${MATRIX_HOMESERVER}/_matrix/client/v3/register?kind=user" \ + -H "Content-Type: application/json" \ + -d "{ + \"auth\": { + \"type\": \"m.login.dummy\" + }, + \"username\": \"$USERNAME\", + \"password\": \"$PASSWORD\", + \"initial_device_display_name\": \"{{ butter_name }}\" + }") + +# Separate body and status +HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') +HTTP_STATUS=$(echo "$HTTP_RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') + +if [ "$HTTP_STATUS" -ne 200 ]; then + echo "❌ Registration failed with HTTP status $HTTP_STATUS" + echo "Response from server:" + echo "$HTTP_BODY" +fi + +# Extract access_token using grep/sed +ACCESS_TOKEN=$(echo "$HTTP_BODY" | grep -o '"access_token"[^,}]*' | sed 's/.*: *"//; s/"$//') + +if [ -z "$ACCESS_TOKEN" ]; then + echo "❌ No access token found in registration response." + echo "Response:" + echo "$HTTP_BODY" + exit 1 +fi + +echo "✅ Registration successful!" +echo "Access Token: $ACCESS_TOKEN" + +# --- Create a room --- +ROOM_ALIAS_NAME="public" +ROOM_NAME="public" + +LANG_ARG={{ butter_language }} + +ROOM_TOPIC="This is a public, unencrypted room. To have a private conversation, create a new room." +if [ "$LANG_ARG" = "es" ]; then + ROOM_TOPIC="Esta es una sala de chat pública y sin cifrar. Para tener una conversación privada, crea una nueva sala." +fi + +echo "Creating public room '${ROOM_NAME}'..." + +ROOM_HTTP_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST \ + "${MATRIX_HOMESERVER}/_matrix/client/v3/createRoom" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"visibility\": \"public\", + \"preset\": \"public_chat\", + \"room_alias_name\": \"${ROOM_ALIAS_NAME}\", + \"name\": \"${ROOM_NAME}\", + \"topic\": \"${ROOM_TOPIC}\" + }") + +ROOM_HTTP_BODY=$(echo "$ROOM_HTTP_RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') +ROOM_HTTP_STATUS=$(echo "$ROOM_HTTP_RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') + +if [ "$ROOM_HTTP_STATUS" -ne 200 ]; then + SEARCH_STRING="already exists" + if echo "$HTTP_BODY" | grep -q "$SEARCH_STRING"; then + echo "Response from server:" + echo "$HTTP_BODY" + exit 0 + fi + echo "❌ Room creation failed with HTTP status $ROOM_HTTP_STATUS" + echo "Response from server:" + echo "$ROOM_HTTP_BODY" + exit 1 +fi + +echo "✅ Public room created successfully!" +echo "Response:" +echo "$ROOM_HTTP_BODY" diff --git a/ansible/templates/hostapd.conf.j2 b/ansible/templates/hostapd.conf.j2 new file mode 100644 index 0000000..1f8b8e4 --- /dev/null +++ b/ansible/templates/hostapd.conf.j2 @@ -0,0 +1,16 @@ +driver=nl80211 +ctrl_interface=/var/run/hostapd +ctrl_interface_group=0 +auth_algs=1 +wpa_key_mgmt=WPA-PSK +beacon_int=100 +ssid={{ butter_name }} +channel=1 +hw_mode=g +ieee80211n=0 +interface=wlan0 +wpa=none +wpa_pairwise=CCMP +wpa_passphrase=ThisIsInertAndSatisfiesValidation +country_code=US +ignore_broadcast_ssid=0 diff --git a/ansible/templates/set-hostapd-iface.service.j2 b/ansible/templates/set-hostapd-iface.service.j2 new file mode 100644 index 0000000..cd8bd2e --- /dev/null +++ b/ansible/templates/set-hostapd-iface.service.j2 @@ -0,0 +1,11 @@ +[Unit] +Description=Set Hostapd Interface at Boot +After=network.target systemd-udev-settle.service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/set_hostapd_iface.py +RemainAfterExit=true + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/ansible/templates/set_hostapd_iface.py b/ansible/templates/set_hostapd_iface.py new file mode 100644 index 0000000..c9ac365 --- /dev/null +++ b/ansible/templates/set_hostapd_iface.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +import subprocess +import re + +CONFIG_FILE = "/etc/hostapd/hostapd.conf" + +def find_ap_interface(): + try: + iw_dev_output = subprocess.check_output(["iw", "dev"], text=True) + except Exception as e: + print(f"Error running iw dev: {e}") + return None + + interfaces = re.findall(r"Interface\s+(\w+)", iw_dev_output) + + for iface in interfaces: + try: + modes_output = subprocess.check_output(["iw", "dev", iface, "info"], text=True) + # Check for AP mode support + phy_output = subprocess.check_output(["iw", iface, "info"], text=True, stderr=subprocess.DEVNULL) + except Exception: + continue + + try: + phy_name = re.search(r"wiphy (\d+)", modes_output) + if not phy_name: + continue + phy_info = subprocess.check_output(["iw", "phy", f"phy{phy_name.group(1)}", "info"], text=True) + if "AP" in phy_info: + return iface + except Exception: + continue + return None + +def update_config(interface_name): + try: + with open(CONFIG_FILE, "r") as f: + lines = f.readlines() + + with open(CONFIG_FILE, "w") as f: + for line in lines: + if line.strip().startswith("interface="): + f.write(f"interface={interface_name}\n") + else: + f.write(line) + print(f"Updated {CONFIG_FILE} to use interface: {interface_name}") + except Exception as e: + print(f"Error updating config: {e}") + +if __name__ == "__main__": + iface = find_ap_interface() + if iface: + update_config(iface) + else: + print("No AP-capable interface found.") diff --git a/vmdb2-recipes/ansible.cfg b/vmdb2-recipes/ansible.cfg new file mode 100644 index 0000000..c5f098a --- /dev/null +++ b/vmdb2-recipes/ansible.cfg @@ -0,0 +1,2 @@ +[defaults] +verbosity=1 diff --git a/vmdb2-recipes/image-specs b/vmdb2-recipes/image-specs new file mode 160000 index 0000000..ff7fdbf --- /dev/null +++ b/vmdb2-recipes/image-specs @@ -0,0 +1 @@ +Subproject commit ff7fdbf07c727ba1d2277dc7f274bd234f2e2bfa diff --git a/vmdb2-recipes/raspi_3_trixie.yaml b/vmdb2-recipes/raspi_3_trixie.yaml new file mode 100644 index 0000000..cd77140 --- /dev/null +++ b/vmdb2-recipes/raspi_3_trixie.yaml @@ -0,0 +1,196 @@ +--- +# 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: 3500M + + - 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: + - 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 + - avahi-daemon + 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" + + 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 + + + # 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 11:25 (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: en + butter_name: butterbox diff --git a/vmdb2-recipes/raspi_4_trixie.yaml b/vmdb2-recipes/raspi_4_trixie.yaml new file mode 100644 index 0000000..cd9925b --- /dev/null +++ b/vmdb2-recipes/raspi_4_trixie.yaml @@ -0,0 +1,198 @@ +--- +# 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 "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" + + 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: en + butter_name: butterbox +