Skip to content

Repository files navigation

Proxmox VE SDK for PHP

🖧 Proxmox VE SDK for PHP

A modern, fully typed PHP SDK for the Proxmox VE API — QEMU, LXC, cluster, storage, backups.

PHP Version Symfony Tests Packagist License

QEMU · LXC · Cluster · Storage · Backups · Network · Access control · Task helpers

Installation · Quick Start · Tasks (UPID) · API Reference


$vmid = (int) $proxmox->cluster()->nextId()->data;
$task = $proxmox->qemu()->clone('pve1', 9000, $vmid, ['name' => 'web01', 'full' => 1]);
$proxmox->nodes()->waitForTask('pve1', $task->upid());
$proxmox->qemu()->start('pve1', $vmid);

Framework-agnostic core — usable from any PHP project, script, or worker — with an optional bundle for first-class Symfony integration. Authenticated with API tokens or ticket/cookie credentials, typed exceptions, async task helpers, and a comment-free, strictly typed codebase (PHP 8.2+, declare(strict_types=1) everywhere).


Table of Contents


Features

  • Wide API coverage: nodes, QEMU VMs, LXC containers, cluster, storage, backup jobs, vzdump, networking, users/tokens/ACLs.
  • Both auth modes: API tokens (recommended, stateless) and ticket/cookie login with automatic re-login on expiry and CSRF handling for write requests.
  • Task-aware: Proxmox returns a UPID for every asynchronous operation — the SDK detects them ($response->upid()) and can block until completion (waitForTask()), failing loudly when the task itself failed.
  • QEMU and LXC share one polished interface: identical lifecycle methods (start, stop, clone, migrate, snapshots, resize…) implemented once, specialized where the APIs differ.
  • Framework-agnostic: one plain facade (Proxmox) you can instantiate anywhere; the only hard dependency is symfony/http-client, a standalone component that works in any PHP project.
  • A single normalized response object (ApiResponse) that unwraps Proxmox's data envelope and flattens field-level validation errors.
  • Typed exception hierarchy under one marker interface, so you can catch narrowly or broadly.
  • Nothing is sealed off: the client's get/post/put/delete accept any path, so an endpoint not wrapped by a module is one call away.
  • Optional Symfony bundle with semantic configuration and autowirable services.
  • Fully unit-tested against MockHttpClient (no network required).

Requirements

Dependency Version
PHP >= 8.2
Proxmox VE 7.x or 8.x (API tokens require 6.2+)
Symfony 6.4 LTS or 7.x — optional, only for the bundle integration

Installation

The package is published on Packagist:

composer require chuckbartowski/proxmox-sdk

Quick Start (plain PHP)

No framework required — build the client and go:

use ChuckBartowski\ProxmoxSdk\Client\ProxmoxClient;
use ChuckBartowski\ProxmoxSdk\Proxmox;

$proxmox = new Proxmox(new ProxmoxClient(
    host: 'pve.example.com',
    tokenId: 'automation@pve!sdk',
    tokenSecret: getenv('PVE_TOKEN_SECRET'),
));

$vmid = (int) $proxmox->cluster()->nextId()->data;

$task = $proxmox->qemu()->clone('pve1', 9000, $vmid, ['name' => 'web01', 'full' => 1]);
$proxmox->nodes()->waitForTask('pve1', $task->upid());

$proxmox->qemu()->start('pve1', $vmid);

Client constructor signature:

new ProxmoxClient(
    string $host,
    string $tokenId = '',                     // 'user@realm!tokenname' (preferred)
    string $tokenSecret = '',
    string $username = '',                    // fallback: ticket auth
    string $password = '',
    string $realm = 'pam',                    // appended when $username has no '@'
    int $port = 8006,
    bool $verifySsl = true,
    float $timeout = 30.0,
    ?HttpClientInterface $httpClient = null,  // inject your own (retries, proxy, mock…)
);

Symfony Integration (optional)

Register the bundle:

// config/bundles.php
return [
    ChuckBartowski\ProxmoxSdk\ProxmoxSdkBundle::class => ['all' => true],
];

Then create config/packages/proxmox_sdk.yaml:

