beziworld / laravel-proxmox
Laravel client for the Proxmox VE API: nodes, QEMU VMs, LXC containers, snapshots, backups, storage, tasks with a UPID waiter, cluster resources and access, for many clusters.
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/cache: ^12.0|^13.0
- illuminate/collections: ^12.0|^13.0
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-10 19:23:12 UTC
README
Laravel client for the Proxmox VE API: nodes, QEMU virtual machines and LXC containers (lifecycle, configuration, cloning, snapshots, backups, migration), storage, asynchronous tasks with a UPID waiter, cluster resources and access control, for one or many clusters.
Features
- API tokens (
PVEAPIToken=user@realm!id=secret) or username/password tickets with theCSRFPreventionToken, cached and renewed automatically - Multiple named clusters plus
using([...])for clusters stored in your database - TLS verification on by default, per connection
verify_ssltrue, false or a CA bundle path (/etc/pve/pve-root-ca.pem) - Guests addressed by VMID:
Proxmox::vm(100)finds the node in the cluster resources - Every asynchronous call returns a
Task(UPID) withwait(),status(),log()andstop(); failures raiseTaskFailedwith the last log lines, long tasksTaskTimeout - Typed DTOs (
Node,ClusterResource,GuestStatus,Snapshot,TaskStatus,Storage,StorageVolume,ApiToken,User) that keep the raw payload - Reads retried on connection errors, HTTP 5xx and 595/596, writes never retried; validation errors exposed per parameter
php artisan proxmox:checkandphp artisan proxmox:task {upid}- PHP 8.3+, Laravel 12 and 13, PHPStan level 8, works with
Http::fake()andSleep::fake()
Why this package
On Packagist (November 2026) corsinvest/cv4pve-api-php has about 1.37M downloads: a complete generated client,
framework-agnostic and curl-based. Laravel packages are rare: irabbi360/laravel-php-proxmox (about 4.6k downloads)
always disables TLS verification, supports only password tickets for a single server and has no task handling;
h1g/proxmox (160 downloads) is a 2016 Laravel 5 binding. This package adds:
- API token authentication and ticket caching shared by queue workers
- several clusters, each with its own credentials, node and TLS settings
- a UPID task waiter, so provisioning flows (clone, configure, start) can be written top to bottom
- Laravel's HTTP client (testable with
Http::fake()), typed exceptions and DTOs
Installation
composer require beziworld/laravel-proxmox
php artisan vendor:publish --tag=proxmox-config # optional
PROXMOX_URL=https://pve1.example.com:8006 PROXMOX_TOKEN_ID=automation@pve!laravel # Datacenter > Permissions > API Tokens PROXMOX_TOKEN_SECRET=b3c6a8e2-.... PROXMOX_NODE=pve1 # default node for create/list calls PROXMOX_VERIFY_SSL=/etc/ssl/pve-root-ca.pem # true, false or a CA bundle path # or a ticket login instead of the token: # PROXMOX_USERNAME=root@pam # PROXMOX_PASSWORD=
php artisan proxmox:check
API tokens with privilege separation only hold the permissions granted to the token itself: grant a role (for
example PVEVMAdmin on /vms and PVEDatastoreUser on /storage) to the token, not only to its user.
More clusters go into config/proxmox.php:
'connections' => [ 'default' => [...], 'dc2' => [ 'url' => 'https://dc2-pve1.example.com:8006', 'token_id' => env('PROXMOX_DC2_TOKEN_ID'), 'token_secret' => env('PROXMOX_DC2_TOKEN_SECRET'), 'node' => 'dc2-pve1', 'verify_ssl' => storage_path('certs/dc2-pve-root-ca.pem'), ], ],
Usage
Provision a VM from a template
use Beziworld\Proxmox\Facades\Proxmox; $vmid = Proxmox::cluster()->nextId(); Proxmox::vm(9000)->clone($vmid, ['name' => 'shop-42', 'full' => true, 'storage' => 'local-lvm'])->wait(); $vm = Proxmox::vm($vmid); $vm->updateConfig(['memory' => 4096, 'cores' => 2, 'ipconfig0' => 'ip=10.0.0.42/24,gw=10.0.0.1'])?->wait(); $vm->resize('scsi0', '+20G')?->wait(); $vm->start()->wait();
Lifecycle
$vm = Proxmox::vm(100); // node looked up in /cluster/resources $vm = Proxmox::vm(100, 'pve1'); // or given explicitly $ct = Proxmox::container(200); $guest = Proxmox::guest(100); // type and node detected $vm->status()->isRunning(); $vm->start(); $vm->shutdown(timeout: 120, forceStop: true); $vm->stop(); $vm->reboot(); $vm->reset(); $vm->suspend(); $vm->resume(); $vm->config(); $vm->config(current: true); $vm->migrate('pve2', online: true)->wait(timeout: 3600); $vm->convertToTemplate(); $vm->destroy(purge: true);
Snapshots and backups
$vm->snapshot('before-upgrade', 'PHP 8.4 upgrade', withRam: false)->wait(); $vm->snapshots(); // list<Snapshot> $vm->rollback('before-upgrade', start: true)->wait(); $vm->deleteSnapshot('before-upgrade'); $vm->backup('pbs', mode: 'snapshot', compress: 'zstd', options: ['notes-template' => '{{guestname}}'])->wait(); $vm->backups('pbs'); // list<StorageVolume> Proxmox::storage('pve1')->deleteVolume('pbs', $backup->volid);
Tasks (UPID)
$task = $vm->start(); // Task $upid = (string) $task; // store it, e.g. in a queued job $status = Proxmox::task($upid)->wait(timeout: 600, interval: 2, onPoll: fn($s) => logger()->info($s->status)); $status->succeeded(); $status->exitStatus; // "OK", "WARNINGS: 1" or the error message Proxmox::task($upid)->log(); // list<string> Proxmox::tasks()->forNode('pve1', ['vmid' => 100, 'errors' => true]); Proxmox::tasks()->recent(); // cluster tasks
php artisan proxmox:task "UPID:pve1:000A1B2C:0123ABCD:6730A1B2:qmstart:100:root@pam:" --timeout=300
Nodes, cluster and storage
Proxmox::nodes()->list(); // list<Node> Proxmox::nodes()->status('pve1'); Proxmox::cluster()->resources('vm'); // list<ClusterResource> Proxmox::cluster()->guests(); Proxmox::cluster()->isIdFree(150); Proxmox::qemu('pve1')->list(); // list<GuestStatus> Proxmox::lxc('pve1')->create([ 'hostname' => 'ct1', 'ostemplate' => 'local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst', 'rootfs' => 'local-lvm:8', 'net0' => 'name=eth0,bridge=vmbr0,ip=dhcp', ])->wait(); Proxmox::storage('pve1')->list('backup'); // list<Storage> Proxmox::storage('pve1')->content('local', 'iso');
Access control
$token = Proxmox::access()->createToken('ci@pve', 'deploy', comment: 'CI', expiresAt: now()->addYear()); $token->value; // shown only once, store it Proxmox::access()->updateAcl('/vms/100', ['PVEVMUser'], tokens: [$token->fullTokenId]); Proxmox::access()->can('/vms/100', 'VM.PowerMgmt'); Proxmox::access()->users();
Runtime clusters and anything else
Proxmox::using([ 'url' => $cluster->url, 'token_id' => $cluster->token_id, 'token_secret' => decrypt($cluster->token_secret), ])->vm(100)->status(); Proxmox::request('GET', '/cluster/ha/resources'); Proxmox::request('POST', '/nodes/pve1/qemu/100/agent/ping');
Exceptions
All exceptions extend Beziworld\Proxmox\Exceptions\ProxmoxException:
| Exception | When |
|---|---|
AuthenticationException |
Token, ticket or login rejected (HTTP 401). A rejected login is never repeated. |
PermissionDenied |
Missing privilege (HTTP 403) |
ApiException |
Other error status; httpStatus(), errors() (per parameter), payload() |
RateLimitException |
HTTP 429 from a proxy in front of the cluster; retryAfter() |
ConnectionFailed |
DNS, TCP, TLS or timeout error (after read retries) |
TaskFailed |
wait() saw an exit status other than OK; status() and the last log lines |
TaskTimeout |
wait() gave up; the task keeps running (upid()) |
GuestNotFound |
VMID not in the cluster resources (or not visible to the token) |
InvalidConfiguration |
Unknown connection, missing URL, credentials or node |
Notes
- Calls are synchronous and tasks such as clones, backups and migrations take minutes: run them in queued jobs and
keep the UPID, or use
wait()with a timeout that fits the job. updateConfig()uses the asynchronous POST endpoint for VMs (needed for hotplug and disk allocation) and returns a task only when Proxmox started one.- Two-factor authentication is not supported for ticket logins: use an API token.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues and feature requests: GitHub issues
- Security vulnerabilities: see SECURITY.md
Need an integration? Get in touch through beziworld.eu.
Other packages by the author
- beziworld/laravel-directadmin: DirectAdmin hosting panel
- beziworld/laravel-plesk: Plesk hosting panel
- beziworld/laravel-unifi: UniFi Network
License
MIT. See LICENSE.md. This package is not affiliated with, endorsed by or sponsored by Proxmox Server Solutions GmbH. Proxmox is a registered trademark of Proxmox Server Solutions GmbH.