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,42 @@
---
# Remote desktop for the 86Box host.
#
# RDP, not VNC — because 86Box needs SOUND and VNC carries no audio. xrdp
# redirects audio; that is the whole reason for this choice.
#
# xrdp destroys a session only on explicit LOGOUT, not on disconnect, so
# emulator instances survive closing the client. That is why 86Box is left as a
# normal desktop app rather than forced into a systemd unit: several GUI
# instances under a manager is a desktop, and pretending otherwise fights the
# tooling for no benefit.
retro_desktop_packages:
- xfce4 # light, and xrdp-friendly
- xfce4-terminal
- dbus-x11
- xrdp
- xorgxrdp
# Audio over RDP — the whole reason this box uses RDP rather than VNC.
#
# Ubuntu 24.04 is PipeWire-based, and it ships a PIPEWIRE-native xrdp module
# (`pipewire-module-xrdp`), so the old pulseaudio-module-xrdp build dance is not
# needed. It currently arrives as a transitive dependency of xrdp — listed here
# EXPLICITLY so a future dependency change cannot silently kill sound.
retro_desktop_audio: true
retro_desktop_audio_packages:
- pipewire-module-xrdp # provides /etc/xdg/autostart/pipewire-xrdp.desktop
- pipewire-pulse
- pulseaudio-utils # paplay/pactl, for testing inside a session
# Channels that must be enabled in xrdp.ini for audio to reach the client.
retro_desktop_audio_channels: [rdpsnd, drdynvc]
# What to rename xorgxrdp's pointer to, so 86Box can capture the mouse.
#
# This string is not cosmetic: it is matched CHARACTER FOR CHARACTER against
# 86Box's allow-list of pointer devices that report absolute coordinates
# (src/qt/xinput2_mouse.cpp, xinput2_get_xtest_pointer()). Change it and mouse
# capture breaks again. The full reasoning is in tasks/main.yml.
retro_desktop_xrdp_pointer_name: "TigerVNC pointer"
retro_desktop_xrdp_xorg_conf: /etc/X11/xrdp/xorg.conf
@@ -0,0 +1,183 @@
---
- name: Install the desktop and xrdp
ansible.builtin.apt:
name: "{{ retro_desktop_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
register: _pkgs
retries: 3
delay: 15
until: _pkgs is succeeded # the WAN drops out; do not fail a long run on one blip
- name: Install audio support
ansible.builtin.apt:
name: "{{ retro_desktop_audio_packages }}"
state: present
register: _apkgs
retries: 3
delay: 15
until: _apkgs is succeeded
when: retro_desktop_audio | bool
- name: Use XFCE for xrdp sessions
ansible.builtin.copy:
dest: /etc/skel/.xsession
content: "xfce4-session\n"
mode: "0644"
- name: Give the existing user an .xsession too
ansible.builtin.copy:
dest: "/home/{{ ansible_user }}/.xsession"
content: "xfce4-session\n"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0644"
# ⚠️ NO WINDOW MANAGER after login? It is a saved session, and it is
# self-perpetuating. xfce4-session restores exactly the client list in
# ~/.cache/sessions/xfce4-session-<host>:<display>, so once xfwm4 is missing
# from that list every later login is WM-less too — no title bars, no
# Applications menu, hence no way to log out and no way to fix it from inside.
# Seen 2026-07-28: the file listed only xfsettingsd, xfce4-panel, Thunar and
# xfdesktop. The distro's failsafe session does NOT cover this — it only runs
# when you pick Failsafe at the greeter, which xrdp never offers.
#
# How xfwm4 falls out of the list: any xfwm4 started outside the session
# manager. `xfwm4 --replace` over SSH has no SESSION_MANAGER in its environment
# and says so ("Failed to connect to session manager"), so the next save does
# not record it. That is what happened here — the recovery caused the relapse.
#
# Recovery, from SSH:
# DISPLAY=:10 XAUTHORITY=~/.Xauthority xfwm4 --replace &
# rm ~/.cache/sessions/xfce4-session-*
# Prevention, applied on this host by hand rather than by a task:
# xfconf-query -c xfce4-session -p /general/SaveOnExit -n -t bool -s false
# Deliberately NOT automated: the system-wide xfce4-session.xml also defines
# the failsafe session, so a role would have to either template the whole
# distro file or add python3-lxml for the xml module, and the per-user file it
# would otherwise write is rewritten by xfconfd at every logout — a permanently
# "changed" task guarding against an operator mistake.
# ---------------------------------------------------------------------------
# Mouse capture in 86Box: rename xorgxrdp's pointer device
# ---------------------------------------------------------------------------
# xorgxrdp's pointer LIES about itself. xrdpmouse/rdpMouse.c labels its two
# valuators REL_X/REL_Y — so XInput2 advertises "Mode: relative" with the dummy
# range -1..-1 — and then posts every event as
# xf86PostMotionEvent(device, TRUE /* is_absolute */, 0, 2, x, y)
# i.e. ABSOLUTE screen coordinates down a nominally RELATIVE axis. RDP itself
# carries no relative motion, so it has nothing else to send.
#
# 86Box's X11 input backend (src/qt/xinput2_mouse.cpp) reads XI_RawMotion and
# branches on exactly that axis mode: for a "relative" axis it feeds the raw
# valuator straight in as a movement delta. Under xrdp that "delta" is an
# absolute pixel coordinate, so the instant you click to capture, the emulated
# pointer is thrown into a corner and stays pinned — which presents as the
# mouse not being captured at all.
#
# Upstream already handles this failure mode, just not for us: VNC servers
# inject through XTEST, which tells the same lie, so 86Box keeps an allow-list
# of pointer device NAMES it treats as absolute — "TigerVNC pointer" and
# "Virtual core XTEST pointer" — and those take a correct absolute->delta path.
# An X input device is named after its xorg.conf Identifier, so renaming xrdp's
# pointer onto that list is the whole fix. Verified safe on this host: the
# identifier occurs only in the two lines this task rewrites (ServerLayout and
# the InputDevice section), and xrdp binds the device by DRIVER ("xrdpmouse"),
# never by identifier.
#
# Known limit, not fixable here: motion still stops when the CLIENT's cursor
# hits the edge of the remote screen, because there is no relative-motion event
# to send. xrdp PR #3091 (TS_RELPOINTER) is still an unmerged draft as of
# 0.9.24 / xorgxrdp 0.9.19. Escaping that needs a different transport, not a
# different setting.
#
# sesman starts a fresh Xorg per login, so nothing needs restarting — but a
# session that is already running keeps the old device until the user LOGS OUT.
# Disconnecting is not enough (that is the point of this host's setup).
- name: Present the xrdp pointer to 86Box as the absolute device it really is
ansible.builtin.replace:
path: "{{ retro_desktop_xrdp_xorg_conf }}"
regexp: '"xrdpMouse"'
replace: '"{{ retro_desktop_xrdp_pointer_name }}"'
# Stop needrestart from ever restarting xrdp-sesman. It has stranded a live
# desktop twice now: 2026-07-25 (our own `apt install` pulled libcap2) and
# 2026-07-28 (unattended-upgrades pulled libc6 at 06:28). Both times sesman came
# back with an EMPTY session table, so it could no longer reattach the running
# display — every reconnect started a new one, xfce4-session refused to run
# twice for the same user, and each new session died in about a second. The
# desktop and its 86Box VM keep running, just permanently unreachable.
#
# `NEEDRESTART_MODE: l` in retrolab.yml only covers playbook runs. It does
# nothing about unattended-upgrades, which is what actually did it the second
# time — so the exemption has to live on the host.
#
# This is not a novel idea: needrestart.conf already ships `override_rc`
# entries pinning gdm, sddm, xdm and friends to 0, because restarting the thing
# that owns your login sessions costs more than it fixes. xrdp-sesman is the
# same class of service and simply is not on that list.
#
# The trade, stated plainly: sesman keeps running against the old libc until
# the host is rebooted. That is the intended outcome — a stale session manager
# that works beats a fresh one that has orphaned every session.
- name: Never let needrestart bounce the xrdp session manager
ansible.builtin.copy:
dest: /etc/needrestart/conf.d/50-xrdp.conf
# conf.d is parsed after needrestart.conf and merges into the hash, so this
# ADDS a key rather than replacing the shipped defaults. Assigning
# $nrconf{override_rc} wholesale here would silently drop them.
content: |
# Managed by Ansible (proxmox/ansible/roles/retro_desktop). See that role
# for why: restarting sesman empties its session table and strands every
# live xrdp desktop until the user logs out.
$nrconf{override_rc}{qr(^xrdp)} = 0;
owner: root
group: root
mode: "0644"
- name: Enable xrdp
ansible.builtin.systemd_service:
name: "{{ item }}"
enabled: true
state: started
loop: [xrdp, xrdp-sesman]
# Audio silently not working is the classic xrdp complaint; assert the pieces
# rather than discover it mid-session.
- name: Read the xrdp channel configuration
ansible.builtin.command:
cmd: sed -n '/^\[Channels\]/,/^\[/p' /etc/xrdp/xrdp.ini
register: _chan
changed_when: false
# Reads nothing and writes nothing, but `command` is skipped under --check by
# default, which left `_chan.stdout` empty and made the assert below fail
# every dry run with a false "audio is broken".
check_mode: false
when: retro_desktop_audio | bool
- name: Assert the audio channels are enabled
ansible.builtin.assert:
that: "'{{ item }}=true' in _chan.stdout"
fail_msg: "xrdp channel {{ item }} is not enabled — audio will not reach the client."
quiet: true
loop: "{{ retro_desktop_audio_channels }}"
when: retro_desktop_audio | bool
- name: Assert the PipeWire xrdp module is present
ansible.builtin.stat:
path: /etc/xdg/autostart/pipewire-xrdp.desktop
register: _pwx
when: retro_desktop_audio | bool
- name: Fail if the audio module is missing
ansible.builtin.assert:
that: _pwx.stat.exists
fail_msg: "pipewire-module-xrdp is not installed; xrdp sessions will have no sound."
quiet: true
when: retro_desktop_audio | bool
- name: Confirm xrdp is listening on 3389
ansible.builtin.wait_for:
port: 3389
timeout: 30