332 lines
14 KiB
YAML
332 lines
14 KiB
YAML
---
|
|
# 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=<value of lab_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="<!-- MANAGED BLOCK FOR MOBILE NAG -->"
|
|
if [ -f "$MOBILE_TPL" ] && ! grep -q "$MARKER" "$MOBILE_TPL"; then
|
|
echo "Patching Mobile UI nag..."
|
|
printf "%s\n" \
|
|
"$MARKER" \
|
|
"<script>" \
|
|
" function removeSubscriptionElements() {" \
|
|
" const dialogs = document.querySelectorAll('dialog.pwt-outer-dialog');" \
|
|
" dialogs.forEach(dialog => {" \
|
|
" const text = (dialog.textContent || '').toLowerCase();" \
|
|
" if (text.includes('subscription')) {" \
|
|
" dialog.remove();" \
|
|
" console.log('Removed subscription dialog');" \
|
|
" }" \
|
|
" });" \
|
|
" const cards = document.querySelectorAll('.pwt-card.pwt-p-2.pwt-d-flex.pwt-interactive.pwt-justify-content-center');" \
|
|
" cards.forEach(card => {" \
|
|
" const text = (card.textContent || '').toLowerCase();" \
|
|
" const hasButton = card.querySelector('button');" \
|
|
" if (!hasButton && text.includes('subscription')) {" \
|
|
" card.remove();" \
|
|
" console.log('Removed subscription card');" \
|
|
" }" \
|
|
" });" \
|
|
" }" \
|
|
" const observer = new MutationObserver(removeSubscriptionElements);" \
|
|
" observer.observe(document.body, { childList: true, subtree: true });" \
|
|
" removeSubscriptionElements();" \
|
|
" setInterval(removeSubscriptionElements, 300);" \
|
|
" setTimeout(() => {observer.disconnect();}, 10000);" \
|
|
"</script>" \
|
|
"" >>"$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
|