proxmox_sdk:
    host: '%env(PVE_HOST)%'
    token_id: '%env(PVE_TOKEN_ID)%'
    token_secret: '%env(PVE_TOKEN_SECRET)%'
    verify_ssl: true
# .env.local
PVE_HOST=pve.example.com
PVE_TOKEN_ID=automation@pve!sdk
PVE_TOKEN_SECRET=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Configuration reference

Key Type Default Description
host string required Proxmox VE hostname (no scheme, no port)
token_id string '' Full token id user@realm!tokenname (preferred)
token_secret string '' Token secret UUID
username / password string '' Ticket auth pair, used when no token is configured
realm string pam Realm appended to username when it has no @
port int 8006 API TLS port
verify_ssl bool true TLS peer/host verification (see Security Notes)
timeout float 30.0 Per-request timeout in seconds

The Proxmox facade is then autowirable in controllers, services, commands, and message handlers. The bundle reuses your application's http_client service when available and falls back to a native client otherwise.

Architecture

src/
├── ProxmoxSdkBundle.php         Symfony bundle: config tree + service wiring (optional)
├── Proxmox.php                  Facade: entry point for all modules
├── Client/
│   └── ProxmoxClient.php        Transport, token/ticket auth, CSRF, JSON handling
├── Response/
│   └── ApiResponse.php          Immutable normalized response (+ upid() helper)
├── Exception/
│   ├── ProxmoxSdkExceptionInterface.php
│   ├── ApiException.php         API answered but reported a failure (or a task failed)
│   ├── AuthenticationException.php
│   └── TransportException.php   Network / TLS / timeout / invalid JSON
└── Api/
    ├── AbstractApi.php          AbstractGuestApi.php (shared QEMU/LXC lifecycle)
    ├── NodeApi.php              QemuApi.php      LxcApi.php     ClusterApi.php
    └── StorageApi.php           BackupApi.php    NetworkApi.php AccessApi.php

Design decisions:

  • Facade + lazy modules: Proxmox instantiates each module on first use and caches it.
  • Modules always validate: every module method calls ensureSuccess() internally and throws ApiException on failure. To inspect a failed response without an exception, drop down to the client level.
  • One guest abstraction: AbstractGuestApi implements the shared VM/container lifecycle; QemuApi adds what only VMs have (hard reset, guest agent, VNC proxy).
  • Ticket state is self-healing: on a 401/403 with ticket auth, the client discards the ticket, re-logs once, and retries the request transparently.

Authentication

Two modes, checked in order:

  1. API token (recommended) — stateless, no CSRF, survives restarts. Create one in Datacenter » Permissions » API Tokens (or via this SDK):
$proxmox->access()->createToken('automation@pve', 'sdk', ['privsep' => 1]);

Configure with tokenId: 'automation@pve!sdk' + the secret shown once at creation. Grant the token its own permissions when privsep is enabled — a common pitfall: a privilege-separated token has no permissions until you add ACLs for it.

  1. Ticket (username/password) — the SDK logs in against /access/ticket, stores the ticket and CSRF token in memory, sends PVEAuthCookie on every call and CSRFPreventionToken on writes, and re-logs automatically when the ticket expires (~2 hours).

Missing credentials throw an AuthenticationException immediately, before any network request.

Asynchronous Tasks (UPID)

Most mutating operations (start, clone, migrate, vzdump, destroy…) return immediately with a task identifier (UPID:node:…) while the work continues server-side:

$task = $proxmox->qemu()->clone('pve1', 9000, 101, ['full' => 1]);

$task->upid();                                        // 'UPID:pve1:...:qmclone:9000:root@pam:'

$proxmox->nodes()->waitForTask('pve1', $task->upid(), timeout: 600.0, pollInterval: 2.0);

waitForTask() polls the task status until it stops, returns the final status on success (OK or warnings), and throws an ApiException carrying the real exitstatus when the task failed — so a failed clone never masquerades as a success. taskLog() fetches the task output for diagnostics.

API Reference

Every method returns an ApiResponse and throws on failure (see Error Handling).

Nodes

$proxmox->nodes()

