chuckbartowski/proxmox-sdk

PHP SDK for the Proxmox VE API with typed clients, high-level modules and task helpers

Maintainers

Package info

github.com/ChuckBartowski11/Proxmox

pkg:composer/chuckbartowski/proxmox-sdk

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-07-21 17:05 UTC

This package is auto-updated.

Last update: 2026-07-22 11:40:31 UTC


README

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