Add Gitea Actions runner, ansible-lint CI, vault setup, and fix group_vars layout

- Move group_vars into inventory/ so playbooks can find them (was only
  visible to ad-hoc commands via CWD)
- Add Ansible Vault for sensitive variables (gitea/grafana passwords,
  tokens, db credentials) with vault_password_file outside the repo
- Enable Gitea Actions and deploy act_runner with Docker backend on
  the gitea VM
- Add .gitea/workflows/lint.yml to run ansible-lint on every push
- Set up Gitea → GitHub push mirror (sync_on_commit)
- Fix ansible.cfg stdout_callback for ansible-core 2.20 compatibility
- Add python-psycopg2 to gitea role (required for postgresql modules)
- Add docker to gitea_runner role (required by act_runner at startup)
- Exclude password hashes and VM images from git

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Adyrem
2026-05-13 17:34:03 +02:00
parent 72d07f49e3
commit 39be9fafe7
25 changed files with 1584 additions and 6 deletions
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env bash
# Run in the Arch live environment to generate archinstall-config-physical.json
# with correct partition sizes for the target SSD.
#
# Usage:
# ./generate-physical-config.sh /dev/sda # outputs to /tmp/config.json
# ./generate-physical-config.sh /dev/nvme0n1p0 # NVMe example
set -euo pipefail
info() { echo -e "\033[1;34m==>\033[0m $*"; }
die() { echo -e "\033[1;31mERROR:\033[0m $*" >&2; exit 1; }
DEVICE="${1:-}"
if [[ -z "$DEVICE" ]]; then
echo "Usage: $0 <ssd-device>"
echo
echo "Available disks:"
lsblk -d -o NAME,SIZE,MODEL
exit 1
fi
[[ -b "$DEVICE" ]] || die "Device '$DEVICE' not found or is not a block device."
# ---------------------------------------------------------------------------
# Partition layout
# ---------------------------------------------------------------------------
# Layout:
# [1 MiB gap] [1 GiB ESP /boot] [rest: BTRFS root with subvolumes]
#
START_PADDING_B=1048576 # 1 MiB
ESP_B=1073741824 # 1 GiB
END_PADDING_B=1048576 # 1 MiB — leave 1 MiB at end for partition table alignment
ROOT_START_B=$(( START_PADDING_B + ESP_B )) # = 1074790400
DISK_BYTES=$(lsblk -b -d -n -o SIZE "$DEVICE")
ROOT_SIZE_B=$(( DISK_BYTES - ROOT_START_B - END_PADDING_B ))
info "Device: $DEVICE"
info "Disk size: $DISK_BYTES bytes ($(( DISK_BYTES / 1024 / 1024 / 1024 )) GiB)"
info "ESP start: $START_PADDING_B size: $ESP_B"
info "Root start: $ROOT_START_B size: $ROOT_SIZE_B"
(( ROOT_SIZE_B > 0 )) || die "Disk is too small to partition."
OUT="/tmp/config.json"
cat > "$OUT" << EOF
{
"app_config": null,
"archinstall-language": "English",
"auth_config": {},
"bootloader_config": {
"bootloader": "Systemd-boot",
"removable": true,
"uki": false
},
"custom_commands": [
"mkdir -p /home/adyrem/.ssh && chmod 700 /home/adyrem/.ssh && echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdW4rhQFv561bYs8w0/VgR6AFgOLvVsAh6pcjZ/CO2R homelab' > /home/adyrem/.ssh/authorized_keys && chmod 600 /home/adyrem/.ssh/authorized_keys && chown -R adyrem:adyrem /home/adyrem/.ssh",
"mkdir -p /vms/infra /vms/dev"
],
"disk_config": {
"btrfs_options": {
"snapshot_config": null
},
"config_type": "manual_partitioning",
"device_modifications": [
{
"device": "$DEVICE",
"wipe": true,
"partitions": [
{
"btrfs": [],
"dev_path": null,
"flags": ["boot", "esp"],
"fs_type": "fat32",
"mount_options": [],
"mountpoint": "/boot",
"obj_id": "part-esp",
"size": {
"sector_size": { "unit": "B", "value": 512 },
"unit": "GiB",
"value": 1
},
"start": {
"sector_size": { "unit": "B", "value": 512 },
"unit": "MiB",
"value": 1
},
"status": "create",
"type": "primary"
},
{
"btrfs": [
{ "name": "@", "mountpoint": "/" },
{ "name": "@home", "mountpoint": "/home" },
{ "name": "@var", "mountpoint": "/var" },
{ "name": "@snapshots", "mountpoint": "/.snapshots" },
{ "name": "@vms-infra", "mountpoint": "/vms/infra" },
{ "name": "@vms-dev", "mountpoint": "/vms/dev" }
],
"dev_path": null,
"flags": [],
"fs_type": "btrfs",
"mount_options": ["compress=zstd", "noatime"],
"mountpoint": null,
"obj_id": "part-root",
"size": {
"sector_size": { "unit": "B", "value": 512 },
"unit": "B",
"value": $ROOT_SIZE_B
},
"start": {
"sector_size": { "unit": "B", "value": 512 },
"unit": "B",
"value": $ROOT_START_B
},
"status": "create",
"type": "primary"
}
]
}
]
},
"hostname": "homelab",
"kernels": ["linux"],
"locale_config": {
"kb_layout": "de_CH-latin1",
"sys_enc": "UTF-8",
"sys_lang": "en_US"
},
"mirror_config": {
"custom_repositories": [],
"custom_servers": [],
"mirror_regions": {
"Worldwide": ["https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"]
},
"optional_repositories": []
},
"network_config": {
"type": "nm"
},
"ntp": true,
"packages": [
"base-devel",
"git",
"openssh",
"networkmanager",
"vim",
"btrbk",
"nftables"
],
"parallel_downloads": 5,
"script": "guided",
"services": ["sshd", "NetworkManager", "nftables"],
"swap": {
"algorithm": "zstd",
"enabled": false
},
"timezone": "Europe/Zurich",
"version": "4.1",
"users": [
{
"enc_password": "\$y\$j9T\$pqMooSvLvxBg2YP7eCQlq/\$Ae8.dvSYB817bQkj5w9S4wu9OtWIq.p/pbWY6A/ythD",
"groups": ["wheel"],
"sudo": true,
"username": "adyrem"
}
]
}
EOF
echo
echo "Config written to $OUT"
echo "Run: archinstall --config $OUT"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Run this inside the Arch live environment on physical hardware.
# Fetches the config generator from the host HTTP server, generates the config
# for the correct SSD device, then starts archinstall.
#
# Usage:
# curl http://<host-ip>:8080/scripts/physical-install.sh | bash
# or with device override:
# curl http://<host-ip>:8080/scripts/physical-install.sh -o /tmp/install.sh
# bash /tmp/install.sh /dev/sda
set -euo pipefail
HOST="${HOST:-http://192.168.1.1:8080}" # update to your host's LAN IP
DEVICE="${1:-}"
echo "==> Available disks:"
lsblk -d -o NAME,SIZE,MODEL
echo
if [[ -z "$DEVICE" ]]; then
read -rp "Enter SSD device for OS install (e.g. /dev/sda): " DEVICE
fi
echo "==> Fetching config generator..."
curl -fsSL "$HOST/scripts/generate-physical-config.sh" -o /tmp/generate-config.sh
chmod +x /tmp/generate-config.sh
echo "==> Generating config for $DEVICE..."
/tmp/generate-config.sh "$DEVICE"
echo "==> Starting archinstall..."
archinstall --config /tmp/config.json
+268
View File
@@ -0,0 +1,268 @@
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# setup-arch-vm.sh
# Creates a minimal Arch Linux VM on the local Fedora host using KVM/libvirt.
# VM disk images are stored under homelab/vms/.
# ---------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
VM_DIR="$PROJECT_DIR/vms"
ISO_DIR="$VM_DIR/iso"
VM_NAME="arch-base"
VM_DISK="$VM_DIR/${VM_NAME}.qcow2"
VM_RAM=2048 # MiB
VM_CPUS=2
VM_DISK_SIZE=20 # GiB
ARCH_ISO_URL="https://geo.mirror.pkgbuild.com/iso/2026.04.01/archlinux-2026.04.01-x86_64.iso"
ARCH_ISO_SHA256="f14bf46afbe782d28835aed99bfa2fe447903872cb9f4b21153196d6ed1d48ae"
ARCH_ISO="$ISO_DIR/archlinux-2026.04.01-x86_64.iso"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
info() { echo -e "\033[1;34m==>\033[0m $*"; }
ok() { echo -e "\033[1;32m ✓\033[0m $*"; }
warn() { echo -e "\033[1;33m !\033[0m $*"; }
die() { echo -e "\033[1;31mERROR:\033[0m $*" >&2; exit 1; }
require_cmd() { command -v "$1" &>/dev/null || die "'$1' not found. Install it and re-run."; }
# ---------------------------------------------------------------------------
# Pre-flight checks
# ---------------------------------------------------------------------------
info "Running pre-flight checks..."
require_cmd virsh
require_cmd virt-install
require_cmd qemu-img
require_cmd sha256sum
# KVM modules — load if not already present
if ! lsmod | grep -q '^kvm'; then
info "KVM modules not loaded — attempting to load..."
if grep -q "GenuineIntel" /proc/cpuinfo 2>/dev/null; then
sudo modprobe kvm_intel || die "Failed to load kvm_intel. Enable virtualisation in BIOS/UEFI."
else
sudo modprobe kvm_amd || die "Failed to load kvm_amd. Enable virtualisation in BIOS/UEFI."
fi
fi
ok "KVM modules loaded"
# br_netfilter — required for virtnetworkd to configure bridge netfilter rules;
# without it /proc/sys/net/bridge/ doesn't exist and net-start fails with EPERM
if ! lsmod | grep -q '^br_netfilter'; then
info "Loading br_netfilter module..."
sudo modprobe br_netfilter || die "Failed to load br_netfilter kernel module."
fi
# Persist across reboots
if [[ ! -f /etc/modules-load.d/libvirt.conf ]]; then
echo -e "kvm_intel\nbr_netfilter" | sudo tee /etc/modules-load.d/libvirt.conf >/dev/null
fi
ok "br_netfilter loaded"
# libvirt daemons — Fedora uses modular socket-activated daemons, not monolithic libvirtd
LIBVIRT_SOCKETS=(virtqemud virtnetworkd virtnodedevd virtstoraged virtsecretd)
info "Enabling libvirt modular daemons..."
for svc in "${LIBVIRT_SOCKETS[@]}"; do
if ! systemctl is-active --quiet "${svc}.socket"; then
sudo systemctl enable --now "${svc}.socket"
fi
# Remove the idle timeout so daemons don't shut down mid-operation
if [[ ! -f /etc/systemd/system/${svc}.service.d/notimeout.conf ]]; then
sudo mkdir -p "/etc/systemd/system/${svc}.service.d"
printf '[Service]\nEnvironment=%s_ARGS=\n' "${svc^^}" \
| sudo tee "/etc/systemd/system/${svc}.service.d/notimeout.conf" >/dev/null
fi
if ! systemctl is-active --quiet "${svc}.service"; then
sudo systemctl start "${svc}.service" 2>/dev/null || true
fi
done
sudo systemctl daemon-reload
ok "libvirt modular daemons ready"
# Group membership — required for polkit to allow bridge/network operations
if ! id -nG | grep -qw libvirt; then
info "Adding $USER to libvirt and kvm groups..."
sudo usermod -aG libvirt,kvm "$USER"
echo
echo " ┌─────────────────────────────────────────────────────────┐"
echo " │ Group membership updated. You must start a new shell │"
echo " │ for the change to take effect, then re-run the script. │"
echo " │ │"
echo " │ Run: newgrp libvirt │"
echo " │ Then: ./scripts/setup-arch-vm.sh │"
echo " └─────────────────────────────────────────────────────────┘"
exit 0
fi
ok "User is in libvirt group"
VIRSH="virsh --connect qemu:///system"
VIRT_INSTALL="virt-install --connect qemu:///system"
QEMU_IMG="qemu-img"
# ---------------------------------------------------------------------------
# Default network
# ---------------------------------------------------------------------------
info "Checking libvirt default network..."
if ! $VIRSH net-list --all | grep -q 'default'; then
info "Default network not defined — creating it..."
$VIRSH net-define /usr/share/libvirt/networks/default.xml \
|| die "Could not define default network. Is libvirt-daemon-config-network installed?"
fi
if ! $VIRSH net-list | grep -q 'default'; then
# Remove a stale virbr0 left over from a previous failed start
if ip link show virbr0 &>/dev/null; then
info "Removing stale virbr0 bridge..."
sudo ip link set virbr0 down 2>/dev/null || true
sudo ip link delete virbr0 2>/dev/null || true
fi
$VIRSH net-start default || true # ignore "already active" race
ok "Default network started"
fi
$VIRSH net-autostart default &>/dev/null || true
ok "Default network ready"
# ---------------------------------------------------------------------------
# Directory setup
# ---------------------------------------------------------------------------
info "Creating VM storage directories..."
mkdir -p "$VM_DIR" "$ISO_DIR"
# Grant the qemu user search (execute) permission on every directory component
# leading to the VM storage — QEMU runs as uid qemu and can't traverse home dirs.
if command -v setfacl &>/dev/null; then
dir="$VM_DIR"
while [[ "$dir" != "/" ]]; do
setfacl -m u:qemu:x "$dir" 2>/dev/null || sudo setfacl -m u:qemu:x "$dir"
dir="$(dirname "$dir")"
done
# Also allow qemu to read/write inside the vms directory itself
setfacl -m u:qemu:rwx "$VM_DIR" 2>/dev/null || sudo setfacl -m u:qemu:rwx "$VM_DIR"
setfacl -m u:qemu:rx "$ISO_DIR" 2>/dev/null || sudo setfacl -m u:qemu:rx "$ISO_DIR"
else
warn "setfacl not found — install 'acl' package if QEMU reports permission denied."
fi
ok "Directories ready: $VM_DIR"
# ---------------------------------------------------------------------------
# ISO download
# ---------------------------------------------------------------------------
if [[ -f "$ARCH_ISO" ]]; then
info "ISO already present — verifying checksum..."
echo "$ARCH_ISO_SHA256 $ARCH_ISO" | sha256sum --check --quiet \
&& ok "Checksum OK" \
|| { warn "Checksum mismatch — re-downloading ISO..."; rm -f "$ARCH_ISO"; }
fi
if [[ ! -f "$ARCH_ISO" ]]; then
info "Downloading Arch Linux ISO (~1.1 GB)..."
curl -L --progress-bar -o "$ARCH_ISO" "$ARCH_ISO_URL"
info "Verifying checksum..."
echo "$ARCH_ISO_SHA256 $ARCH_ISO" | sha256sum --check --quiet \
|| die "ISO checksum verification failed. File may be corrupt."
ok "ISO downloaded and verified"
fi
# ---------------------------------------------------------------------------
# VM disk image
# ---------------------------------------------------------------------------
if [[ -f "$VM_DISK" ]]; then
warn "Disk image already exists at $VM_DISK"
read -rp " Delete and recreate? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || die "Aborting — existing disk left intact."
rm -f "$VM_DISK"
fi
info "Creating ${VM_DISK_SIZE}G qcow2 disk image..."
$QEMU_IMG create -f qcow2 "$VM_DISK" "${VM_DISK_SIZE}G" -q
ok "Disk image created: $VM_DISK"
# ---------------------------------------------------------------------------
# VM definition
# ---------------------------------------------------------------------------
if $VIRSH dominfo "$VM_NAME" &>/dev/null; then
warn "VM '$VM_NAME' already defined in libvirt."
read -rp " Undefine and recreate? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || die "Aborting — existing VM left intact."
$VIRSH destroy "$VM_NAME" 2>/dev/null || true
$VIRSH undefine "$VM_NAME" --remove-all-storage 2>/dev/null || true
fi
# Verify virbr0 is actually up — virtnetworkd may have timed out and torn it down
if ! ip link show virbr0 &>/dev/null; then
info "virbr0 is gone — restarting default network..."
sudo systemctl restart virtnetworkd.service
sleep 2
$VIRSH net-start default 2>/dev/null || true
ip link show virbr0 &>/dev/null || die "virbr0 still missing after restart — check virtnetworkd logs."
fi
OVMF_CODE="/usr/share/edk2/ovmf/OVMF_CODE.fd"
OVMF_VARS="/usr/share/edk2/ovmf/OVMF_VARS.fd"
info "Defining VM '$VM_NAME'..."
$VIRT_INSTALL \
--name "$VM_NAME" \
--ram "$VM_RAM" \
--vcpus "$VM_CPUS" \
--cpu host-passthrough \
--os-variant archlinux \
--disk "path=$VM_DISK,format=qcow2,bus=virtio,cache=writeback" \
--disk "path=$ARCH_ISO,device=cdrom,readonly=on,bus=sata" \
--network network=default,model=virtio \
--graphics spice,listen=127.0.0.1 \
--video qxl \
--boot "loader=${OVMF_CODE},loader.readonly=yes,loader.type=pflash,nvram.template=${OVMF_VARS},cdrom,hd" \
--noautoconsole
ok "VM '$VM_NAME' defined and started"
# ---------------------------------------------------------------------------
# Get VM IP (may take a moment for DHCP)
# ---------------------------------------------------------------------------
info "Waiting for VM to get a DHCP address (up to 60s)..."
VM_IP=""
for i in $(seq 1 12); do
sleep 5
VM_IP=$($VIRSH domifaddr "$VM_NAME" 2>/dev/null | grep -oP '\d+\.\d+\.\d+\.\d+' | head -1 || true)
[[ -n "$VM_IP" ]] && break
echo -n "."
done
echo
if [[ -n "$VM_IP" ]]; then
ok "VM IP address: $VM_IP"
else
warn "Could not detect VM IP yet — it may still be booting."
warn "Run: virsh domifaddr $VM_NAME to check later."
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " VM '$VM_NAME' is booting into the Arch Linux live environment."
echo
echo " Open the console in virt-manager, then run:"
echo
echo " 1. On your host — serve the config (if not already running):"
echo " cd ~/dev/homelab && python3 -m http.server 8080"
echo
echo " 2. In the VM console — one-liner to fetch config and start install:"
echo " curl http://192.168.122.1:8080/scripts/vm-install.sh | bash"
echo
echo " 3. After install completes, select Reboot."
echo " SSH will be available on the installed system:"
if [[ -n "$VM_IP" ]]; then
echo " ssh -i ~/dev/homelab/keys/homelab_ed25519 adyrem@$VM_IP"
fi
echo
echo " Arch ISO: $ARCH_ISO"
echo " Disk image: $VM_DISK"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Run this inside the Arch live environment to fetch the config and start archinstall.
# Usage: curl http://192.168.122.1:8080/scripts/vm-install.sh | bash
set -euo pipefail
HOST="http://192.168.122.1:8080"
echo "==> Fetching archinstall config..."
curl -fsSL "$HOST/archinstall-config.json" -o /tmp/config.json
echo "==> Starting archinstall..."
archinstall --config /tmp/config.json
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
VM_NAME="arch-base"
virsh --connect qemu:///system change-media "$VM_NAME" sda --eject 2>/dev/null && echo "ISO ejected" || echo "No ISO to eject"
virsh --connect qemu:///system reboot "$VM_NAME"
echo "Rebooting $VM_NAME..."