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. Use the Proxmoxer module to dynamically pull the list of all VMs/LXCs in the cluster;
- 2. Use nmap to scan 192.168.8.0/24, filtering for devices with port 22 open that return “SSH-2.0-OpenSSH”;
- 3. Compare the two lists → Identify new machines that have “SSH but are not in Ansible inventory”;
- 4. Automatically generate host_vars and write
<span>ansible_user=root ansible_password=admin12345</span>; - 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. LXC Nested Virtualization If you want to run Docker inside the container, remember to check the “Nesting” option in PVE.
- 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. 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. 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.