--- - 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 helm_key_fingerprint="DDF78C3E6EBB2D2CC223C95C62BA89D07698DBC6" tmp_key="$(mktemp)" trap 'rm -f "${tmp_key}"' EXIT curl -fsSL https://packages.buildkite.com/helm-linux/helm-debian/gpgkey >"${tmp_key}" actual_fingerprint="$(gpg --show-keys --with-colons "${tmp_key}" | awk -F: '$1 == "fpr" { print $10; exit }')" if [ "${actual_fingerprint}" != "${helm_key_fingerprint}" ]; then echo "Unexpected Helm apt key fingerprint: ${actual_fingerprint}" >&2 exit 1 fi gpg --dearmor <"${tmp_key}" >/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://packages.buildkite.com/helm-linux/helm-debian/any/ any 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: Ensure pipx binary path is configured for the homelab user ansible.builtin.command: cmd: python3 -m pipx ensurepath become: true become_user: "{{ debian_pc_user }}" environment: PATH: "{{ debian_pc_user_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin" changed_when: false - name: Install user pipx CLI tools ansible.builtin.command: argv: - python3 - -m - pipx - install - "{{ item.package }}" creates: "{{ debian_pc_user_home }}/.local/bin/{{ item.app }}" become: true become_user: "{{ debian_pc_user }}" environment: PATH: "{{ debian_pc_user_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin" loop: "{{ debian_pc_pipx_packages }}" - 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: Check whether Ollama is already installed ansible.builtin.shell: cmd: command -v ollama >/dev/null 2>&1 register: debian_pc_ollama_installed changed_when: false failed_when: false when: debian_pc_install_ollama | bool - name: Install Ollama from local Linux tarball when provided ansible.builtin.shell: | set -euo pipefail tar -C /usr -xzf "{{ debian_pc_ollama_tarball_file }}" if ! id ollama >/dev/null 2>&1; then useradd -r -s /bin/false -U -m -d /usr/share/ollama ollama fi if getent group render >/dev/null 2>&1; then usermod -aG render ollama fi if getent group video >/dev/null 2>&1; then usermod -aG video ollama fi cat >/etc/systemd/system/ollama.service <<'EOF' [Unit] Description=Ollama Service After=network-online.target [Service] ExecStart=/usr/bin/ollama serve User=ollama Group=ollama Restart=always RestartSec=3 Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" [Install] WantedBy=default.target EOF args: executable: /bin/bash when: - debian_pc_install_ollama | bool - debian_pc_ollama_installed.rc | default(1) != 0 - debian_pc_ollama_tarball_file | length > 0 - name: Install Ollama from official Linux installer ansible.builtin.shell: | set -euo pipefail tmp_installer="$(mktemp)" trap 'rm -f "${tmp_installer}"' EXIT curl -fsSL \ {% if debian_pc_ollama_install_curl_ip_version | length > 0 %} "{{ debian_pc_ollama_install_curl_ip_version }}" \ {% endif %} --connect-timeout "{{ debian_pc_ollama_install_connect_timeout }}" \ --max-time "{{ debian_pc_ollama_install_max_time }}" \ "{{ debian_pc_ollama_install_url }}" \ -o "${tmp_installer}" sh "${tmp_installer}" args: executable: /bin/bash when: - debian_pc_install_ollama | bool - debian_pc_ollama_installed.rc | default(1) != 0 - debian_pc_ollama_tarball_file | length == 0 - name: Ensure Ollama model storage exists on data disk ansible.builtin.file: path: "{{ debian_pc_ollama_models_dir }}" state: directory owner: ollama group: ollama mode: "0775" when: debian_pc_install_ollama | bool - name: Ensure Ollama systemd override directory exists ansible.builtin.file: path: /etc/systemd/system/ollama.service.d state: directory mode: "0755" when: debian_pc_install_ollama | bool - name: Configure Ollama for homelab LAN access and data storage ansible.builtin.copy: dest: /etc/systemd/system/ollama.service.d/homelab.conf mode: "0644" content: | [Service] Environment="OLLAMA_HOST={{ debian_pc_ollama_host }}" Environment="OLLAMA_MODELS={{ debian_pc_ollama_models_dir }}" register: debian_pc_ollama_override when: debian_pc_install_ollama | bool - name: Enable expected services when installed ansible.builtin.systemd: name: "{{ item }}" enabled: true state: started loop: "{{ debian_pc_enable_services }}" failed_when: false - name: Enable and start Ollama ansible.builtin.systemd: name: ollama enabled: true state: "{{ 'restarted' if debian_pc_ollama_override.changed | default(false) else 'started' }}" daemon_reload: true when: debian_pc_install_ollama | bool - name: Wait for Ollama API ansible.builtin.uri: url: http://127.0.0.1:11434/api/tags method: GET status_code: 200 register: debian_pc_ollama_api retries: 12 delay: 5 until: debian_pc_ollama_api.status == 200 when: debian_pc_install_ollama | bool - name: Pull default Ollama model for backstage homelab diagnostics ansible.builtin.shell: | set -euo pipefail if ollama list | awk -v model="{{ debian_pc_ollama_default_model }}" 'NR > 1 && ($1 == model || $1 == model ":latest") { found = 1 } END { exit found ? 0 : 1 }'; then echo present exit 0 fi ollama pull "{{ debian_pc_ollama_default_model }}" echo pulled args: executable: /bin/bash register: debian_pc_ollama_pull_result changed_when: "'pulled' in debian_pc_ollama_pull_result.stdout" environment: OLLAMA_HOST: http://127.0.0.1:11434 when: debian_pc_install_ollama | bool - 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