Method Endpoint
list() GET /nodes
status(string $node) GET /nodes/{node}/status
reboot(string $node) / shutdown(string $node) POST /nodes/{node}/status
version(string $node) GET /nodes/{node}/version
services(string $node) / restartService(string $node, string $service) GET/POST /nodes/{node}/services…
tasks(string $node, array $filters = []) GET /nodes/{node}/tasks
taskStatus(...) / taskLog(...) / stopTask(...) GET/DELETE /nodes/{node}/tasks/{upid}…
waitForTask(string $node, string $upid, float $timeout = 300.0, float $pollInterval = 1.0) polling helper

QEMU virtual machines

$proxmox->qemu() — full VM lifecycle on /nodes/{node}/qemu.

Method Notes
list(node) / create(node, vmid, options) / remove(node, vmid, purge: bool) purge: true also removes unreferenced disks and job entries
config(node, vmid) / updateConfig(node, vmid, options)
currentStatus(node, vmid)
start / stop / shutdown / reboot / reset / suspend / resume all return a UPID
clone(node, vmid, newid, options) ['full' => 1] for a full clone, template linked clones otherwise
migrate(node, vmid, target, options) ['online' => 1] for live migration
resize(node, vmid, disk, size) e.g. ('scsi0', '+10G')
snapshots / createSnapshot / deleteSnapshot / rollbackSnapshot
agentPing(node, vmid) / agentExec(node, vmid, command) QEMU guest agent
vncProxy(node, vmid) websocket-enabled console ticket
$proxmox->qemu()->create('pve1', 101, [
    'name' => 'web01',
    'memory' => 4096,
    'cores' => 2,
    'net0' => 'virtio,bridge=vmbr0',
    'scsi0' => 'local-lvm:32',
    'ide2' => 'local:iso/debian-12.iso,media=cdrom',
]);

LXC containers

$proxmox->lxc() — the same lifecycle interface as QEMU (minus VM-only operations) on /nodes/{node}/lxc.

$proxmox->lxc()->create('pve1', 200, [
    'ostemplate' => 'local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst',
    'hostname' => 'app01',
    'memory' => 1024,
    'rootfs' => 'local-lvm:8',
    'net0' => 'name=eth0,bridge=vmbr0,ip=dhcp',
    'unprivileged' => 1,
]);

Cluster

$proxmox->cluster()version(), status(), resources(?type) (filter: vm, storage, node, sdn), tasks(), nextId(), options() / setOptions(), haResources(), and resource pools: pools(), pool(poolId), createPool(poolId, ?comment), updatePool(poolId, fields) (add/remove vms/storage members, delete => 1 to remove), deletePool(poolId).

nextId() + create() is the standard provisioning pattern shown in the Quick Start.

$proxmox->cluster()->createPool('customers');
$proxmox->cluster()->updatePool('customers', ['vms' => '101,102']);

Storage

$proxmox->storage() — cluster-wide definitions (list, create, update, remove) and per-node views: nodeStorages(node), status(node, storage), content(node, storage, ?contentType), deleteVolume(...), and downloadUrl(node, storage, url, filename) to pull ISOs/templates straight from a URL (returns a UPID).

Backups

$proxmox->backups() — scheduled job management on /cluster/backup (jobs, createJob, updateJob, deleteJob) and on-demand dumps: run(node, options) (POST /nodes/{node}/vzdump) and defaults(node).

$proxmox->backups()->run('pve1', ['vmid' => '101', 'storage' => 'backup-nfs', 'mode' => 'snapshot', 'compress' => 'zstd']);

Network

$proxmox->network() — node network interfaces: list, find, create(node, iface, type, options) (types: bridge, bond, vlan, …), update, remove, plus apply(node) to activate pending changes and revert(node) to discard them. Proxmox stages network changes — nothing is live until apply().

Access control

$proxmox->access() — users (users, createUser, updateUser, deleteUser), API tokens (tokens, createToken, deleteToken), groups, roles(), and ACLs: acl(), updateAcl(path, roles, options) (add ['delete' => 1] to revoke), permissions(?path, ?userid).

$proxmox->access()->createUser('deploy@pve', ['password' => 'S3cret!', 'groups' => 'automation']);
$proxmox->access()->updateAcl('/vms/101', 'PVEVMAdmin', ['users' => 'deploy@pve']);

