Establish clean homelab infrastructure baseline
Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
---
|
||||
# 86Box + Avalonia86, installed as AppImages.
|
||||
#
|
||||
# WHY AppImage: 86Box is not in the Ubuntu archive. The alternatives were a
|
||||
# Flatpak (adds a ~1-2GB runtime, and its sandbox complicates letting the
|
||||
# manager pass per-VM config paths) or building from source (large Qt6/SDL2
|
||||
# toolchain, manual updates). Upstream publishes official Linux AppImages for
|
||||
# BOTH 86Box and Avalonia86, so this is a single file each, no runtime, no
|
||||
# sandbox, and the manager can point straight at the binary.
|
||||
|
||||
retro_86box_dir: /opt/86box
|
||||
|
||||
# Pinned. Bump deliberately — a silent upstream change to the emulator is not
|
||||
# something you want arriving with an unrelated play run.
|
||||
retro_86box_version: "6.0"
|
||||
retro_86box_build: "b9001"
|
||||
retro_86box_url: "https://github.com/86Box/86Box/releases/download/v{{ retro_86box_version }}/86Box-Linux-x86_64-{{ retro_86box_build }}.AppImage"
|
||||
|
||||
retro_86box_mgr_version: "1.5.1"
|
||||
retro_86box_mgr_url: "https://github.com/notBald/Avalonia86/releases/download/v{{ retro_86box_mgr_version }}/Avalonia-86-for-Linux-x64-{{ retro_86box_mgr_version }}.AppImage"
|
||||
|
||||
# ROMs are NOT bundled with 86Box (licensing). Without them no machine will
|
||||
# boot — the emulator starts and then cannot find a system BIOS.
|
||||
retro_86box_roms_url: "https://github.com/86Box/roms/archive/refs/tags/v{{ retro_86box_version }}.tar.gz"
|
||||
|
||||
# AppImages need FUSE. Ubuntu 24.04 does not ship libfuse2 by default; without
|
||||
# it an AppImage fails with "dlopen(): error loading libfuse.so.2".
|
||||
# patchelf + setcap are needed to make TAP networking work (see below).
|
||||
retro_86box_packages:
|
||||
- libfuse2t64
|
||||
- patchelf
|
||||
- libcap2-bin
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TAP networking: why the AppImage is extracted instead of run as-is
|
||||
# ---------------------------------------------------------------------------
|
||||
# 86Box's TAP backend creates the tap device itself and enslaves it to a
|
||||
# bridge (verified in src/network/net_tap.c):
|
||||
# ifr.ifr_flags = IFF_TAP | IFF_NO_PI; ioctl(fd, TUNSETIFF, &ifr);
|
||||
# ioctl_or_fail(sock, SIOCBRADDBR, &ifr_bridge); /* create bridge */
|
||||
# ioctl_or_fail(sock, SIOCBRADDIF, &ifr_bridge); /* enslave the tap */
|
||||
# so it needs CAP_NET_ADMIN (upstream's own error text says as much). There is
|
||||
# no "pre-create the tap and hand it over" path -- the setting is a BRIDGE
|
||||
# name, not a tap name.
|
||||
#
|
||||
# File capabilities cannot be applied to the AppImage: the real ELF lives
|
||||
# inside a read-only squashfs mounted through FUSE with nosuid, where the
|
||||
# kernel ignores file caps entirely. So the AppImage is extracted once and the
|
||||
# extracted binary carries the caps.
|
||||
#
|
||||
# That in turn breaks the AppImage's own library loading, because a file with
|
||||
# capabilities runs in secure-execution mode (AT_SECURE=1) and glibc drops
|
||||
# LD_LIBRARY_PATH -- which is exactly how AppRun points the binary at its
|
||||
# bundled Qt. Two patches fix it, and BOTH are load-bearing:
|
||||
# 1. PT_INTERP is the RELATIVE path "lib64/ld-linux-x86-64.so.2"; it must be
|
||||
# made absolute or exec fails with "required file not found".
|
||||
# 2. The rpath must be DT_RPATH (--force-rpath), NOT the modern DT_RUNPATH.
|
||||
# RUNPATH is not inherited by transitive dependencies, and 8 of the
|
||||
# bundled libraries (libicuuc, libpcre2-16, libFLAC, ...) are needed by
|
||||
# Qt itself rather than by 86Box, so with RUNPATH they are "not found".
|
||||
retro_86box_app_dir: "{{ retro_86box_dir }}/app"
|
||||
retro_86box_bin: "{{ retro_86box_app_dir }}/usr/local/bin/86Box"
|
||||
retro_86box_wrapper: /usr/local/bin/86box
|
||||
retro_86box_interp: /lib64/ld-linux-x86-64.so.2
|
||||
retro_86box_caps: "CAP_NET_RAW,CAP_NET_ADMIN=eip"
|
||||
|
||||
# Taken from the AppImage's own AppRun.env (APPDIR_LIBRARY_PATH), made
|
||||
# absolute. Keep in sync if a future 86Box release changes its bundle layout.
|
||||
retro_86box_rpath_dirs:
|
||||
- "{{ retro_86box_app_dir }}/lib/x86_64-linux-gnu"
|
||||
- "{{ retro_86box_app_dir }}/usr/lib"
|
||||
- "{{ retro_86box_app_dir }}/usr/lib/x86_64-linux-gnu"
|
||||
- "{{ retro_86box_app_dir }}/lib/x86_64"
|
||||
|
||||
# The bridge 86Box should attach its taps to. Must match tap_bridge_name.
|
||||
retro_86box_tap_bridge: br-retro
|
||||
@@ -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 }} "$@"
|
||||
Reference in New Issue
Block a user