Establish clean homelab infrastructure baseline
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled

Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
2026-09-09 16:47:20 +00:00
commit 88a02ababa
418 changed files with 50579 additions and 0 deletions
@@ -0,0 +1,127 @@
---
- name: Install AppImage runtime dependency
ansible.builtin.apt:
name: "{{ retro_86box_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
register: _fuse
retries: 3
delay: 15
until: _fuse is succeeded
- name: Create the 86Box directory
ansible.builtin.file:
path: "{{ retro_86box_dir }}"
state: directory
mode: "0755"
- name: Download 86Box and Avalonia86
# The WAN drops out; get_url resumes rather than restarting from zero.
ansible.builtin.get_url:
url: "{{ item.url }}"
dest: "{{ retro_86box_dir }}/{{ item.name }}"
mode: "0755"
loop:
- { name: "86Box.AppImage", url: "{{ retro_86box_url }}" }
- { name: "Avalonia86.AppImage", url: "{{ retro_86box_mgr_url }}" }
loop_control:
label: "{{ item.name }}"
register: _dl
retries: 3
delay: 20
until: _dl is succeeded
- name: Check whether ROMs are already extracted
ansible.builtin.stat:
path: "{{ retro_86box_dir }}/roms/machines"
register: _roms
- name: Create the roms directory
ansible.builtin.file:
path: "{{ retro_86box_dir }}/roms"
state: directory
mode: "0755"
- name: Download and extract the ROM set
# Extract INTO roms/, stripping the tarball's own roms-<ver>/ wrapper.
# Extracting to the parent with --strip-components=1 dumps machines/, floppy/,
# hdd/ ... loose next to the binaries, which is NOT the layout 86Box and
# Avalonia86 expect (they want everything under roms/).
ansible.builtin.unarchive:
src: "{{ retro_86box_roms_url }}"
dest: "{{ retro_86box_dir }}/roms"
remote_src: true
extra_opts: [--strip-components=1]
creates: "{{ retro_86box_dir }}/roms/machines"
when: not _roms.stat.exists
register: _romdl
retries: 3
delay: 20
until: _romdl is succeeded
- name: Extract the AppImage and grant it CAP_NET_ADMIN for TAP networking
ansible.builtin.include_tasks: privileged.yml
- name: Add desktop launchers
ansible.builtin.copy:
dest: "/usr/share/applications/{{ item.file }}"
mode: "0644"
content: |
[Desktop Entry]
Type=Application
Name={{ item.name }}
Exec={{ item.bin }}
Icon=computer
Categories=System;Emulator;
Terminal=false
loop:
- file: "avalonia86.desktop"
name: "Avalonia86 (86Box manager)"
bin: "{{ retro_86box_dir }}/Avalonia86.AppImage"
# NOT the .AppImage: only the extracted binary carries CAP_NET_ADMIN, so
# launching the AppImage would silently lose TAP networking.
- file: "86box.desktop"
name: "86Box"
bin: "{{ retro_86box_wrapper }}"
loop_control:
label: "{{ item.name }}"
- name: Verify 86Box actually runs
# Runs the CAPABILITY-BEARING binary, which is the one that must work.
# QT_QPA_PLATFORM=offscreen is REQUIRED: 86Box is a Qt GUI app and still
# initialises a display for --help, so it dies headless with
# "could not connect to display / Could not load the Qt platform plugin xcb".
# This also proves the patchelf work: under secure-execution mode (which
# file capabilities trigger) glibc drops LD_LIBRARY_PATH, so if the RPATH or
# the interpreter were wrong this step fails with "required file not found".
ansible.builtin.command:
cmd: "{{ retro_86box_wrapper }} --help"
environment:
QT_QPA_PLATFORM: offscreen
register: _ver
changed_when: false
failed_when: "'86box' not in (_ver.stdout + _ver.stderr) | lower and 'usage' not in (_ver.stdout + _ver.stderr) | lower"
- name: Read back the capabilities actually on the binary
ansible.builtin.command:
cmd: "getcap {{ retro_86box_bin }}"
register: _capcheck
changed_when: false
- name: Verify the capabilities survived
ansible.builtin.assert:
that:
- "'cap_net_admin' in _capcheck.stdout"
- "'cap_net_raw' in _capcheck.stdout"
fail_msg: "86Box has no CAP_NET_ADMIN; TAP networking will fail to allocate a tap device"
success_msg: "{{ _capcheck.stdout | trim }}"
- name: Report
ansible.builtin.debug:
msg:
- "86Box: {{ retro_86box_wrapper }} -> {{ retro_86box_bin }}"
- "caps: {{ _capcheck.stdout | trim }}"
- "roms: {{ retro_86box_dir }}/roms"
- "ISOs: /mnt/iso (read-only)"
- "TAP: set each NIC to 'TAP' with bridge '{{ retro_86box_tap_bridge }}'"
@@ -0,0 +1,102 @@
---
# Extract the AppImage and give the real binary CAP_NET_ADMIN so TAP
# networking works. See the long comment in defaults/main.yml for why every
# step here is necessary.
- name: Read the extracted build id
# The extracted tree is a derived artifact; it must be rebuilt whenever the
# pinned AppImage build changes, or a version bump would leave the old
# binary in place while the AppImage next to it says otherwise.
ansible.builtin.slurp:
src: "{{ retro_86box_app_dir }}/.build-id"
register: _bid
failed_when: false
changed_when: false
- name: Decide whether the AppImage must be re-extracted
ansible.builtin.set_fact:
_86box_extract: >-
{{ _bid.content is not defined
or (_bid.content | b64decode | trim) != retro_86box_build }}
- name: Extract the AppImage
# --appimage-extract always writes ./squashfs-root in the CWD and refuses to
# target a directory, hence the extract-then-move.
ansible.builtin.shell:
cmd: |
set -e
rm -rf "{{ retro_86box_app_dir }}" "{{ retro_86box_dir }}/squashfs-root"
cd "{{ retro_86box_dir }}"
./86Box.AppImage --appimage-extract >/dev/null
mv squashfs-root "{{ retro_86box_app_dir }}"
when: _86box_extract
changed_when: true
- name: Read the current ELF interpreter
ansible.builtin.command:
cmd: "patchelf --print-interpreter {{ retro_86box_bin }}"
register: _interp
changed_when: false
- name: Make the ELF interpreter absolute
ansible.builtin.command:
cmd: "patchelf --set-interpreter {{ retro_86box_interp }} {{ retro_86box_bin }}"
when: _interp.stdout | trim != retro_86box_interp
changed_when: true
- name: Read the current rpath
ansible.builtin.command:
cmd: "patchelf --print-rpath {{ retro_86box_bin }}"
register: _rpath
changed_when: false
- name: Bake the bundled library directories in as DT_RPATH
ansible.builtin.command:
cmd: >-
patchelf --force-rpath --set-rpath
"{{ retro_86box_rpath_dirs | join(':') }}" {{ retro_86box_bin }}
when: _rpath.stdout | trim != (retro_86box_rpath_dirs | join(':'))
changed_when: true
- name: Verify no library is left unresolved
# Must pass BEFORE setcap is worth doing -- a capability binary that cannot
# load its libraries fails with a misleading "required file not found".
ansible.builtin.shell:
cmd: "ldd {{ retro_86box_bin }} 2>&1 | grep -c 'not found' || true"
register: _missing
changed_when: false
failed_when: (_missing.stdout | trim | int) != 0
- name: Read the current capabilities
ansible.builtin.command:
cmd: "getcap {{ retro_86box_bin }}"
register: _caps
changed_when: false
- name: Grant CAP_NET_ADMIN and CAP_NET_RAW
# NOTE: must run AFTER patchelf. patchelf rewrites the file and drops the
# security.capability xattr, so setting caps first silently loses them.
ansible.builtin.command:
cmd: "setcap '{{ retro_86box_caps }}' {{ retro_86box_bin }}"
when: "'cap_net_admin' not in _caps.stdout or 'cap_net_raw' not in _caps.stdout"
changed_when: true
- name: Record the extracted build id
ansible.builtin.copy:
content: "{{ retro_86box_build }}\n"
dest: "{{ retro_86box_app_dir }}/.build-id"
mode: "0644"
- name: Install the launcher wrapper
ansible.builtin.copy:
dest: "{{ retro_86box_wrapper }}"
mode: "0755"
content: |
#!/bin/sh
# Managed by Ansible (roles/retro_86box).
#
# Runs the EXTRACTED 86Box, not the AppImage: only the extracted binary
# can carry CAP_NET_ADMIN, which 86Box needs to create its tap device
# and enslave it to {{ retro_86box_tap_bridge }}. Point Avalonia86 at
# this path, not at the .AppImage.
exec {{ retro_86box_bin }} "$@"