--- # playbooks/onboarding/proxmox_onboarding.yml # Complete Proxmox host onboarding for 12th Gen Intel laptops # # What this does: # 1. Creates the operational user (lab_ansible_user) with SSH key auth and passwordless sudo # 2. Configures Proxmox repos (removes enterprise, adds no-subscription) # 3. Applies 12th Gen Intel hybrid core optimizations (intel_pstate=passive, pcie_aspm=force) # 4. Hardens laptop for headless operation (disables lid-close suspend) # 5. Disables swap to protect NVMe lifespan # 6. Removes Proxmox subscription nag from web/mobile UI # 7. Optionally disables HA/Corosync for standalone-only nodes # 8. Runs dist-upgrade to ensure all packages current # # Prerequisites: # - SSH access to Proxmox host as root (with SSH keys) # - Host defined in inventory under [proxmox_cluster] # - SSH public key at ~/.ssh/id_ed25519_homelab.pub on control machine # # Usage: # # All hosts in proxmox_cluster: # ansible-playbook -i inventory/hosts.ini playbooks/onboarding/proxmox_onboarding.yml # # # Single host: # ansible-playbook -i inventory/hosts.ini playbooks/onboarding/proxmox_onboarding.yml --limit pve01 # # # New host without SSH keys (first run only): # ansible-playbook -i inventory/hosts.ini playbooks/onboarding/proxmox_onboarding.yml \ # --limit pve01 -e "ansible_user=root" --ask-pass # # # Standalone prep mode (default): # ansible-playbook -i inventory/hosts.ini playbooks/onboarding/proxmox_onboarding.yml --limit pve01 # # # Cluster-intent prep (skip disabling HA/Corosync): # ansible-playbook -i inventory/hosts.ini playbooks/onboarding/proxmox_onboarding.yml --limit pve01 -e standalone_mode=false # # ───────────────────────────────────────────────────────────────────────────── # This playbook is for DAY-0 ONBOARDING ONLY (first-time provisioning). # It runs dist-upgrade and may reboot. Do not use for routine enforcement. # # For ongoing drift enforcement: playbooks/proxmox/pve_baseline.yml # For rolling package updates: playbooks/proxmox/pve_update.yml # For cross-node consistency audit: playbooks/proxmox/pve_audit.yml # ───────────────────────────────────────────────────────────────────────────── # # After first run, update inventory to use lab_ansible_user (see group_vars/all.yml): # [proxmox_cluster:vars] # ansible_user= # ansible_become=true - name: Proxmox host onboarding and laptop hardening hosts: proxmox_cluster become: true vars: is_laptop: true # standalone_mode controls whether pve-ha-lrm, pve-ha-crm, and corosync are stopped. # DEFAULT IS FALSE — pve01/pve02/pve03 always run as a 3-node cluster. # Only set true with -e standalone_mode=true for a truly isolated single-node PVE install. standalone_mode: false # Operational user to create. References group_vars/all.yml; override with -e lab_ansible_user=otheruser. lab_user: "{{ lab_ansible_user | default('chester') }}" controller_ssh_pubkey_candidates: - "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519_homelab.pub" - "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519.pub" tasks: - name: "0. Identity Management: Create User '{{ lab_user }}'" block: - name: "Install sudo package" ansible.builtin.apt: name: sudo state: present update_cache: false - name: "Ensure group '{{ lab_user }}' exists" ansible.builtin.group: name: "{{ lab_user }}" state: present - name: "Create user '{{ lab_user }}' with sudo access" ansible.builtin.user: name: "{{ lab_user }}" group: "{{ lab_user }}" groups: sudo shell: /bin/bash password: '!' password_lock: true - name: "Locate SSH public key on control machine" ansible.builtin.set_fact: controller_ssh_pubkey_path: >- {{ lookup('ansible.builtin.first_found', {'files': controller_ssh_pubkey_candidates, 'skip': true}) }} delegate_to: localhost become: false - name: "Fail early if SSH public key is missing" ansible.builtin.fail: msg: >- SSH public key not found on the control machine. Checked: {{ controller_ssh_pubkey_candidates | join(', ') }} Generate one with: ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 when: controller_ssh_pubkey_path | default('') | length == 0 - name: "Deploy SSH Key to {{ lab_user }} user" ansible.posix.authorized_key: user: "{{ lab_user }}" state: present key: "{{ lookup('file', controller_ssh_pubkey_path) }}" - name: "Allow '{{ lab_user }}' to use sudo without password" ansible.builtin.copy: dest: "/etc/sudoers.d/{{ lab_user }}" content: "{{ lab_user }} ALL=(ALL) NOPASSWD: ALL\n" mode: '0440' owner: root group: root validate: '/usr/sbin/visudo -cf %s' - name: "1. Repository & Package Optimization" block: - name: "Check if /etc/apt/sources.list exists" ansible.builtin.stat: path: /etc/apt/sources.list register: apt_sources_list_stat - name: "Remove Proxmox enterprise repo files (.list/.sources)" ansible.builtin.file: path: "{{ item }}" state: absent loop: - /etc/apt/sources.list.d/pve-enterprise.list - /etc/apt/sources.list.d/pve-enterprise.sources - /etc/apt/sources.list.d/ceph.list - /etc/apt/sources.list.d/ceph.sources - /etc/apt/sources.list.d/ceph-enterprise.list - /etc/apt/sources.list.d/ceph-enterprise.sources - name: "Remove enterprise.proxmox.com entries from /etc/apt/sources.list" ansible.builtin.lineinfile: path: /etc/apt/sources.list regexp: '^.*enterprise\.proxmox\.com.*$' state: absent when: apt_sources_list_stat.stat.exists - name: "Add Proxmox no-subscription repository" ansible.builtin.apt_repository: repo: "deb http://download.proxmox.com/debian/pve {{ ansible_distribution_release }} pve-no-subscription" filename: pve-no-subscription state: present - name: "Add Proxmox Ceph no-subscription repository" ansible.builtin.apt_repository: repo: "deb http://download.proxmox.com/debian/ceph-squid {{ ansible_distribution_release }} no-subscription" filename: ceph-no-subscription state: present - name: "Ensure Latest Intel Microcode (Required for Hybrid Cores)" ansible.builtin.apt: name: [intel-microcode, htop, nvme-cli, lm-sensors] state: present update_cache: true - name: "Run apt dist-upgrade" ansible.builtin.apt: upgrade: dist update_cache: false register: dist_upgrade_result - name: "Reboot if kernel was updated" ansible.builtin.reboot: msg: "Rebooting after kernel upgrade — initiated by Ansible" reboot_timeout: 300 when: dist_upgrade_result is changed and dist_upgrade_result.stdout is search('linux-image') - name: "2. Kernel Tuning (12th Gen & Power)" block: - name: "Configure GRUB for ASPM & Power Savings" ansible.builtin.lineinfile: path: /etc/default/grub regexp: '^GRUB_CMDLINE_LINUX_DEFAULT=' line: 'GRUB_CMDLINE_LINUX_DEFAULT="quiet pcie_aspm=force intel_pstate=passive"' notify: Update Grub - name: "3. Laptop Safety: Disable Lid-Close Suspend" when: is_laptop | default(false) block: - name: "Configure logind.conf to ignore lid switch" ansible.builtin.lineinfile: path: /etc/systemd/logind.conf regexp: "^#?{{ item.key }}=" line: "{{ item.key }}={{ item.value }}" loop: - { key: "HandleLidSwitch", value: "ignore" } - { key: "HandleLidSwitchExternalPower", value: "ignore" } notify: Restart Logind - name: "Mask Sleep/Suspend Targets (Hardware Lock)" ansible.builtin.systemd: name: "{{ item }}" masked: true loop: - sleep.target - suspend.target - hibernate.target - hybrid-sleep.target - name: "4. Storage & SSD Health" block: - name: "Disable Swap (Protect NVMe Lifespan)" ansible.builtin.shell: | swapoff -a sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab when: ansible_swaptotal_mb > 0 changed_when: ansible_swaptotal_mb > 0 - name: "5. Intel Thread Director Support Check" ansible.builtin.shell: "dmesg | grep -i 'Hardware Feedback Interface'" register: hfi_check failed_when: false changed_when: false - name: "6. Proxmox Web UI: Remove Subscription Nag" block: - name: "Deploy subscription nag removal script" ansible.builtin.copy: dest: /usr/local/bin/pve-remove-nag.sh owner: root group: root mode: '0755' content: | #!/bin/sh WEB_JS=/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js if [ -s "$WEB_JS" ] && ! grep -q NoMoreNagging "$WEB_JS"; then echo "Patching Web UI nag..." sed -i -e "/data\.status/ s/!//" -e "/data\.status/ s/active/NoMoreNagging/" "$WEB_JS" fi MOBILE_TPL=/usr/share/pve-yew-mobile-gui/index.html.tpl MARKER="" if [ -f "$MOBILE_TPL" ] && ! grep -q "$MARKER" "$MOBILE_TPL"; then echo "Patching Mobile UI nag..." printf "%s\n" \ "$MARKER" \ "" \ "" >>"$MOBILE_TPL" fi - name: "Configure dpkg hook to auto-run nag removal after upgrades" ansible.builtin.copy: dest: /etc/apt/apt.conf.d/no-nag-script owner: root group: root mode: '0644' content: | DPkg::Post-Invoke { "/usr/local/bin/pve-remove-nag.sh"; }; - name: "Run nag removal script immediately" ansible.builtin.command: /usr/local/bin/pve-remove-nag.sh register: nag_removal_output changed_when: "'Patching' in nag_removal_output.stdout" - name: "Reinstall proxmox-widget-toolkit to ensure nag patches apply" ansible.builtin.apt: name: proxmox-widget-toolkit state: present register: widget_reinstall failed_when: false - name: "7. Standalone Optimization: Disable HA/Corosync Services" when: standalone_mode | bool block: - name: "Stop and disable pve-ha-lrm service" ansible.builtin.systemd: name: pve-ha-lrm state: stopped enabled: false failed_when: false - name: "Stop and disable pve-ha-crm service" ansible.builtin.systemd: name: pve-ha-crm state: stopped enabled: false failed_when: false - name: "Stop and disable Corosync service" ansible.builtin.systemd: name: corosync state: stopped enabled: false failed_when: false handlers: - name: Update Grub ansible.builtin.command: update-grub register: grub_update_result changed_when: grub_update_result.rc == 0 - name: Restart Logind ansible.builtin.systemd: name: systemd-logind state: restarted