Proxmox Provider
Setup
PROXMOX_URL=https://pve.example.com:8006
PROXMOX_USER=root@pam
PROXMOX_TOKEN_NAME=puls
PROXMOX_TOKEN_SECRET=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PROXMOX_NODES=pve1,pve2
PROXMOX_DNS_DOMAIN=nolimit.int
PROXMOX_DNS_SERVERS=10.8.10.11,10.8.10.12,10.8.10.13
Auth uses PVE API tokens (USER@REALM!TOKENNAME=SECRET). Create one in Datacenter → Permissions → API Tokens.
Define environments once in src/types/proxmox.ts:
export const CONFIG = {
STAGING: {
url: process.env.PROXMOX_URL!,
user: process.env.PROXMOX_USER!,
tokenName: process.env.PROXMOX_TOKEN_NAME!,
tokenSecret: process.env.PROXMOX_TOKEN_SECRET!,
nodes: process.env.PROXMOX_NODES?.split(','),
dnsDomain: process.env.PROXMOX_DNS_DOMAIN,
dnsServers: process.env.PROXMOX_DNS_SERVERS?.split(','),
verifySsl: false,
},
};
Reference everywhere via @Deploy({ proxmox: CONFIG.STAGING }).
VM
Proxmox.VM("ix-sto1-app01")
.image(OS.UBUNTU_24_04) // template VMID or name substring
.cores(4)
.memory(8192) // MB
.ip("10.8.10.83") // static IP - omit for DHCP or DNS auto-resolve
.vlan(2010) // VLAN tag on vmbr1
.sshKey(KEYS) // array of public key strings
.provision("config/default.yaml") // single script or playbook
.provision(["common.sh", "app.yml"]) // or multiple files run in order
.replace("ix-sto1-app01-old") // destroy old VM after new one is up
Templates (OS constants)
Templates are matched by VMID (numeric string) or name substring:
export const OS = {
UBUNTU_22_04: "ubuntu-22.04", // name substring match
UBUNTU_24_04: "9017", // VMID match - use when name is ambiguous
DEBIAN_11: "debian-11",
DEBIAN_12: "debian-12",
} as const;
SSH keys (KEYS constant)
Pass via .sshKey(KEYS) - injected into cloud-init so every VM has your team's keys from first boot.
IP resolution
Static IP via .ip("10.8.10.83") - sets cloud-init ipconfig0 with gateway derived from the first three octets (10.8.10.1).
If no .ip() is set, Puls tries DNS first:
Configure dnsDomain in CONFIG to enable this.
Provisioning pipeline
After a VM starts, Puls runs these steps for each provisioner provided to .provision():
- TCP probe (first provisioner only) - polls port 22 until SSH is accepting connections
- cloud-init wait (first provisioner only) - runs
cloud-init statusvia SSH until it returnsdoneorerror - Provisioner - dispatched by file extension
| Extension | Action |
|---|---|
.yaml / .yml |
ansible-playbook -i "IP," -u root --private-key ... |
.sh |
scp -r scriptDir/ root@IP:/ then ssh 'bash -s' < script |
.pp |
scp manifest.pp /tmp/ then ssh 'puppet apply /tmp/manifest.pp' |
For shell scripts, the entire directory containing the script is scp'd first - companion files (GPG keys, configs) are available on the remote at the same relative path.
Ansible example (config/default.yaml)
---
- name: Install Puppet 8
hosts: all
become: true
vars:
puppet_server: ix-sto1-puppet01
tasks:
- name: Copy Puppet Labs keyring
ansible.builtin.copy:
src: puppetlabs-keyring.gpg
dest: /etc/apt/keyrings/puppetlabs-keyring.gpg
mode: "0644"
- name: Add Puppet 8 apt repository
ansible.builtin.apt_repository:
repo: "deb [signed-by=/etc/apt/keyrings/puppetlabs-keyring.gpg] http://apt.puppet.com noble puppet8"
state: present
filename: puppetlabs
- name: Install Puppet
ansible.builtin.apt:
name: puppet
state: present
update_cache: true
- name: Configure puppet.conf
ansible.builtin.copy:
dest: /etc/puppetlabs/puppet/puppet.conf
mode: "0644"
content: |
[main]
certname = {{ ansible_facts['hostname'] }}
environment = production
server = {{ puppet_server }}
Destroy
// Tear down the whole stack
@Destroy({ proxmox: CONFIG.STAGING })
class StagingInfra extends Stack { ... }
// Destroy one resource during a deploy (replace pattern)
@Deploy({ proxmox: CONFIG.STAGING })
class StagingInfra extends Stack {
new = Proxmox.VM("ix-sto1-app02")...;
@Destroy
old = Proxmox.VM("ix-sto1-app01");
}
Or use .replace() for an atomic swap - new VM is fully provisioned before the old one is removed:
Destroy stops the VM gracefully, then deletes it with purge=1&destroy-unreferenced-disks=1.
Provisioning is skipped during destroy - only discovery and deletion run.
Full example
import { Proxmox, PROXMOX_TYPES, Stack, Deploy, Destroy, Protected } from "puls-dev";
const { CONFIG, OS, KEYS } = PROXMOX_TYPES;
@Deploy({ proxmox: CONFIG.STAGING })
class StagingInfra extends Stack {
@Protected
db = Proxmox.VM("ix-sto1-db01")
.image(OS.UBUNTU_24_04)
.cores(2).memory(4096)
.ip("10.8.10.50").vlan(2010)
.sshKey(KEYS);
app = Proxmox.VM("ix-sto1-app01")
.image(OS.UBUNTU_24_04)
.cores(4).memory(8192)
.ip("10.8.10.51").vlan(2010)
.sshKey(KEYS)
.provision("config/default.yaml");
}