diff --git a/.gitignore b/.gitignore index d26c15f..5af1ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,11 @@ *.tfstate.backup .terraform/ .lab/ +bootstrap/host/inventory/ # Ignore local archive dumps and backups *.tar +*.tar.gz *.zip infra/gitea/data/ diff --git a/README.md b/README.md index 621b324..920ebf7 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,21 @@ On the Debian host: - SSH access to the OCI edge host - enough persistent storage for `/var/openebs/local` and `/var/lib/docker` +After a clean Debian reinstall, bootstrap the host package set and desktop +configuration first: + +```bash +sudo apt-get update +sudo apt-get install -y git ansible sudo +git clone /home/jv/my-homelab-configs +cd /home/jv/my-homelab-configs +ansible-playbook -K -i bootstrap/host/inventory.ini bootstrap/host/playbook.yml +``` + +If you kept the private app-config archive generated from the old Debian host, +pass its generated `ansible-vars.yml` with `-e @/path/to/ansible-vars.yml`. +Details are in `bootstrap/host/README.md`. + The default kubeconfig path is `/home/jv/.kube/config`. Override it with `KUBECONFIG_PATH` or `TF_VAR_kubeconfig_path` when needed. diff --git a/bootstrap/host/README.md b/bootstrap/host/README.md new file mode 100644 index 0000000..7142d9e --- /dev/null +++ b/bootstrap/host/README.md @@ -0,0 +1,40 @@ +# Debian Host Bootstrap + +This layer restores the Debian control-plane and personal desktop package set +before the OpenTofu/Kubernetes bootstrap runs. + +Fresh Debian recovery path: + +```bash +sudo apt-get update +sudo apt-get install -y git ansible sudo +sudo usermod -aG sudo jv +git clone /home/jv/my-homelab-configs +cd /home/jv/my-homelab-configs +ansible-playbook -K -i bootstrap/host/inventory.ini bootstrap/host/playbook.yml +./lab.sh up +``` + +To also restore captured application behavior/configuration, keep the generated +`home-configs.tar.gz` outside Git and pass the generated vars file: + +```bash +ansible-playbook -K -i bootstrap/host/inventory.ini bootstrap/host/playbook.yml \ + -e @/path/to/debian-pc-ansible-inventory/ansible-vars.yml +``` + +The playbook: + +- configures apt repositories for Docker, Kubernetes, Helm, OpenTofu, and Brave +- installs the captured Debian package set, filtering packages unavailable on + the current Debian release +- installs Zoom from the upstream `.deb` when requested on amd64 +- adds `jv` to expected local groups +- enables core services such as Docker, containerd, kubelet, ssh, cron, and + Bluetooth when installed +- creates homelab persistence directories used by `lab.sh up` +- optionally extracts the captured user config archive into `/home/jv` + +Secrets and active sessions are intentionally not stored in this repo. Keep +SSH keys, kubeconfigs, SOPS keys, Docker auth, and browser session/password data +in a separate encrypted backup if you want them restored. diff --git a/bootstrap/host/group_vars/debian_pc.yml b/bootstrap/host/group_vars/debian_pc.yml new file mode 100644 index 0000000..edf50f0 --- /dev/null +++ b/bootstrap/host/group_vars/debian_pc.yml @@ -0,0 +1,253 @@ +--- +debian_pc_user: jv +debian_pc_user_home: "/home/{{ debian_pc_user }}" +debian_pc_kubernetes_minor_version: v1.36 +debian_pc_enable_passwordless_sudo: true + +debian_pc_persistent_directories: + - /var/openebs/local/registry + - /var/lib/docker + - "/home/{{ debian_pc_user }}/git-server" + - "/home/{{ debian_pc_user }}/backups/gitea" + - "/home/{{ debian_pc_user }}/backups/gitea-restore-drills" + +debian_pc_user_groups: + - sudo + - docker + - audio + - bluetooth + - video + - plugdev + +debian_pc_enable_services: + - ssh + - cron + - docker + - containerd + - kubelet + - bluetooth + +debian_pc_hold_packages: + - containerd.io + - kubeadm + - kubectl + - kubelet + +debian_pc_base_packages: + - apt-transport-https + - bash-completion + - ca-certificates + - curl + - git + - gnupg + - gpg + - python3 + - python3-apt + - rsync + - sudo + - tar + - unzip + - wget + - zip + +# Captured from the Debian PC manual package inventory. The playbook filters +# packages that are unavailable for the current Debian release after adding the +# third-party repositories, so this list can remain faithful to the source host. +debian_pc_apt_manual_packages: + - adduser + - amd64-microcode + - ansible + - apt + - apt-listchanges + - apt-transport-https + - apt-utils + - aria2 + - base-files + - base-passwd + - bash + - bash-completion + - bind9-dnsutils + - bind9-host + - blueman + - bluez + - bluez-tools + - brave-browser + - bsdutils + - busybox + - bzip2 + - ca-certificates + - chafa + - chromium + - conky-all + - console-setup + - containerd.io + - containernetworking-plugins + - coreutils + - cpio + - cron + - cron-daemon-common + - curl + - dash + - dbus + - debconf + - debconf-i18n + - debian-archive-keyring + - debian-faq + - debianutils + - dhcpcd-base + - diffutils + - direnv + - dmidecode + - dnsmasq + - doc-debian + - docker-buildx-plugin + - docker-ce + - docker-ce-cli + - docker-ce-rootless-extras + - docker-compose-plugin + - docker-model-plugin + - dosfstools + - dpkg + - e2fsprogs + - fdisk + - file + - findutils + - firmware-amd-graphics + - firmware-realtek + - firmware-sof-signed + - gcc-14-base + - geany + - geany-plugins + - gettext-base + - git + - gnumeric + - gnupg + - gparted + - gpg + - grep + - groff-base + - grub-common + - grub-efi-amd64 + - gzip + - haveged + - helm + - hostname + - i3 + - ifupdown + - imagemagick + - inetutils-telnet + - init + - initramfs-tools + - init-system-helpers + - installation-report + - iproute2 + - iputils-ping + - keyboard-configuration + - kitty + - kmod + - krb5-locales + - kubeadm + - kubectl + - kubelet + - laptop-detect + - less + - linux-image-amd64 + - linux-sysctl-defaults + - locales + - login + - login.defs + - logrotate + - logsave + - lsof + - lvm2 + - man-db + - manpages + - mawk + - mcomix + - media-types + - mount + - mplayer + - nano + - ncurses-base + - ncurses-bin + - ncurses-term + - netbase + - netcat-traditional + - nfs-common + - nftables + - nginx + - nodejs + - openbox + - open-iscsi + - openssh-client + - openssl-provider-legacy + - os-prober + - parted + - passwd + - pavucontrol + - pciutils + - perl + - perl-base + - pipx + - procps + - psmisc + - pulseaudio-module-bluetooth + - qcomicbook + - readline-common + - reportbug + - rocminfo + - rsync + - screen + - sed + - sensible-utils + - shellcheck + - shim-signed + - smplayer + - sqv + - sudo + - synapse + - systemd + - systemd-sysv + - systemd-timesyncd + - sysvinit-utils + - tar + - task-english + - task-laptop + - tasksel + - task-ssh-server + - tcpdump + - thefuck + - tilda + - tmux + - tofu + - traceroute + - transmission + - tree + - tzdata + - ucf + - udev + - unzip + - usbutils + - util-linux + - util-linux-extra + - vim + - vim-common + - vim-tiny + - virtualenvwrapper + - vlc + - wamerican + - wget + - whiptail + - wmctrl + - wtmpdb + - xlsx2csv + - xorg + - xz-utils + - zip + - zlib1g + - zoom + - zstd + +debian_pc_config_archive: "" +debian_pc_restore_configs: true + diff --git a/bootstrap/host/inventory.ini b/bootstrap/host/inventory.ini new file mode 100644 index 0000000..a59d625 --- /dev/null +++ b/bootstrap/host/inventory.ini @@ -0,0 +1,3 @@ +[debian_pc] +localhost ansible_connection=local + diff --git a/bootstrap/host/playbook.yml b/bootstrap/host/playbook.yml new file mode 100644 index 0000000..ff9b4c3 --- /dev/null +++ b/bootstrap/host/playbook.yml @@ -0,0 +1,242 @@ +--- +- name: Restore Debian homelab control-plane and desktop host + hosts: debian_pc + become: true + gather_facts: true + + pre_tasks: + - name: Require Debian + ansible.builtin.assert: + that: + - ansible_facts.os_family == "Debian" + - ansible_facts.distribution == "Debian" + fail_msg: "This host bootstrap is intentionally scoped to Debian." + + - name: Install apt prerequisites + ansible.builtin.apt: + name: "{{ debian_pc_base_packages }}" + state: present + update_cache: true + + - name: Ensure apt keyring directory exists + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + + tasks: + - name: Add Docker apt key + ansible.builtin.get_url: + url: https://download.docker.com/linux/debian/gpg + dest: /etc/apt/keyrings/docker.asc + mode: "0644" + + - name: Add Docker apt source + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/docker.sources + mode: "0644" + content: | + Types: deb + URIs: https://download.docker.com/linux/debian + Suites: {{ ansible_facts.distribution_release }} + Components: stable + Architectures: {{ ansible_facts.architecture | regex_replace('x86_64', 'amd64') | regex_replace('aarch64', 'arm64') }} + Signed-By: /etc/apt/keyrings/docker.asc + + - name: Add Kubernetes apt key + ansible.builtin.shell: | + set -euo pipefail + curl -fsSL "https://pkgs.k8s.io/core:/stable:/{{ debian_pc_kubernetes_minor_version }}/deb/Release.key" | + gpg --dearmor >/etc/apt/keyrings/kubernetes-apt-keyring.gpg + chmod 0644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg + args: + executable: /bin/bash + creates: /etc/apt/keyrings/kubernetes-apt-keyring.gpg + + - name: Add Kubernetes apt source + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/kubernetes.list + mode: "0644" + content: | + deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/{{ debian_pc_kubernetes_minor_version }}/deb/ / + + - name: Add Helm apt key + ansible.builtin.shell: | + set -euo pipefail + curl -fsSL https://baltocdn.com/helm/signing.asc | + gpg --dearmor >/etc/apt/keyrings/helm.gpg + chmod 0644 /etc/apt/keyrings/helm.gpg + args: + executable: /bin/bash + creates: /etc/apt/keyrings/helm.gpg + + - name: Add Helm apt source + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/helm-stable-debian.list + mode: "0644" + content: | + deb [arch={{ ansible_facts.architecture | regex_replace('x86_64', 'amd64') | regex_replace('aarch64', 'arm64') }} signed-by=/etc/apt/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main + + - name: Add OpenTofu apt keys + ansible.builtin.shell: | + set -euo pipefail + curl -fsSL https://get.opentofu.org/opentofu.gpg >/etc/apt/keyrings/opentofu.gpg + curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | + gpg --dearmor >/etc/apt/keyrings/opentofu-repo.gpg + chmod 0644 /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpg + args: + executable: /bin/bash + creates: /etc/apt/keyrings/opentofu-repo.gpg + + - name: Add OpenTofu apt source + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/opentofu.list + mode: "0644" + content: | + deb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main + + - name: Add Brave apt key + ansible.builtin.get_url: + url: https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg + dest: /etc/apt/keyrings/brave-browser-archive-keyring.gpg + mode: "0644" + + - name: Add Brave apt source + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/brave-browser-release.sources + mode: "0644" + content: | + Types: deb + URIs: https://brave-browser-apt-release.s3.brave.com/ + Suites: stable + Components: main + Architectures: amd64 arm64 + Signed-By: /etc/apt/keyrings/brave-browser-archive-keyring.gpg + + - name: Refresh apt cache after repository setup + ansible.builtin.apt: + update_cache: true + + - name: Write desired package list for availability filtering + ansible.builtin.copy: + dest: /tmp/homelab-debian-pc-desired-packages.txt + mode: "0644" + content: | + {% for package in (debian_pc_base_packages + debian_pc_apt_manual_packages) | unique | sort %} + {{ package }} + {% endfor %} + + - name: Filter packages available to apt + ansible.builtin.shell: | + set -euo pipefail + while IFS= read -r package; do + [ -n "$package" ] || continue + if apt-cache policy "$package" | awk '/Candidate:/ && $2 != "(none)" { found = 1 } END { exit found ? 0 : 1 }'; then + printf '%s\n' "$package" + fi + done 0 + + - name: Install Zoom from upstream deb when requested on amd64 + ansible.builtin.apt: + deb: https://zoom.us/client/latest/zoom_amd64.deb + state: present + when: + - "'zoom' in debian_pc_apt_manual_packages" + - ansible_facts.architecture == "x86_64" + + - name: Hold Kubernetes and runtime packages + ansible.builtin.dpkg_selections: + name: "{{ item }}" + selection: hold + loop: "{{ debian_pc_hold_packages }}" + failed_when: false + + - name: Ensure expected local groups exist + ansible.builtin.group: + name: "{{ item }}" + state: present + loop: "{{ debian_pc_user_groups }}" + + - name: Ensure user supplementary groups + ansible.builtin.user: + name: "{{ debian_pc_user }}" + groups: "{{ debian_pc_user_groups }}" + append: true + + - name: Allow passwordless sudo for homelab automation + ansible.builtin.copy: + dest: "/etc/sudoers.d/90-homelab-{{ debian_pc_user }}" + owner: root + group: root + mode: "0440" + validate: "visudo -cf %s" + content: | + {{ debian_pc_user }} ALL=(ALL) NOPASSWD:ALL + when: debian_pc_enable_passwordless_sudo | bool + + - name: Ensure persistent homelab directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ debian_pc_user if item.startswith(debian_pc_user_home) else 'root' }}" + group: "{{ debian_pc_user if item.startswith(debian_pc_user_home) else 'root' }}" + mode: "{{ '0755' if item.startswith(debian_pc_user_home) else '0775' }}" + loop: "{{ debian_pc_persistent_directories }}" + + - name: Enable expected services when installed + ansible.builtin.systemd: + name: "{{ item }}" + enabled: true + state: started + loop: "{{ debian_pc_enable_services }}" + failed_when: false + + - name: Check whether config archive is present + ansible.builtin.stat: + path: "{{ debian_pc_config_archive }}" + register: debian_pc_config_archive_stat + when: + - debian_pc_restore_configs | bool + - debian_pc_config_archive | length > 0 + + - name: Restore captured home application configs + ansible.builtin.unarchive: + src: "{{ debian_pc_config_archive }}" + dest: "{{ debian_pc_user_home }}" + remote_src: true + owner: "{{ debian_pc_user }}" + group: "{{ debian_pc_user }}" + when: + - debian_pc_restore_configs | bool + - debian_pc_config_archive | length > 0 + - debian_pc_config_archive_stat.stat.exists | default(false) + + - name: Report packages skipped because apt has no candidate + ansible.builtin.shell: | + set -euo pipefail + comm -23 \ + <(sort -u /tmp/homelab-debian-pc-desired-packages.txt) \ + <(printf '%s\n' {{ debian_pc_installable_packages | map('quote') | join(' ') }} | sort -u) + args: + executable: /bin/bash + register: debian_pc_skipped_packages_result + changed_when: false + + - name: Show skipped packages + ansible.builtin.debug: + msg: "{{ debian_pc_skipped_packages_result.stdout_lines }}" + when: debian_pc_skipped_packages_result.stdout_lines | length > 0