One-Click Pain-Free! I Turned My PVE Cluster into an ‘Auto-Scaling’ Toy Box with Ansible

One-Click Pain-Free! I Turned My PVE Cluster into an ‘Auto-Scaling’ Toy Box with Ansible

Tags: Proxmox | Ansible | Home Lab | Debian/Ubuntu | Domestic Acceleration

01 Showcasing the Results

  • • At 23:18, I casually clicked “Clone” in the PVE Web interface, and 30 seconds later, a new LXC container was online;
  • • At 23:19, Ansible automatically discovered the new node and pushed Tsinghua source + Docker image acceleration;
  • • At 23:20, I returned with my coffee,<span>apt update</span> had already completed, all without manual SSH.
  • [That’s right, I went crazy with PVE, drinking coffee in the middle of the night]

In a nutshell: Even a ‘weak password’ like “root / admin12345” can be used creatively in a family cluster, as long as the automation is smooth enough.

PS: Use root and admin12345 only in a local area network, as well as in campus networks and labs.

02 What Does My Home Lab Look Like?

Node Role System IP
pve1 Main Node PVE 8.2 192.168.8.21
pve2 Secondary PVE 8.2 192.168.8.22
All LXC/VM Debian12 / Ubuntu 22.04 Unified root / admin12345 DHCP Pool

Note: The weak password is merely a trade-off for internal network isolation + snapshot rollback. Please use keys + Vault in production environments!

03 Approach: Let Ansible ‘Sniff’ Out New Boxes in the Cluster

  1. 1. Use the Proxmoxer module to dynamically pull the list of all VMs/LXCs in the cluster;
  2. 2. Use nmap to scan 192.168.8.0/24, filtering for devices with port 22 open that return “SSH-2.0-OpenSSH”;
  3. 3. Compare the two lists → Identify new machines that have “SSH but are not in Ansible inventory”;
  4. 4. Automatically generate host_vars and write <span>ansible_user=root ansible_password=admin12345</span>;
  5. 5. Execute the playbook: change sources → install basic packages → restart containers/VMs.

04 Core Code (Feel Free to Copy)

1. Dynamic Inventory Script <span>pve_dynamic.py</span>

#!/usr/bin/env python3
import proxmoxer, os, json
api = proxmoxer.ProxmoxAPI('192.168.8.21', user='root@pam', password='your_PVE_password', verify_ssl=False)
inventory = {'all': {'hosts': []}, '_meta': {'hostvars': {}}}
for node in api.nodes.get():
    for vm in api.nodes(node['node']).qemu.get():
        ip = api.nodes(node['node']).qemu(vm['vmid']).agent('network-get-interfaces').get()['result'][1]['ip-addresses'][0]['ip-address']
        inventory['all']['hosts'].append(ip)
        inventory['_meta']['hostvars'][ip] = {'ansible_user': 'root', 'ansible_password': 'admin12345'}
print(json.dumps(inventory))

Grant execution permissions and place it in <span>/etc/ansible/inventory/pve_dynamic.py</span>, add to ansible.cfg:

[inventory]
enable_plugins = script

2. One-Click Source Change Playbook <span>site.yml</span>

- hosts: all
  gather_facts: yes
  tasks:
    - name: Backup original sources
      shell: cp /etc/apt/sources.list /etc/apt/sources.list.bak

    - name: Tsinghua Source for Debian12
      copy:
        dest: /etc/apt/sources.list
        content: |
          deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware
          deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware
          deb https://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
      when: ansible_distribution == "Debian" and ansible_distribution_major_version == "12"

    - name: Tsinghua Source for Ubuntu22.04
      copy:
        dest: /etc/apt/sources.list
        content: |
          deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
          deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
          deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-security main restricted universe multiverse
      when: ansible_distribution == "Ubuntu" and ansible_distribution_version == "22.04"

    - name: Install common packages
      apt:
        name: ['curl', 'htop', 'vim', 'docker.io']
        update_cache: yes
        state: present

    - name: Docker image acceleration
      copy:
        dest: /etc/docker/daemon.json
        content: |
          {"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"]}
      notify: restart docker

  handlers:
    - name: restart docker
      service: name=docker state=restarted

Execute:

ansible-playbook -i /etc/ansible/inventory/pve_dynamic.py site.yml

05 Pitfall Tips

  1. 1. LXC Nested Virtualization If you want to run Docker inside the container, remember to check the “Nesting” option in PVE.
  2. 2. qemu-guest-agent Without the agent, you won’t get the IP, and the dynamic inventory will miss it; install <span>qemu-guest-agent</span> in the template and enable the service in advance.
  3. 3. Sudo Prompt Some Ubuntu images force a password change on first login, which can cause Ansible to hang; execute <span>chage -d 0 root</span> in the template to bypass this.
  4. 4. Password Cracking After writing the playbook, move the <span>ansible_password</span> to <span>host_vars/</span> and add it to <span>.gitignore</span>, do not push it to GitHub!

06 What Else Can Be Done?

  • • Split the playbook into roles to support CentOS / Alpine across multiple systems;
  • • Use Terraform to call the PVE Provider, achieving “Infrastructure as Code”;
  • • Integrate Prometheus+Grafana, automatically registering exporters for new nodes;
  • • Push Ansible into GitLab CI, triggering scaling → testing → snapshot rollback on merge requests.

07 Conclusion

The biggest enemy of a home lab is not performance, but repetitive labor. When you turn “adding nodes” into an API call and write “setting up the environment” as a playbook, the entire cluster truly becomes malleable like clay.

I hope today’s script can save you one SSH session and earn you another cup of coffee.

Leave a Comment