Responses

All calls return an immutable ApiResponse; the Proxmox data envelope is already unwrapped:

$response = $proxmox->cluster()->resources('vm');

$response->success;          // bool
$response->statusCode;       // int
$response->data;             // mixed — the payload's data section
$response->data('version');  // keyed access with optional default
$response->errors;           // list<string> — field errors flattened as 'field: message'
$response->raw;              // complete decoded payload (envelope included)
$response->upid();           // task id string when the call started an async task, null otherwise

Error Handling

All SDK exceptions implement ProxmoxSdkExceptionInterface, so a single catch covers everything:

Exception Thrown when Extras
ApiException The API answered but reported a failure, or an awaited task failed getErrors(), getStatusCode(), getRaw()
AuthenticationException Credentials are missing, login failed, or 401/403 persisted after re-login thrown before any request when credentials are empty
TransportException Network error, TLS failure, timeout, or a non-JSON response body wraps the underlying symfony/http-client exception
use ChuckBartowski\ProxmoxSdk\Exception\ApiException;
use ChuckBartowski\ProxmoxSdk\Exception\ProxmoxSdkExceptionInterface;

try {
    $task = $proxmox->qemu()->clone('pve1', 9000, $vmid, ['full' => 1]);
    $proxmox->nodes()->waitForTask('pve1', $task->upid());
} catch (ApiException $e) {
    $this->logger->error('VM provisioning failed', ['errors' => $e->getErrors()]);
} catch (ProxmoxSdkExceptionInterface $e) {
    throw new ProvisioningUnavailableException(previous: $e);
}

To inspect a failed response without exceptions, use the client directly — client-level methods return the response as-is:

$response = $proxmox->client()->post('/nodes/pve1/qemu', $params);
if (!$response->success) {
    // $response->statusCode, $response->errors, $response->raw
}

Testing

The suite runs entirely offline against MockHttpClient:

composer install
vendor/bin/phpunit

To test your own services, inject a ProxmoxClient built with a mock:

use ChuckBartowski\ProxmoxSdk\Client\ProxmoxClient;
use ChuckBartowski\ProxmoxSdk\Proxmox;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\JsonMockResponse;

$http = new MockHttpClient(new JsonMockResponse(['data' => []]));
$proxmox = new Proxmox(new ProxmoxClient('host', tokenId: 't@pve!x', tokenSecret: 's', httpClient: $http));

Security Notes

  • Secrets (tokenSecret, password) are passed with #[\SensitiveParameter], so they never appear in stack traces.
  • Prefer API tokens with privilege separation (privsep => 1) and grant them only the ACLs they need (PVEVMAdmin on /vms, not Administrator on /).
  • A fresh Proxmox VE ships with a self-signed certificate. Rather than disabling verify_ssl, install a proper certificate (ACME is built into Proxmox) — keep verify_ssl: false strictly for lab environments.
  • Keep credentials in .env.local or your secret vault — never commit them.
  • remove() on guests (especially with purge: true), deleteVolume() and rollbackSnapshot() are irreversible — gate destructive calls behind confirmation flows in your application.

WHMCS module

A ready-to-use WHMCS provisioning module ships in whmcs/modules/servers/proxmoxsdk/. It provisions VPS by cloning a template through this SDK — clone + wait + start on create, suspend/resume, terminate (stop + purge), and reboot.

Install

  1. composer require chuckbartowski/proxmox-sdk in your WHMCS root.
  2. Copy the proxmoxsdk folder into <whmcs>/modules/servers/.
  3. Add a server with Type: Proxmox VE (SDK), the PVE hostname, the token id (user@realm!name) as username, and the token secret in the Access Hash field.
  4. Set the config options: Node, Template VMID, and VMID base (the new VMID is base + service id, so it is deterministic and collision-free).
Operation SDK call
Create qemu()->clone()nodes()->waitForTask()qemu()->start()
Suspend / Unsuspend qemu()->suspend() / resume()
Terminate qemu()->stop()remove(purge: true)
Reboot qemu()->reboot()

License

MIT

About

Modern PHP SDK for the Proxmox VE API — typed client, QEMU/LXC lifecycle, task helpers. Optional Symfony bundle.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages