- Added template typing so approved templates can represent either QEMU VMs or LXC containers.
- Added LXC template discovery from Proxmox storage `vztmpl` content in the admin template manager. - Added live LXC container provisioning through the Proxmox API with configurable rootfs storage and optional DHCP bridge. - Routed start, stop, delete, expiration, status, and IP refresh operations through typed Proxmox VM/LXC API paths. - Added Proxmox tags to newly created VMs and containers, including a sanitized per-user tag for easier PVE administration. - Updated the admin and portal UI to show VM versus LXC template/deployment types and generic Proxmox resource IDs. - Added schema upgrades for template provisioning type, LXC template references, and deployment resource type. - Documented LXC setup, storage permissions, and the new Proxmox settings.
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.0 - 2026-04-24
|
||||
|
||||
- Added template typing so approved templates can represent either QEMU VMs or LXC containers.
|
||||
- Added LXC template discovery from Proxmox storage `vztmpl` content in the admin template manager.
|
||||
- Added live LXC container provisioning through the Proxmox API with configurable rootfs storage and optional DHCP bridge.
|
||||
- Routed start, stop, delete, expiration, status, and IP refresh operations through typed Proxmox VM/LXC API paths.
|
||||
- Added Proxmox tags to newly created VMs and containers, including a sanitized per-user tag for easier PVE administration.
|
||||
- Updated the admin and portal UI to show VM versus LXC template/deployment types and generic Proxmox resource IDs.
|
||||
- Added schema upgrades for template provisioning type, LXC template references, and deployment resource type.
|
||||
- Hardened admin template/settings saves against array-shaped POST values.
|
||||
- Kept QEMU template management usable if the Proxmox token cannot list LXC-capable storages.
|
||||
- Documented LXC setup, storage permissions, and the new Proxmox settings.
|
||||
|
||||
## 0.6.0 - 2026-04-24
|
||||
|
||||
- Added deployment sharing with a dedicated `wp_spp_deployment_shares` table.
|
||||
|
||||
64
README.md
64
README.md
@@ -1,6 +1,6 @@
|
||||
# Support Provisioning Portal
|
||||
|
||||
Internal WordPress plugin for support staff to provision standardized Proxmox VE VMs without direct Proxmox access.
|
||||
Internal WordPress plugin for support staff to provision standardized Proxmox VE VMs and LXC containers without direct Proxmox access.
|
||||
|
||||
The application runs as a WordPress plugin and exposes both:
|
||||
|
||||
@@ -35,12 +35,12 @@ The application runs as a WordPress plugin and exposes both:
|
||||
- Audit log rows for every mutating action
|
||||
- Optional `Never expire` deployments
|
||||
- Per-user and global RAM contingents
|
||||
- IP address display for deployments when available from the mock adapter or Proxmox guest agent
|
||||
- IP address display for deployments when available from the mock adapter, Proxmox guest agent, or LXC interfaces
|
||||
- Manual IP refresh action for deployments
|
||||
- Non-destructive expiration: expired VMs are stopped and locked, not deleted
|
||||
- Non-destructive expiration: expired VMs and containers are stopped and locked, not deleted
|
||||
- Plugin-owned per-user rights for portal access, lifecycle actions, settings, and user-rights management
|
||||
- Admin-panel user rights management for WordPress and SSO-created users
|
||||
- Admin-panel template management for importing QEMU templates from the configured Proxmox node
|
||||
- Admin-panel template management for importing QEMU VM and LXC templates from the configured Proxmox node
|
||||
- Owner/private deployment visibility with explicit per-deployment sharing
|
||||
- Minimal admin/frontend UI for:
|
||||
- deployment dashboard
|
||||
@@ -108,9 +108,9 @@ Deployment creation supports either a TTL in hours or **Never expire**.
|
||||
When a deployment reaches its TTL:
|
||||
|
||||
- WordPress cron and REST requests detect the expiration.
|
||||
- The plugin attempts to stop the VM.
|
||||
- The plugin attempts to stop the VM or container.
|
||||
- The deployment status becomes `EXPIRED`.
|
||||
- The VM cannot be started again until the user prolongs its TTL.
|
||||
- The deployment cannot be started again until the user prolongs its TTL.
|
||||
- The user can either prolong the TTL or delete the deployment.
|
||||
- Deleted deployments are hidden from the active deployment list, but audit rows remain.
|
||||
|
||||
@@ -125,9 +125,9 @@ Per-user overrides are available in the **User Rights** section on the **Support
|
||||
|
||||
## IP Addresses
|
||||
|
||||
Deployments include an `ipAddresses` field in REST responses and show those addresses in the UI. In mock mode, deterministic documentation-range IPs are assigned. In HTTP mode, the plugin reads IPs from the Proxmox guest-agent `network-get-interfaces` endpoint when available.
|
||||
Deployments include an `ipAddresses` field in REST responses and show those addresses in the UI. In mock mode, deterministic documentation-range IPs are assigned. In HTTP mode, the plugin reads VM IPs from the Proxmox guest-agent `network-get-interfaces` endpoint when available and reads LXC IPs from the container interfaces endpoint.
|
||||
|
||||
If the guest agent reports IPs only after boot, use **Refresh IPs** in the deployment detail view.
|
||||
If IPs are reported only after boot, use **Refresh IPs** in the deployment detail view.
|
||||
|
||||
## Proxmox Settings
|
||||
|
||||
@@ -138,6 +138,8 @@ The plugin defaults to mock mode. Configure live Proxmox access from the **Suppo
|
||||
- Token ID, for example `user@realm!token-name`
|
||||
- Token Secret. The saved secret is not rendered back into the settings form; leave the field blank to keep it unchanged.
|
||||
- Node, for example `pve-01`
|
||||
- LXC rootfs storage, for example `local-lvm`. This is required before live LXC deployments can be created.
|
||||
- LXC network bridge, for example `vmbr0`. Leave empty if containers should be created without an automatic DHCP `net0`.
|
||||
|
||||
No Proxmox secrets are committed to the repository.
|
||||
|
||||
@@ -147,16 +149,17 @@ Use this section when moving from mock mode to a real Proxmox VE node.
|
||||
|
||||
### 1. Prepare Proxmox Templates
|
||||
|
||||
The plugin only provisions from approved templates. Proxmox remains the source for actual VM templates, while the plugin stores an approved template policy row with display name, OS type, CPU, RAM, disk, default TTL, and Proxmox template VMID.
|
||||
The plugin only provisions from approved templates. Proxmox remains the source for actual QEMU VM templates and LXC OS templates, while the plugin stores an approved template policy row with display name, OS type, CPU, RAM, disk, default TTL, template type, and either a Proxmox template VMID or an LXC `ostemplate` reference.
|
||||
|
||||
Open the **Templates** section on the **Support Provisioning** admin page to:
|
||||
|
||||
- see approved plugin templates
|
||||
- import QEMU templates from the configured Proxmox node
|
||||
- import QEMU VM templates from the configured Proxmox node
|
||||
- import LXC templates from the configured node's storage `vztmpl` content
|
||||
- edit the policy values used during provisioning
|
||||
- remove templates from new provisioning without breaking historical deployment records
|
||||
|
||||
The plugin lists QEMU templates from the configured node via the Proxmox API. It does not provision from LXC templates.
|
||||
The plugin lists QEMU templates from `/nodes/{node}/qemu` and LXC templates from storage content with `content=vztmpl`.
|
||||
|
||||
Seeded example template IDs:
|
||||
|
||||
@@ -166,9 +169,9 @@ Seeded example template IDs:
|
||||
| Windows Support Client | `9002` |
|
||||
| Linux Utility VM | `9003` |
|
||||
|
||||
The fastest first test is to create one real Proxmox QEMU template, then import it from **Support Provisioning > Templates**.
|
||||
The fastest first test is to create one real Proxmox QEMU template or download one LXC OS template, then import it from **Support Provisioning > Templates**.
|
||||
|
||||
Template requirements:
|
||||
QEMU template requirements:
|
||||
|
||||
- The VM must be converted to a Proxmox template.
|
||||
- The template must exist on the Proxmox node configured in the plugin.
|
||||
@@ -176,6 +179,13 @@ Template requirements:
|
||||
- CPU and memory are set by the plugin after clone based on the approved template policy row.
|
||||
- For IP address display, install and enable `qemu-guest-agent` inside the guest and enable the guest agent option on the VM/template.
|
||||
|
||||
LXC template requirements:
|
||||
|
||||
- The OS template must be visible as `vztmpl` storage content on the configured Proxmox node.
|
||||
- Configure **LXC rootfs storage** in the plugin before deploying live containers.
|
||||
- Configure **LXC network bridge** if containers should receive a DHCP `eth0` during creation.
|
||||
- The rootfs storage must have enough free capacity for the approved template disk size.
|
||||
|
||||
### 2. Create A Dedicated Proxmox API User
|
||||
|
||||
In Proxmox, create a dedicated user instead of using `root@pam` for the plugin.
|
||||
@@ -214,12 +224,16 @@ The plugin needs permissions for:
|
||||
|
||||
- getting the next VMID
|
||||
- listing QEMU templates on the configured node
|
||||
- listing LXC template content on the node's storages
|
||||
- cloning a template VM
|
||||
- creating LXC containers from an OS template
|
||||
- changing CPU and memory after clone
|
||||
- starting and stopping VMs
|
||||
- deleting VMs
|
||||
- reading VM status
|
||||
- setting Proxmox tags on created VMs and containers
|
||||
- starting and stopping VMs and containers
|
||||
- deleting VMs and containers
|
||||
- reading VM and container status
|
||||
- reading guest-agent network interfaces for IP display
|
||||
- reading LXC interfaces for IP display
|
||||
|
||||
Practical first-test option:
|
||||
|
||||
@@ -236,7 +250,7 @@ pveum aclmod /storage/local-lvm -user wp-support@pve -role PVEDatastoreAdmin
|
||||
For a tighter production setup, create a custom role with only the required privileges and assign it to the API token or user:
|
||||
|
||||
```bash
|
||||
pveum role add SupportProvisioner -privs "VM.Allocate VM.Audit VM.Clone VM.Config.CPU VM.Config.Memory VM.PowerMgmt Datastore.AllocateSpace Datastore.Audit Sys.Audit"
|
||||
pveum role add SupportProvisioner -privs "VM.Allocate VM.Audit VM.Clone VM.Config.CPU VM.Config.Disk VM.Config.Memory VM.Config.Network VM.Config.Options VM.PowerMgmt Datastore.AllocateSpace Datastore.Audit Sys.Audit"
|
||||
pveum aclmod / -user wp-support@pve -role SupportProvisioner
|
||||
```
|
||||
|
||||
@@ -268,6 +282,8 @@ In WordPress admin, open **Support Provisioning** and use the **Proxmox Settings
|
||||
| Token ID | `wp-support@pve!support-portal` |
|
||||
| Token Secret | token secret from Proxmox |
|
||||
| Node | Proxmox node name, for example `pve-01` |
|
||||
| LXC rootfs storage | target container rootfs storage, for example `local-lvm` |
|
||||
| LXC network bridge | bridge for DHCP `net0`, for example `vmbr0` |
|
||||
|
||||
The **Node** value must match the Proxmox node name exactly as shown in the Proxmox UI under **Datacenter**.
|
||||
|
||||
@@ -278,9 +294,10 @@ Before creating a deployment:
|
||||
- Confirm WordPress can reach Proxmox on TCP `8006`.
|
||||
- Confirm the Proxmox base URL opens from the WordPress server.
|
||||
- Confirm the configured node name is exact.
|
||||
- Confirm the selected plugin template points to a real Proxmox template VMID.
|
||||
- Confirm the token has clone, VM config, power, delete, audit, and datastore permissions.
|
||||
- Confirm target storage has enough capacity for a full clone.
|
||||
- Confirm the selected plugin template points to a real Proxmox template VMID or visible LXC `vztmpl` item.
|
||||
- For LXC tests, confirm **LXC rootfs storage** is configured.
|
||||
- Confirm the token has clone/create, VM config, power, delete, audit, and datastore permissions.
|
||||
- Confirm target storage has enough capacity for the VM clone or LXC rootfs.
|
||||
- Confirm RAM contingents in WordPress are not blocking the selected template.
|
||||
|
||||
Then test in this order:
|
||||
@@ -302,16 +319,17 @@ Then test in this order:
|
||||
| `Missing Proxmox HTTP configuration` | One of Base URL, Token ID, Token Secret, or Node is empty. |
|
||||
| `Proxmox request failed with HTTP 401` | Token ID/secret is wrong, token was deleted, or the secret was copied incorrectly. |
|
||||
| `Proxmox request failed with HTTP 403` | Token exists but lacks permissions for clone/config/power/delete/storage. |
|
||||
| `Proxmox request failed with HTTP 404` | Node name or template VMID is wrong, or the template is on a different node. |
|
||||
| `Proxmox request failed with HTTP 404` | Node name, template VMID, or LXC template reference is wrong, or the template is on a different node/storage. |
|
||||
| `Missing LXC rootfs storage configuration` | An LXC template was selected but no target rootfs storage is configured in plugin settings. |
|
||||
| SSL/cURL certificate error | WordPress/PHP does not trust the Proxmox certificate. |
|
||||
| Deployment created but no IPs | Guest agent is missing, disabled, not running, or the VM has not finished booting. |
|
||||
| Deployment created but no IPs | Guest agent is missing/disabled for a VM, the VM/container has not finished booting, or the LXC bridge/DHCP path is not configured. |
|
||||
| Start is blocked | Deployment is `EXPIRED`; prolong the TTL first. |
|
||||
|
||||
## Design Notes
|
||||
|
||||
- Templates are the only provisioning path.
|
||||
- Resource limits come from approved templates, not user input.
|
||||
- Deployment lifecycle operations are routed through a dedicated Proxmox client interface.
|
||||
- Deployment lifecycle operations are routed through a dedicated Proxmox client interface and include the stored resource type so VM and LXC actions use the correct Proxmox API path.
|
||||
- Live Proxmox access can be replaced or expanded without changing REST or UI code.
|
||||
- WordPress users are used as actors for audit logging. Portal permissions are assigned per user inside the plugin so users created by SSO or external identity providers do not need WordPress author/editor roles.
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@
|
||||
return '<span class="spp-badge OWNER">OWNER</span>';
|
||||
};
|
||||
|
||||
const resourceTypeLabel = (type) => type === "lxc" ? "LXC" : "VM";
|
||||
|
||||
const actionButton = (permission, action, label, id, className = "", disabled = false) => {
|
||||
if (!can(permission)) {
|
||||
return "";
|
||||
@@ -195,7 +197,7 @@
|
||||
<div class="spp-header">
|
||||
<div>
|
||||
<h2 class="spp-title">Support Provisioning Portal</h2>
|
||||
<p class="spp-subtitle">Template-based VM provisioning for support work.</p>
|
||||
<p class="spp-subtitle">Template-based VM and LXC provisioning for support work.</p>
|
||||
${quotaLine()}
|
||||
</div>
|
||||
<div class="spp-tabs">
|
||||
@@ -215,7 +217,7 @@
|
||||
<div class="spp-panel">
|
||||
<table class="spp-table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Status</th><th>Access</th><th>Template</th><th>IP addresses</th><th>Expires</th><th></th></tr>
|
||||
<tr><th>Name</th><th>Status</th><th>Access</th><th>Type</th><th>Template</th><th>IP addresses</th><th>Expires</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${state.deployments.map((deployment) => `
|
||||
@@ -223,6 +225,7 @@
|
||||
<td><strong>${escapeHtml(deployment.name)}</strong></td>
|
||||
<td>${statusBadge(deployment.status)}</td>
|
||||
<td>${accessLabel(deployment)}</td>
|
||||
<td>${resourceTypeLabel(deployment.provisioningType)}</td>
|
||||
<td>${escapeHtml(deployment.templateName)}</td>
|
||||
<td>${ipList(deployment.ipAddresses)}</td>
|
||||
<td>${dateTime(deployment.expiresAt)}</td>
|
||||
@@ -243,6 +246,7 @@
|
||||
<p>${escapeHtml(template.description)}</p>
|
||||
<div class="spp-meta">
|
||||
<span>OS<strong>${template.osType}</strong></span>
|
||||
<span>Type<strong>${resourceTypeLabel(template.provisioningType)}</strong></span>
|
||||
<span>CPU<strong>${template.cpuCores} cores</strong></span>
|
||||
<span>Memory<strong>${template.memoryMb} MB</strong></span>
|
||||
<span>Disk<strong>${template.diskGb} GB</strong></span>
|
||||
@@ -263,7 +267,7 @@
|
||||
<form class="spp-form" id="spp-create-form">
|
||||
<label>Template
|
||||
<select class="spp-select" name="templateId" required>
|
||||
${state.templates.map((template) => `<option value="${template.id}">${escapeHtml(template.name)}</option>`).join("")}
|
||||
${state.templates.map((template) => `<option value="${template.id}">[${resourceTypeLabel(template.provisioningType)}] ${escapeHtml(template.name)}</option>`).join("")}
|
||||
</select>
|
||||
</label>
|
||||
<label>Deployment name
|
||||
@@ -313,9 +317,10 @@
|
||||
</div>
|
||||
<div class="spp-meta">
|
||||
<span>Template<strong>${escapeHtml(deployment.templateName)}</strong></span>
|
||||
<span>Type<strong>${resourceTypeLabel(deployment.provisioningType)}</strong></span>
|
||||
<span>Requested by<strong>${escapeHtml(deployment.requestedByName)}</strong></span>
|
||||
<span>Access<strong>${deployment.accessType ? escapeHtml(deployment.accessType) : "Owner"}</strong></span>
|
||||
<span>Proxmox VM ID<strong>${deployment.proxmoxVmId || "Pending"}</strong></span>
|
||||
<span>Proxmox ID<strong>${deployment.proxmoxResourceId || deployment.proxmoxVmId || "Pending"}</strong></span>
|
||||
<span>IP addresses<strong>${ipList(deployment.ipAddresses)}</strong></span>
|
||||
<span>CPU<strong>${deployment.cpuCores} cores</strong></span>
|
||||
<span>Memory<strong>${deployment.memoryMb} MB</strong></span>
|
||||
|
||||
@@ -6,7 +6,7 @@ if (!defined('ABSPATH')) {
|
||||
|
||||
final class SPP_Activator
|
||||
{
|
||||
private const DB_VERSION = '0.6.0';
|
||||
private const DB_VERSION = '0.7.0';
|
||||
|
||||
public static function activate(): void
|
||||
{
|
||||
@@ -15,6 +15,8 @@ final class SPP_Activator
|
||||
|
||||
add_option('spp_proxmox_mode', 'mock');
|
||||
add_option('spp_proxmox_node', 'pve-01');
|
||||
add_option('spp_lxc_rootfs_storage', '');
|
||||
add_option('spp_lxc_bridge', 'vmbr0');
|
||||
add_option('spp_mock_next_vm_id', 10000);
|
||||
add_option('spp_quota_user_memory_mb', 0);
|
||||
add_option('spp_quota_global_memory_mb', 0);
|
||||
@@ -63,7 +65,9 @@ final class SPP_Activator
|
||||
memory_mb int unsigned NOT NULL,
|
||||
disk_gb int unsigned NOT NULL,
|
||||
default_ttl_hours int unsigned NOT NULL,
|
||||
provisioning_type varchar(16) NOT NULL DEFAULT 'qemu',
|
||||
proxmox_template_id int unsigned NOT NULL,
|
||||
proxmox_template_ref varchar(255) NULL,
|
||||
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
@@ -75,6 +79,7 @@ final class SPP_Activator
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(160) NOT NULL,
|
||||
status varchar(32) NOT NULL,
|
||||
provisioning_type varchar(16) NOT NULL DEFAULT 'qemu',
|
||||
proxmox_vm_id int unsigned DEFAULT NULL,
|
||||
ip_addresses longtext NULL,
|
||||
error_message text NULL,
|
||||
@@ -92,6 +97,10 @@ final class SPP_Activator
|
||||
|
||||
$wpdb->query("ALTER TABLE {$deployments} MODIFY expires_at datetime NULL");
|
||||
|
||||
self::add_column_if_missing($templates, 'provisioning_type', "ALTER TABLE {$templates} ADD COLUMN provisioning_type varchar(16) NOT NULL DEFAULT 'qemu' AFTER default_ttl_hours");
|
||||
self::add_column_if_missing($templates, 'proxmox_template_ref', "ALTER TABLE {$templates} ADD COLUMN proxmox_template_ref varchar(255) NULL AFTER proxmox_template_id");
|
||||
self::add_column_if_missing($deployments, 'provisioning_type', "ALTER TABLE {$deployments} ADD COLUMN provisioning_type varchar(16) NOT NULL DEFAULT 'qemu' AFTER status");
|
||||
|
||||
$ip_column = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM {$deployments} LIKE %s", 'ip_addresses'));
|
||||
if ($ip_column === null) {
|
||||
$wpdb->query("ALTER TABLE {$deployments} ADD COLUMN ip_addresses longtext NULL AFTER proxmox_vm_id");
|
||||
@@ -138,6 +147,16 @@ final class SPP_Activator
|
||||
return $wpdb->prefix . 'spp_' . $name;
|
||||
}
|
||||
|
||||
private static function add_column_if_missing(string $table, string $column, string $sql): void
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$exists = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM {$table} LIKE %s", $column));
|
||||
if ($exists === null) {
|
||||
$wpdb->query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
private static function seed_templates(): void
|
||||
{
|
||||
global $wpdb;
|
||||
@@ -154,7 +173,9 @@ final class SPP_Activator
|
||||
'memory_mb' => 2048,
|
||||
'disk_gb' => 24,
|
||||
'default_ttl_hours' => 72,
|
||||
'provisioning_type' => 'qemu',
|
||||
'proxmox_template_id' => 9001,
|
||||
'proxmox_template_ref' => null,
|
||||
],
|
||||
[
|
||||
'template_key' => 'windows-support-client',
|
||||
@@ -165,7 +186,9 @@ final class SPP_Activator
|
||||
'memory_mb' => 8192,
|
||||
'disk_gb' => 80,
|
||||
'default_ttl_hours' => 48,
|
||||
'provisioning_type' => 'qemu',
|
||||
'proxmox_template_id' => 9002,
|
||||
'proxmox_template_ref' => null,
|
||||
],
|
||||
[
|
||||
'template_key' => 'linux-utility-vm',
|
||||
@@ -176,7 +199,9 @@ final class SPP_Activator
|
||||
'memory_mb' => 2048,
|
||||
'disk_gb' => 32,
|
||||
'default_ttl_hours' => 168,
|
||||
'provisioning_type' => 'qemu',
|
||||
'proxmox_template_id' => 9003,
|
||||
'proxmox_template_ref' => null,
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ final class SPP_Admin_Page
|
||||
'settings_saved' => __('Settings saved.', 'support-provisioning-portal'),
|
||||
'template_saved' => __('Template saved.', 'support-provisioning-portal'),
|
||||
'template_removed' => __('Template removed from new provisioning.', 'support-provisioning-portal'),
|
||||
'template_error' => __('Template could not be saved. Check the fields and confirm the VMID exists as a Proxmox QEMU template on the configured node.', 'support-provisioning-portal'),
|
||||
'template_error' => __('Template could not be saved. Check the fields and confirm the selected Proxmox VM or LXC template exists on the configured node.', 'support-provisioning-portal'),
|
||||
'user_access_saved' => __('User rights saved.', 'support-provisioning-portal'),
|
||||
'manager_required' => __('At least one user must keep the Manage user rights permission.', 'support-provisioning-portal'),
|
||||
];
|
||||
@@ -169,6 +169,14 @@ final class SPP_Admin_Page
|
||||
<span>Node</span>
|
||||
<input name="spp_proxmox_node" type="text" value="<?php echo esc_attr(get_option('spp_proxmox_node', 'pve-01')); ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>LXC rootfs storage</span>
|
||||
<input name="spp_lxc_rootfs_storage" type="text" value="<?php echo esc_attr(get_option('spp_lxc_rootfs_storage', '')); ?>" placeholder="local-lvm">
|
||||
</label>
|
||||
<label>
|
||||
<span>LXC network bridge</span>
|
||||
<input name="spp_lxc_bridge" type="text" value="<?php echo esc_attr(get_option('spp_lxc_bridge', 'vmbr0')); ?>" placeholder="vmbr0">
|
||||
</label>
|
||||
<h2><?php echo esc_html__('RAM Contingents', 'support-provisioning-portal'); ?></h2>
|
||||
<p class="description"><?php echo esc_html__('Set 0 for unlimited. Active allocations include provisioning, stopped, running, and deleting deployments.', 'support-provisioning-portal'); ?></p>
|
||||
<label>
|
||||
@@ -187,7 +195,7 @@ final class SPP_Admin_Page
|
||||
private function render_template_management(): void
|
||||
{
|
||||
$approved_templates = $this->repository->admin_templates();
|
||||
$active_proxmox_ids = $this->repository->active_proxmox_template_ids();
|
||||
$active_template_keys = $this->repository->active_template_identity_keys();
|
||||
$proxmox_error = null;
|
||||
|
||||
try {
|
||||
@@ -220,7 +228,7 @@ final class SPP_Admin_Page
|
||||
<div class="spp-template-row-head">
|
||||
<div>
|
||||
<strong><?php echo esc_html((string) $template['name']); ?></strong>
|
||||
<span><?php echo esc_html(sprintf('PVE VMID %d', (int) $template['proxmoxTemplateId'])); ?></span>
|
||||
<span><?php echo esc_html($this->template_identity_label($template)); ?></span>
|
||||
</div>
|
||||
<?php if (!empty($template['isActive'])) : ?>
|
||||
<span class="spp-badge RUNNING"><?php echo esc_html__('Active', 'support-provisioning-portal'); ?></span>
|
||||
@@ -229,12 +237,24 @@ final class SPP_Admin_Page
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="spp-template-fields">
|
||||
<input type="hidden" name="spp_provisioning_type" value="<?php echo esc_attr((string) $template['provisioningType']); ?>">
|
||||
<label>Name
|
||||
<input name="spp_template_name" type="text" required maxlength="160" value="<?php echo esc_attr((string) $template['name']); ?>">
|
||||
</label>
|
||||
<label>PVE template VMID
|
||||
<input name="spp_proxmox_template_id" type="number" min="1" required value="<?php echo esc_attr((string) $template['proxmoxTemplateId']); ?>">
|
||||
<label>Type
|
||||
<input type="text" readonly value="<?php echo esc_attr($this->template_type_label((string) $template['provisioningType'])); ?>">
|
||||
</label>
|
||||
<?php if ((string) $template['provisioningType'] === 'lxc') : ?>
|
||||
<input type="hidden" name="spp_proxmox_template_id" value="0">
|
||||
<label>LXC template ref
|
||||
<input name="spp_proxmox_template_ref" type="text" readonly required value="<?php echo esc_attr((string) $template['proxmoxTemplateRef']); ?>">
|
||||
</label>
|
||||
<?php else : ?>
|
||||
<input type="hidden" name="spp_proxmox_template_ref" value="">
|
||||
<label>PVE template VMID
|
||||
<input name="spp_proxmox_template_id" type="number" min="1" required value="<?php echo esc_attr((string) $template['proxmoxTemplateId']); ?>">
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
<label>OS type
|
||||
<?php $this->render_os_type_select((string) $template['osType']); ?>
|
||||
</label>
|
||||
@@ -271,17 +291,27 @@ final class SPP_Admin_Page
|
||||
<p class="spp-error"><?php echo esc_html($proxmox_error); ?></p>
|
||||
<?php elseif (empty($proxmox_templates)) : ?>
|
||||
<div class="spp-panel spp-admin-notice">
|
||||
<p><?php echo esc_html__('No QEMU templates were returned by Proxmox for the configured node.', 'support-provisioning-portal'); ?></p>
|
||||
<p><?php echo esc_html__('No QEMU VM or LXC templates were returned by Proxmox for the configured node.', 'support-provisioning-portal'); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="spp-pve-template-grid">
|
||||
<?php foreach ($proxmox_templates as $template) : ?>
|
||||
<?php $is_imported = in_array((int) $template['vmId'], $active_proxmox_ids, true); ?>
|
||||
<?php
|
||||
$provisioning_type = $this->normalise_template_type((string) ($template['provisioningType'] ?? 'qemu'));
|
||||
$template_ref = (string) ($template['templateRef'] ?? '');
|
||||
$is_imported = in_array(
|
||||
$this->template_identity_key($provisioning_type, (int) ($template['vmId'] ?? 0), $template_ref),
|
||||
$active_template_keys,
|
||||
true
|
||||
);
|
||||
?>
|
||||
<form class="spp-pve-template" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<input type="hidden" name="action" value="spp_save_template">
|
||||
<input type="hidden" name="spp_template_action" value="import">
|
||||
<input type="hidden" name="spp_provisioning_type" value="<?php echo esc_attr($provisioning_type); ?>">
|
||||
<input type="hidden" name="spp_template_name" value="<?php echo esc_attr((string) $template['name']); ?>">
|
||||
<input type="hidden" name="spp_proxmox_template_id" value="<?php echo esc_attr((string) $template['vmId']); ?>">
|
||||
<input type="hidden" name="spp_proxmox_template_id" value="<?php echo esc_attr((string) ($template['vmId'] ?? 0)); ?>">
|
||||
<input type="hidden" name="spp_proxmox_template_ref" value="<?php echo esc_attr($template_ref); ?>">
|
||||
<input type="hidden" name="spp_cpu_cores" value="<?php echo esc_attr((string) $template['cpuCores']); ?>">
|
||||
<input type="hidden" name="spp_memory_mb" value="<?php echo esc_attr((string) $template['memoryMb']); ?>">
|
||||
<input type="hidden" name="spp_disk_gb" value="<?php echo esc_attr((string) $template['diskGb']); ?>">
|
||||
@@ -289,13 +319,18 @@ final class SPP_Admin_Page
|
||||
<div class="spp-template-row-head">
|
||||
<div>
|
||||
<strong><?php echo esc_html((string) $template['name']); ?></strong>
|
||||
<span><?php echo esc_html(sprintf('PVE VMID %d', (int) $template['vmId'])); ?></span>
|
||||
<span><?php echo esc_html($this->template_identity_label([
|
||||
'provisioningType' => $provisioning_type,
|
||||
'proxmoxTemplateId' => (int) ($template['vmId'] ?? 0),
|
||||
'proxmoxTemplateRef' => $template_ref,
|
||||
])); ?></span>
|
||||
</div>
|
||||
<?php if ($is_imported) : ?>
|
||||
<span class="spp-badge RUNNING"><?php echo esc_html__('Imported', 'support-provisioning-portal'); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="spp-meta">
|
||||
<span>Type<strong><?php echo esc_html($this->template_type_label($provisioning_type)); ?></strong></span>
|
||||
<span>CPU<strong><?php echo esc_html((string) $template['cpuCores']); ?> cores</strong></span>
|
||||
<span>Memory<strong><?php echo esc_html((string) $template['memoryMb']); ?> MB</strong></span>
|
||||
<span>Disk<strong><?php echo esc_html((string) $template['diskGb']); ?> GB</strong></span>
|
||||
@@ -309,7 +344,7 @@ final class SPP_Admin_Page
|
||||
<input name="spp_default_ttl_hours" type="number" min="1" max="720" value="168" required>
|
||||
</label>
|
||||
<label>Description
|
||||
<textarea name="spp_template_description" rows="2" required><?php echo esc_textarea(sprintf('Imported from Proxmox template VMID %d.', (int) $template['vmId'])); ?></textarea>
|
||||
<textarea name="spp_template_description" rows="2" required><?php echo esc_textarea($provisioning_type === 'lxc' ? sprintf('Imported from Proxmox LXC template %s.', $template_ref) : sprintf('Imported from Proxmox template VMID %d.', (int) $template['vmId'])); ?></textarea>
|
||||
</label>
|
||||
<button class="button button-primary" type="submit"><?php echo esc_html__('Import Template', 'support-provisioning-portal'); ?></button>
|
||||
<?php endif; ?>
|
||||
@@ -318,10 +353,12 @@ final class SPP_Admin_Page
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3><?php echo esc_html__('Add Template Manually', 'support-provisioning-portal'); ?></h3>
|
||||
<h3><?php echo esc_html__('Add QEMU Template Manually', 'support-provisioning-portal'); ?></h3>
|
||||
<form class="spp-template-row" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<input type="hidden" name="action" value="spp_save_template">
|
||||
<input type="hidden" name="spp_template_action" value="import">
|
||||
<input type="hidden" name="spp_provisioning_type" value="qemu">
|
||||
<input type="hidden" name="spp_proxmox_template_ref" value="">
|
||||
<?php wp_nonce_field('spp_save_template'); ?>
|
||||
<div class="spp-template-fields">
|
||||
<label>Name
|
||||
@@ -464,6 +501,8 @@ final class SPP_Admin_Page
|
||||
update_option('spp_proxmox_token_secret', $token_secret);
|
||||
}
|
||||
update_option('spp_proxmox_node', sanitize_text_field($this->posted_string('spp_proxmox_node')));
|
||||
update_option('spp_lxc_rootfs_storage', $this->sanitize_proxmox_identifier($this->posted_string('spp_lxc_rootfs_storage')));
|
||||
update_option('spp_lxc_bridge', $this->sanitize_proxmox_identifier($this->posted_string('spp_lxc_bridge')));
|
||||
update_option('spp_quota_user_memory_mb', max(0, absint($this->posted_string('spp_quota_user_memory_mb'))));
|
||||
update_option('spp_quota_global_memory_mb', max(0, absint($this->posted_string('spp_quota_global_memory_mb'))));
|
||||
|
||||
@@ -495,7 +534,11 @@ final class SPP_Admin_Page
|
||||
$this->redirect_to_admin_page('template_error');
|
||||
}
|
||||
|
||||
if (!$this->proxmox_template_exists((int) $data['proxmox_template_id'])) {
|
||||
if (!$this->proxmox_template_exists(
|
||||
(string) $data['provisioning_type'],
|
||||
(int) $data['proxmox_template_id'],
|
||||
(string) $data['proxmox_template_ref']
|
||||
)) {
|
||||
$this->redirect_to_admin_page('template_error');
|
||||
}
|
||||
|
||||
@@ -579,7 +622,13 @@ final class SPP_Admin_Page
|
||||
|
||||
private function posted_string(string $key): string
|
||||
{
|
||||
return isset($_POST[$key]) ? (string) wp_unslash($_POST[$key]) : '';
|
||||
if (!isset($_POST[$key])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = wp_unslash($_POST[$key]);
|
||||
|
||||
return is_scalar($value) ? (string) $value : '';
|
||||
}
|
||||
|
||||
private function sanitize_proxmox_base_url(string $value): string
|
||||
@@ -593,11 +642,30 @@ final class SPP_Admin_Page
|
||||
return wp_parse_url($url, PHP_URL_SCHEME) === 'https' ? $url : '';
|
||||
}
|
||||
|
||||
private function proxmox_template_exists(int $vm_id): bool
|
||||
private function sanitize_proxmox_identifier(string $value): string
|
||||
{
|
||||
$value = sanitize_text_field($value);
|
||||
$value = preg_replace('/[^A-Za-z0-9_.:-]/', '', $value);
|
||||
|
||||
return $value === null ? '' : $value;
|
||||
}
|
||||
|
||||
private function proxmox_template_exists(string $provisioning_type, int $vm_id, string $template_ref): bool
|
||||
{
|
||||
$provisioning_type = $this->normalise_template_type($provisioning_type);
|
||||
|
||||
try {
|
||||
foreach ($this->proxmox->list_templates() as $template) {
|
||||
if ((int) $template['vmId'] === $vm_id) {
|
||||
$candidate_type = $this->normalise_template_type((string) ($template['provisioningType'] ?? 'qemu'));
|
||||
if ($candidate_type !== $provisioning_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($candidate_type === 'lxc' && (string) ($template['templateRef'] ?? '') === $template_ref) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($candidate_type === 'qemu' && (int) ($template['vmId'] ?? 0) === $vm_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -614,15 +682,25 @@ final class SPP_Admin_Page
|
||||
private function posted_template_data(): ?array
|
||||
{
|
||||
$name = sanitize_text_field($this->posted_string('spp_template_name'));
|
||||
$provisioning_type = $this->normalise_template_type($this->posted_string('spp_provisioning_type'));
|
||||
$proxmox_template_id = absint($this->posted_string('spp_proxmox_template_id'));
|
||||
$proxmox_template_ref = sanitize_text_field($this->posted_string('spp_proxmox_template_ref'));
|
||||
$description = sanitize_textarea_field($this->posted_string('spp_template_description'));
|
||||
|
||||
if ($name === '' || $proxmox_template_id < 1 || $description === '') {
|
||||
if ($name === '' || $description === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($provisioning_type === 'qemu' && $proxmox_template_id < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($provisioning_type === 'lxc' && $proxmox_template_ref === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'template_key' => 'pve-template-' . $proxmox_template_id . '-' . sanitize_title($name),
|
||||
'template_key' => $this->template_identity_key($provisioning_type, $proxmox_template_id, $proxmox_template_ref) . '-' . sanitize_title($name),
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'os_type' => $this->posted_os_type(),
|
||||
@@ -630,7 +708,9 @@ final class SPP_Admin_Page
|
||||
'memory_mb' => max(128, absint($this->posted_string('spp_memory_mb'))),
|
||||
'disk_gb' => max(1, absint($this->posted_string('spp_disk_gb'))),
|
||||
'default_ttl_hours' => max(1, min(720, absint($this->posted_string('spp_default_ttl_hours')))),
|
||||
'provisioning_type' => $provisioning_type,
|
||||
'proxmox_template_id' => $proxmox_template_id,
|
||||
'proxmox_template_ref' => $proxmox_template_ref,
|
||||
'is_active' => $this->posted_string('spp_is_active') === '1',
|
||||
];
|
||||
}
|
||||
@@ -642,6 +722,37 @@ final class SPP_Admin_Page
|
||||
return in_array($os_type, ['LINUX', 'WINDOWS', 'APPLIANCE', 'OTHER'], true) ? $os_type : 'OTHER';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $template
|
||||
*/
|
||||
private function template_identity_label(array $template): string
|
||||
{
|
||||
$type = $this->normalise_template_type((string) ($template['provisioningType'] ?? 'qemu'));
|
||||
|
||||
if ($type === 'lxc') {
|
||||
return 'LXC ' . (string) ($template['proxmoxTemplateRef'] ?? $template['templateRef'] ?? '');
|
||||
}
|
||||
|
||||
return sprintf('PVE VMID %d', (int) ($template['proxmoxTemplateId'] ?? $template['vmId'] ?? 0));
|
||||
}
|
||||
|
||||
private function template_type_label(string $type): string
|
||||
{
|
||||
return $this->normalise_template_type($type) === 'lxc' ? 'LXC container' : 'QEMU VM';
|
||||
}
|
||||
|
||||
private function template_identity_key(string $provisioning_type, int $proxmox_template_id, string $proxmox_template_ref): string
|
||||
{
|
||||
return $this->normalise_template_type($provisioning_type) === 'lxc'
|
||||
? 'lxc:' . $proxmox_template_ref
|
||||
: 'qemu:' . $proxmox_template_id;
|
||||
}
|
||||
|
||||
private function normalise_template_type(string $type): string
|
||||
{
|
||||
return strtolower($type) === 'lxc' ? 'lxc' : 'qemu';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,7 @@ final class SPP_Expiration_Service
|
||||
|
||||
if (!empty($deployment['proxmox_vm_id']) && $deployment['status'] !== 'STOPPED') {
|
||||
try {
|
||||
$this->proxmox->stop_vm((int) $deployment['proxmox_vm_id']);
|
||||
$this->proxmox->stop_instance((string) ($deployment['provisioning_type'] ?? 'qemu'), (int) $deployment['proxmox_vm_id']);
|
||||
} catch (Throwable $error) {
|
||||
$stop_error = $error->getMessage();
|
||||
}
|
||||
|
||||
@@ -6,18 +6,81 @@ if (!defined('ABSPATH')) {
|
||||
|
||||
final class SPP_Http_Proxmox_Client implements SPP_Proxmox_Client
|
||||
{
|
||||
/** @var array{base_url:string, token_id:string, token_secret:string, node:string} */
|
||||
/** @var array{base_url:string, token_id:string, token_secret:string, node:string, lxc_rootfs_storage:string, lxc_bridge:string} */
|
||||
private array $options;
|
||||
|
||||
/**
|
||||
* @param array{base_url:string, token_id:string, token_secret:string, node:string} $options
|
||||
* @param array{base_url:string, token_id:string, token_secret:string, node:string, lxc_rootfs_storage?:string, lxc_bridge?:string} $options
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$this->options = $options;
|
||||
$this->options = array_merge([
|
||||
'lxc_rootfs_storage' => '',
|
||||
'lxc_bridge' => 'vmbr0',
|
||||
], $options);
|
||||
}
|
||||
|
||||
public function list_templates(): array
|
||||
{
|
||||
$templates = array_merge(
|
||||
$this->list_qemu_templates(),
|
||||
$this->list_lxc_templates()
|
||||
);
|
||||
|
||||
usort(
|
||||
$templates,
|
||||
static function (array $left, array $right): int {
|
||||
$type_compare = strcmp((string) $left['provisioningType'], (string) $right['provisioningType']);
|
||||
|
||||
return $type_compare !== 0
|
||||
? $type_compare
|
||||
: strcasecmp((string) $left['name'], (string) $right['name']);
|
||||
}
|
||||
);
|
||||
|
||||
return $templates;
|
||||
}
|
||||
|
||||
public function provision_instance(array $input): array
|
||||
{
|
||||
return $this->normalise_type((string) ($input['provisioning_type'] ?? 'qemu')) === 'lxc'
|
||||
? $this->create_lxc_container($input)
|
||||
: $this->clone_qemu_vm($input);
|
||||
}
|
||||
|
||||
public function start_instance(string $type, int $vm_id): void
|
||||
{
|
||||
$this->request($this->guest_path($type, $vm_id) . '/status/start', 'POST');
|
||||
}
|
||||
|
||||
public function stop_instance(string $type, int $vm_id): void
|
||||
{
|
||||
$this->request($this->guest_path($type, $vm_id) . '/status/stop', 'POST');
|
||||
}
|
||||
|
||||
public function delete_instance(string $type, int $vm_id): void
|
||||
{
|
||||
$this->request($this->guest_path($type, $vm_id), 'DELETE');
|
||||
}
|
||||
|
||||
public function get_status(string $type, int $vm_id): string
|
||||
{
|
||||
$data = $this->request($this->guest_path($type, $vm_id) . '/status/current', 'GET');
|
||||
|
||||
return is_array($data) && isset($data['status']) ? (string) $data['status'] : 'unknown';
|
||||
}
|
||||
|
||||
public function get_ip_addresses(string $type, int $vm_id): array
|
||||
{
|
||||
return $this->normalise_type($type) === 'lxc'
|
||||
? $this->get_lxc_ip_addresses($vm_id)
|
||||
: $this->get_qemu_ip_addresses($vm_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function list_qemu_templates(): array
|
||||
{
|
||||
$data = $this->request("/nodes/{$this->options['node']}/qemu", 'GET');
|
||||
$vms = is_array($data) ? $data : [];
|
||||
@@ -29,7 +92,10 @@ final class SPP_Http_Proxmox_Client implements SPP_Proxmox_Client
|
||||
}
|
||||
|
||||
$templates[] = [
|
||||
'provisioningType' => 'qemu',
|
||||
'vmId' => (int) $vm['vmid'],
|
||||
'templateRef' => '',
|
||||
'storage' => '',
|
||||
'name' => isset($vm['name']) && $vm['name'] !== '' ? (string) $vm['name'] : 'template-' . (int) $vm['vmid'],
|
||||
'cpuCores' => max(1, (int) ($vm['cpus'] ?? 1)),
|
||||
'memoryMb' => $this->bytes_to_mb((int) ($vm['maxmem'] ?? 0)),
|
||||
@@ -38,15 +104,83 @@ final class SPP_Http_Proxmox_Client implements SPP_Proxmox_Client
|
||||
];
|
||||
}
|
||||
|
||||
usort(
|
||||
$templates,
|
||||
static fn(array $left, array $right): int => strcasecmp((string) $left['name'], (string) $right['name'])
|
||||
);
|
||||
return $templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function list_lxc_templates(): array
|
||||
{
|
||||
try {
|
||||
$storages = $this->request("/nodes/{$this->options['node']}/storage", 'GET');
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$storages = is_array($storages) ? $storages : [];
|
||||
$templates = [];
|
||||
|
||||
foreach ($storages as $storage) {
|
||||
if (!is_array($storage) || empty($storage['storage'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($storage['enabled']) && (int) $storage['enabled'] !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = is_array($storage['content'] ?? null)
|
||||
? implode(',', $storage['content'])
|
||||
: (string) ($storage['content'] ?? '');
|
||||
|
||||
if ($content !== '' && !str_contains(strtolower($content), 'vztmpl')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$storage_id = (string) $storage['storage'];
|
||||
|
||||
try {
|
||||
$items = $this->request(
|
||||
add_query_arg(['content' => 'vztmpl'], "/nodes/{$this->options['node']}/storage/" . rawurlencode($storage_id) . '/content'),
|
||||
'GET'
|
||||
);
|
||||
} catch (Throwable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (is_array($items) ? $items : [] as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$volid = (string) ($item['volid'] ?? $item['volume'] ?? '');
|
||||
if ($volid === '' || (isset($item['content']) && (string) $item['content'] !== 'vztmpl')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$templates[] = [
|
||||
'provisioningType' => 'lxc',
|
||||
'vmId' => 0,
|
||||
'templateRef' => $volid,
|
||||
'storage' => $storage_id,
|
||||
'name' => $this->template_name_from_volid($volid),
|
||||
'cpuCores' => 1,
|
||||
'memoryMb' => 1024,
|
||||
'diskGb' => 8,
|
||||
'status' => 'available',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $templates;
|
||||
}
|
||||
|
||||
public function clone_vm(array $input): array
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
* @return array{vm_id:int}
|
||||
*/
|
||||
private function clone_qemu_vm(array $input): array
|
||||
{
|
||||
$vm_id = (int) $this->request('/cluster/nextid', 'GET');
|
||||
$template_id = (int) $input['template_vm_id'];
|
||||
@@ -60,34 +194,46 @@ final class SPP_Http_Proxmox_Client implements SPP_Proxmox_Client
|
||||
$this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/config", 'PUT', [
|
||||
'cores' => (int) $input['cpu_cores'],
|
||||
'memory' => (int) $input['memory_mb'],
|
||||
'tags' => $this->tags_string($input['tags'] ?? []),
|
||||
]);
|
||||
|
||||
return ['vm_id' => $vm_id];
|
||||
}
|
||||
|
||||
public function start_vm(int $vm_id): void
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
* @return array{vm_id:int}
|
||||
*/
|
||||
private function create_lxc_container(array $input): array
|
||||
{
|
||||
$this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/status/start", 'POST');
|
||||
$rootfs_storage = $this->sanitize_proxmox_identifier((string) $this->options['lxc_rootfs_storage']);
|
||||
if ($rootfs_storage === '') {
|
||||
throw new RuntimeException('Missing LXC rootfs storage configuration.');
|
||||
}
|
||||
|
||||
$vm_id = (int) $this->request('/cluster/nextid', 'GET');
|
||||
$body = [
|
||||
'vmid' => $vm_id,
|
||||
'hostname' => $this->sanitize_hostname((string) $input['name']),
|
||||
'ostemplate' => (string) $input['lxc_template_ref'],
|
||||
'cores' => (int) $input['cpu_cores'],
|
||||
'memory' => (int) $input['memory_mb'],
|
||||
'rootfs' => $rootfs_storage . ':' . max(1, (int) $input['disk_gb']),
|
||||
'unprivileged' => 1,
|
||||
'tags' => $this->tags_string($input['tags'] ?? []),
|
||||
];
|
||||
|
||||
$bridge = $this->sanitize_proxmox_identifier((string) $this->options['lxc_bridge']);
|
||||
if ($bridge !== '') {
|
||||
$body['net0'] = 'name=eth0,bridge=' . $bridge . ',ip=dhcp';
|
||||
}
|
||||
|
||||
$this->request("/nodes/{$this->options['node']}/lxc", 'POST', $body);
|
||||
|
||||
return ['vm_id' => $vm_id];
|
||||
}
|
||||
|
||||
public function stop_vm(int $vm_id): void
|
||||
{
|
||||
$this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/status/stop", 'POST');
|
||||
}
|
||||
|
||||
public function delete_vm(int $vm_id): void
|
||||
{
|
||||
$this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}", 'DELETE');
|
||||
}
|
||||
|
||||
public function get_status(int $vm_id): string
|
||||
{
|
||||
$data = $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/status/current", 'GET');
|
||||
|
||||
return is_array($data) && isset($data['status']) ? (string) $data['status'] : 'unknown';
|
||||
}
|
||||
|
||||
public function get_ip_addresses(int $vm_id): array
|
||||
private function get_qemu_ip_addresses(int $vm_id): array
|
||||
{
|
||||
$data = $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/agent/network-get-interfaces", 'GET');
|
||||
$interfaces = is_array($data) && isset($data['result']) && is_array($data['result']) ? $data['result'] : [];
|
||||
@@ -115,6 +261,91 @@ final class SPP_Http_Proxmox_Client implements SPP_Proxmox_Client
|
||||
return array_values(array_unique($ips));
|
||||
}
|
||||
|
||||
private function get_lxc_ip_addresses(int $vm_id): array
|
||||
{
|
||||
$data = $this->request("/nodes/{$this->options['node']}/lxc/{$vm_id}/interfaces", 'GET');
|
||||
$interfaces = is_array($data) ? $data : [];
|
||||
$ips = [];
|
||||
|
||||
foreach ($interfaces as $interface) {
|
||||
if (!is_array($interface)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (['inet', 'inet6'] as $key) {
|
||||
if (empty($interface[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ip = preg_replace('/\/\d+$/', '', (string) $interface[$key]);
|
||||
if ($ip === null || $ip === '127.0.0.1' || $ip === '::1' || str_starts_with($ip, 'fe80:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ips[] = $ip;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($ips));
|
||||
}
|
||||
|
||||
private function guest_path(string $type, int $vm_id): string
|
||||
{
|
||||
$guest_type = $this->normalise_type($type) === 'lxc' ? 'lxc' : 'qemu';
|
||||
|
||||
return "/nodes/{$this->options['node']}/{$guest_type}/{$vm_id}";
|
||||
}
|
||||
|
||||
private function normalise_type(string $type): string
|
||||
{
|
||||
return strtolower($type) === 'lxc' ? 'lxc' : 'qemu';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $tags
|
||||
*/
|
||||
private function tags_string($tags): string
|
||||
{
|
||||
if (!is_array($tags)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$clean = [];
|
||||
foreach ($tags as $tag) {
|
||||
$tag = strtolower(sanitize_title((string) $tag));
|
||||
if ($tag !== '') {
|
||||
$clean[] = substr($tag, 0, 40);
|
||||
}
|
||||
}
|
||||
|
||||
return implode(';', array_values(array_unique($clean)));
|
||||
}
|
||||
|
||||
private function sanitize_hostname(string $name): string
|
||||
{
|
||||
$hostname = strtolower(sanitize_title($name));
|
||||
$hostname = preg_replace('/[^a-z0-9-]/', '-', $hostname);
|
||||
$hostname = trim((string) $hostname, '-');
|
||||
|
||||
return substr($hostname !== '' ? $hostname : 'spp-lxc', 0, 63);
|
||||
}
|
||||
|
||||
private function sanitize_proxmox_identifier(string $value): string
|
||||
{
|
||||
$value = sanitize_text_field($value);
|
||||
$value = preg_replace('/[^A-Za-z0-9_.-]/', '', $value);
|
||||
|
||||
return $value === null ? '' : $value;
|
||||
}
|
||||
|
||||
private function template_name_from_volid(string $volid): string
|
||||
{
|
||||
$name = basename(str_replace('\\', '/', $volid));
|
||||
$name = preg_replace('/\.(tar\.zst|tar\.xz|tar\.gz|tar)$/', '', $name);
|
||||
|
||||
return $name !== null && $name !== '' ? $name : $volid;
|
||||
}
|
||||
|
||||
private function bytes_to_mb(int $bytes): int
|
||||
{
|
||||
if ($bytes < 1) {
|
||||
|
||||
@@ -10,7 +10,10 @@ final class SPP_Mock_Proxmox_Client implements SPP_Proxmox_Client
|
||||
{
|
||||
return [
|
||||
[
|
||||
'provisioningType' => 'qemu',
|
||||
'vmId' => 9001,
|
||||
'templateRef' => '',
|
||||
'storage' => '',
|
||||
'name' => 'Turnkey PBX Test Appliance',
|
||||
'cpuCores' => 2,
|
||||
'memoryMb' => 2048,
|
||||
@@ -18,7 +21,10 @@ final class SPP_Mock_Proxmox_Client implements SPP_Proxmox_Client
|
||||
'status' => 'stopped',
|
||||
],
|
||||
[
|
||||
'provisioningType' => 'qemu',
|
||||
'vmId' => 9002,
|
||||
'templateRef' => '',
|
||||
'storage' => '',
|
||||
'name' => 'Windows Support Client',
|
||||
'cpuCores' => 4,
|
||||
'memoryMb' => 8192,
|
||||
@@ -26,51 +32,80 @@ final class SPP_Mock_Proxmox_Client implements SPP_Proxmox_Client
|
||||
'status' => 'stopped',
|
||||
],
|
||||
[
|
||||
'provisioningType' => 'qemu',
|
||||
'vmId' => 9003,
|
||||
'templateRef' => '',
|
||||
'storage' => '',
|
||||
'name' => 'Linux Utility VM',
|
||||
'cpuCores' => 2,
|
||||
'memoryMb' => 2048,
|
||||
'diskGb' => 32,
|
||||
'status' => 'stopped',
|
||||
],
|
||||
[
|
||||
'provisioningType' => 'lxc',
|
||||
'vmId' => 0,
|
||||
'templateRef' => 'local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst',
|
||||
'storage' => 'local',
|
||||
'name' => 'debian-12-standard',
|
||||
'cpuCores' => 1,
|
||||
'memoryMb' => 1024,
|
||||
'diskGb' => 8,
|
||||
'status' => 'available',
|
||||
],
|
||||
[
|
||||
'provisioningType' => 'lxc',
|
||||
'vmId' => 0,
|
||||
'templateRef' => 'local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst',
|
||||
'storage' => 'local',
|
||||
'name' => 'ubuntu-24.04-standard',
|
||||
'cpuCores' => 1,
|
||||
'memoryMb' => 1024,
|
||||
'diskGb' => 8,
|
||||
'status' => 'available',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function clone_vm(array $input): array
|
||||
public function provision_instance(array $input): array
|
||||
{
|
||||
$next_id = (int) get_option('spp_mock_next_vm_id', 10000);
|
||||
update_option('spp_mock_next_vm_id', $next_id + 1, false);
|
||||
update_option('spp_mock_vm_status_' . $next_id, 'stopped', false);
|
||||
update_option('spp_mock_vm_type_' . $next_id, $this->normalise_type((string) ($input['provisioning_type'] ?? 'qemu')), false);
|
||||
update_option('spp_mock_vm_tags_' . $next_id, is_array($input['tags'] ?? null) ? array_values($input['tags']) : [], false);
|
||||
update_option('spp_mock_vm_ips_' . $next_id, [sprintf('192.0.2.%d', (($next_id - 10000) % 200) + 10)], false);
|
||||
|
||||
return ['vm_id' => $next_id];
|
||||
}
|
||||
|
||||
public function start_vm(int $vm_id): void
|
||||
public function start_instance(string $type, int $vm_id): void
|
||||
{
|
||||
$this->ensure_vm($vm_id);
|
||||
update_option('spp_mock_vm_status_' . $vm_id, 'running', false);
|
||||
}
|
||||
|
||||
public function stop_vm(int $vm_id): void
|
||||
public function stop_instance(string $type, int $vm_id): void
|
||||
{
|
||||
$this->ensure_vm($vm_id);
|
||||
update_option('spp_mock_vm_status_' . $vm_id, 'stopped', false);
|
||||
}
|
||||
|
||||
public function delete_vm(int $vm_id): void
|
||||
public function delete_instance(string $type, int $vm_id): void
|
||||
{
|
||||
$this->ensure_vm($vm_id);
|
||||
delete_option('spp_mock_vm_status_' . $vm_id);
|
||||
delete_option('spp_mock_vm_type_' . $vm_id);
|
||||
delete_option('spp_mock_vm_tags_' . $vm_id);
|
||||
delete_option('spp_mock_vm_ips_' . $vm_id);
|
||||
}
|
||||
|
||||
public function get_status(int $vm_id): string
|
||||
public function get_status(string $type, int $vm_id): string
|
||||
{
|
||||
return (string) get_option('spp_mock_vm_status_' . $vm_id, 'unknown');
|
||||
}
|
||||
|
||||
public function get_ip_addresses(int $vm_id): array
|
||||
public function get_ip_addresses(string $type, int $vm_id): array
|
||||
{
|
||||
$ips = get_option('spp_mock_vm_ips_' . $vm_id, []);
|
||||
|
||||
@@ -80,7 +115,12 @@ final class SPP_Mock_Proxmox_Client implements SPP_Proxmox_Client
|
||||
private function ensure_vm(int $vm_id): void
|
||||
{
|
||||
if (get_option('spp_mock_vm_status_' . $vm_id, null) === null) {
|
||||
throw new RuntimeException('Mock Proxmox VM does not exist.');
|
||||
throw new RuntimeException('Mock Proxmox resource does not exist.');
|
||||
}
|
||||
}
|
||||
|
||||
private function normalise_type(string $type): string
|
||||
{
|
||||
return strtolower($type) === 'lxc' ? 'lxc' : 'qemu';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ final class SPP_Plugin
|
||||
'token_id' => (string) get_option('spp_proxmox_token_id', ''),
|
||||
'token_secret' => (string) get_option('spp_proxmox_token_secret', ''),
|
||||
'node' => (string) get_option('spp_proxmox_node', ''),
|
||||
'lxc_rootfs_storage' => (string) get_option('spp_lxc_rootfs_storage', ''),
|
||||
'lxc_bridge' => (string) get_option('spp_lxc_bridge', 'vmbr0'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ final class SPP_Repository
|
||||
$wpdb->insert($table, [
|
||||
'name' => $name,
|
||||
'status' => 'STOPPED',
|
||||
'provisioning_type' => $this->normalise_template_type((string) ($template['provisioning_type'] ?? 'qemu')),
|
||||
'proxmox_vm_id' => $vm_id,
|
||||
'ip_addresses' => wp_json_encode(array_values($ip_addresses)),
|
||||
'expires_at' => $expires_at,
|
||||
@@ -169,6 +170,7 @@ final class SPP_Repository
|
||||
$deployment_id = (int) $wpdb->insert_id;
|
||||
$this->audit('DEPLOYMENT_CREATED', 'deployment', $deployment_id, $actor_id, [
|
||||
'template_id' => (int) $template['id'],
|
||||
'provisioning_type' => $this->normalise_template_type((string) ($template['provisioning_type'] ?? 'qemu')),
|
||||
'proxmox_vm_id' => $vm_id,
|
||||
'ip_addresses' => $ip_addresses,
|
||||
'ttl_hours' => $ttl_hours,
|
||||
@@ -439,15 +441,11 @@ final class SPP_Repository
|
||||
|
||||
$table = SPP_Activator::table('templates');
|
||||
$now = current_time('mysql');
|
||||
$template_key = $this->unique_template_key((string) $data['template_key'], (int) $data['proxmox_template_id']);
|
||||
$existing_id = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE proxmox_template_id = %d OR template_key = %s ORDER BY proxmox_template_id = %d DESC LIMIT 1",
|
||||
(int) $data['proxmox_template_id'],
|
||||
$template_key,
|
||||
(int) $data['proxmox_template_id']
|
||||
)
|
||||
);
|
||||
$provisioning_type = $this->normalise_template_type((string) ($data['provisioning_type'] ?? 'qemu'));
|
||||
$proxmox_template_id = $provisioning_type === 'qemu' ? absint($data['proxmox_template_id']) : 0;
|
||||
$proxmox_template_ref = $provisioning_type === 'lxc' ? sanitize_text_field((string) ($data['proxmox_template_ref'] ?? '')) : null;
|
||||
$template_key = $this->unique_template_key((string) $data['template_key'], $provisioning_type, $proxmox_template_id, (string) $proxmox_template_ref);
|
||||
$existing_id = $this->template_existing_id($provisioning_type, $proxmox_template_id, (string) $proxmox_template_ref, $template_key);
|
||||
|
||||
$row = [
|
||||
'template_key' => $template_key,
|
||||
@@ -458,7 +456,9 @@ final class SPP_Repository
|
||||
'memory_mb' => max(128, absint($data['memory_mb'])),
|
||||
'disk_gb' => max(1, absint($data['disk_gb'])),
|
||||
'default_ttl_hours' => max(1, min(720, absint($data['default_ttl_hours']))),
|
||||
'proxmox_template_id' => absint($data['proxmox_template_id']),
|
||||
'provisioning_type' => $provisioning_type,
|
||||
'proxmox_template_id' => $proxmox_template_id,
|
||||
'proxmox_template_ref' => $proxmox_template_ref,
|
||||
'is_active' => 1,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
@@ -474,7 +474,9 @@ final class SPP_Repository
|
||||
}
|
||||
|
||||
$this->audit($action, 'template', $template_id, $actor_id, [
|
||||
'provisioning_type' => (string) $row['provisioning_type'],
|
||||
'proxmox_template_id' => (int) $row['proxmox_template_id'],
|
||||
'proxmox_template_ref' => (string) ($row['proxmox_template_ref'] ?? ''),
|
||||
'name' => (string) $row['name'],
|
||||
]);
|
||||
|
||||
@@ -489,6 +491,9 @@ final class SPP_Repository
|
||||
global $wpdb;
|
||||
|
||||
$table = SPP_Activator::table('templates');
|
||||
$provisioning_type = $this->normalise_template_type((string) ($data['provisioning_type'] ?? 'qemu'));
|
||||
$proxmox_template_id = $provisioning_type === 'qemu' ? absint($data['proxmox_template_id']) : 0;
|
||||
$proxmox_template_ref = $provisioning_type === 'lxc' ? sanitize_text_field((string) ($data['proxmox_template_ref'] ?? '')) : null;
|
||||
$row = [
|
||||
'name' => sanitize_text_field((string) $data['name']),
|
||||
'description' => sanitize_textarea_field((string) $data['description']),
|
||||
@@ -497,7 +502,9 @@ final class SPP_Repository
|
||||
'memory_mb' => max(128, absint($data['memory_mb'])),
|
||||
'disk_gb' => max(1, absint($data['disk_gb'])),
|
||||
'default_ttl_hours' => max(1, min(720, absint($data['default_ttl_hours']))),
|
||||
'proxmox_template_id' => absint($data['proxmox_template_id']),
|
||||
'provisioning_type' => $provisioning_type,
|
||||
'proxmox_template_id' => $proxmox_template_id,
|
||||
'proxmox_template_ref' => $proxmox_template_ref,
|
||||
'is_active' => empty($data['is_active']) ? 0 : 1,
|
||||
'updated_at' => current_time('mysql'),
|
||||
];
|
||||
@@ -505,7 +512,9 @@ final class SPP_Repository
|
||||
$wpdb->update($table, $row, ['id' => $id]);
|
||||
|
||||
$this->audit('TEMPLATE_UPDATED', 'template', $id, $actor_id, [
|
||||
'provisioning_type' => (string) $row['provisioning_type'],
|
||||
'proxmox_template_id' => (int) $row['proxmox_template_id'],
|
||||
'proxmox_template_ref' => (string) ($row['proxmox_template_ref'] ?? ''),
|
||||
'name' => (string) $row['name'],
|
||||
'is_active' => (int) $row['is_active'],
|
||||
]);
|
||||
@@ -529,16 +538,28 @@ final class SPP_Repository
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function active_proxmox_template_ids(): array
|
||||
public function active_template_identity_keys(): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$table = SPP_Activator::table('templates');
|
||||
$ids = $wpdb->get_col("SELECT proxmox_template_id FROM {$table} WHERE is_active = 1");
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT provisioning_type, proxmox_template_id, proxmox_template_ref FROM {$table} WHERE is_active = 1",
|
||||
ARRAY_A
|
||||
);
|
||||
$keys = [];
|
||||
|
||||
return array_values(array_map('intval', is_array($ids) ? $ids : []));
|
||||
foreach (is_array($rows) ? $rows : [] as $row) {
|
||||
$keys[] = $this->template_identity_key(
|
||||
(string) ($row['provisioning_type'] ?? 'qemu'),
|
||||
(int) ($row['proxmox_template_id'] ?? 0),
|
||||
(string) ($row['proxmox_template_ref'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
return array_values(array_unique($keys));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -617,17 +638,58 @@ final class SPP_Repository
|
||||
return is_array($row) ? $this->template_dto($row) : null;
|
||||
}
|
||||
|
||||
private function unique_template_key(string $raw_key, int $proxmox_template_id): string
|
||||
private function unique_template_key(string $raw_key, string $provisioning_type, int $proxmox_template_id, string $proxmox_template_ref): string
|
||||
{
|
||||
$key = sanitize_title($raw_key);
|
||||
|
||||
if ($key === '') {
|
||||
$key = 'pve-template-' . $proxmox_template_id;
|
||||
$key = $this->template_identity_key($provisioning_type, $proxmox_template_id, $proxmox_template_ref);
|
||||
}
|
||||
|
||||
return substr($key, 0, 80);
|
||||
}
|
||||
|
||||
private function template_existing_id(string $provisioning_type, int $proxmox_template_id, string $proxmox_template_ref, string $template_key): int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$table = SPP_Activator::table('templates');
|
||||
|
||||
if ($provisioning_type === 'lxc') {
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table}
|
||||
WHERE (provisioning_type = 'lxc' AND proxmox_template_ref = %s) OR template_key = %s
|
||||
LIMIT 1",
|
||||
$proxmox_template_ref,
|
||||
$template_key
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table}
|
||||
WHERE (provisioning_type = 'qemu' AND proxmox_template_id = %d) OR template_key = %s
|
||||
LIMIT 1",
|
||||
$proxmox_template_id,
|
||||
$template_key
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function template_identity_key(string $provisioning_type, int $proxmox_template_id, string $proxmox_template_ref): string
|
||||
{
|
||||
return $this->normalise_template_type($provisioning_type) === 'lxc'
|
||||
? 'lxc:' . $proxmox_template_ref
|
||||
: 'qemu:' . $proxmox_template_id;
|
||||
}
|
||||
|
||||
private function normalise_template_type(string $type): string
|
||||
{
|
||||
return strtolower($type) === 'lxc' ? 'lxc' : 'qemu';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $metadata
|
||||
*/
|
||||
@@ -661,7 +723,9 @@ final class SPP_Repository
|
||||
'memoryMb' => (int) $row['memory_mb'],
|
||||
'diskGb' => (int) $row['disk_gb'],
|
||||
'defaultTtlHours' => (int) $row['default_ttl_hours'],
|
||||
'provisioningType' => $this->normalise_template_type((string) ($row['provisioning_type'] ?? 'qemu')),
|
||||
'proxmoxTemplateId' => (int) $row['proxmox_template_id'],
|
||||
'proxmoxTemplateRef' => (string) ($row['proxmox_template_ref'] ?? ''),
|
||||
'isActive' => (int) $row['is_active'] === 1,
|
||||
];
|
||||
}
|
||||
@@ -676,6 +740,7 @@ final class SPP_Repository
|
||||
'id' => (int) $row['id'],
|
||||
'name' => (string) $row['name'],
|
||||
'status' => (string) $row['status'],
|
||||
'provisioningType' => $this->normalise_template_type((string) ($row['provisioning_type'] ?? 'qemu')),
|
||||
'templateName' => (string) $row['template_name'],
|
||||
'requestedById' => (int) $row['requested_by'],
|
||||
'requestedByName' => (string) $row['requested_by_name'],
|
||||
@@ -695,6 +760,8 @@ final class SPP_Repository
|
||||
return array_merge($this->deployment_summary_dto($row), [
|
||||
'templateId' => (int) $row['template_id'],
|
||||
'proxmoxVmId' => isset($row['proxmox_vm_id']) ? (int) $row['proxmox_vm_id'] : null,
|
||||
'proxmoxResourceType' => $this->normalise_template_type((string) ($row['provisioning_type'] ?? 'qemu')),
|
||||
'proxmoxResourceId' => isset($row['proxmox_vm_id']) ? (int) $row['proxmox_vm_id'] : null,
|
||||
'cpuCores' => (int) $row['cpu_cores'],
|
||||
'memoryMb' => (int) $row['memory_mb'],
|
||||
'diskGb' => (int) $row['disk_gb'],
|
||||
|
||||
@@ -222,22 +222,27 @@ final class SPP_REST_Controller
|
||||
return $quota_error;
|
||||
}
|
||||
|
||||
$provisioning_type = $this->normalise_template_type((string) ($template['provisioning_type'] ?? 'qemu'));
|
||||
|
||||
try {
|
||||
$clone = $this->proxmox->clone_vm([
|
||||
$instance = $this->proxmox->provision_instance([
|
||||
'provisioning_type' => $provisioning_type,
|
||||
'template_vm_id' => (int) $template['proxmox_template_id'],
|
||||
'lxc_template_ref' => (string) ($template['proxmox_template_ref'] ?? ''),
|
||||
'name' => $name,
|
||||
'cpu_cores' => (int) $template['cpu_cores'],
|
||||
'memory_mb' => (int) $template['memory_mb'],
|
||||
'disk_gb' => (int) $template['disk_gb'],
|
||||
'tags' => $this->deployment_tags(wp_get_current_user()),
|
||||
]);
|
||||
|
||||
$vm_id = (int) $clone['vm_id'];
|
||||
$vm_id = (int) $instance['vm_id'];
|
||||
$deployment = $this->repository->create_deployment(
|
||||
$template,
|
||||
$name,
|
||||
$ttl_hours,
|
||||
$vm_id,
|
||||
$this->safe_ip_addresses($vm_id),
|
||||
$this->safe_ip_addresses($provisioning_type, $vm_id),
|
||||
get_current_user_id()
|
||||
);
|
||||
} catch (Throwable $error) {
|
||||
@@ -278,17 +283,17 @@ final class SPP_REST_Controller
|
||||
|
||||
public function start_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
{
|
||||
return $this->apply_lifecycle_action((int) $request['id'], 'RUNNING', 'DEPLOYMENT_STARTED', 'start_vm');
|
||||
return $this->apply_lifecycle_action((int) $request['id'], 'RUNNING', 'DEPLOYMENT_STARTED', 'start');
|
||||
}
|
||||
|
||||
public function stop_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
{
|
||||
return $this->apply_lifecycle_action((int) $request['id'], 'STOPPED', 'DEPLOYMENT_STOPPED', 'stop_vm');
|
||||
return $this->apply_lifecycle_action((int) $request['id'], 'STOPPED', 'DEPLOYMENT_STOPPED', 'stop');
|
||||
}
|
||||
|
||||
public function delete_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
{
|
||||
return $this->apply_lifecycle_action((int) $request['id'], 'DELETED', 'DEPLOYMENT_DELETED', 'delete_vm');
|
||||
return $this->apply_lifecycle_action((int) $request['id'], 'DELETED', 'DEPLOYMENT_DELETED', 'delete');
|
||||
}
|
||||
|
||||
public function prolong_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
@@ -351,13 +356,16 @@ final class SPP_REST_Controller
|
||||
}
|
||||
|
||||
if (empty($record['proxmox_vm_id'])) {
|
||||
return new WP_Error('spp_missing_vm_id', 'Deployment is missing a Proxmox VM id.', ['status' => 409]);
|
||||
return new WP_Error('spp_missing_vm_id', 'Deployment is missing a Proxmox resource id.', ['status' => 409]);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->update_deployment_ips(
|
||||
$id,
|
||||
$this->proxmox->get_ip_addresses((int) $record['proxmox_vm_id']),
|
||||
$this->proxmox->get_ip_addresses(
|
||||
(string) ($record['provisioning_type'] ?? 'qemu'),
|
||||
(int) $record['proxmox_vm_id']
|
||||
),
|
||||
get_current_user_id()
|
||||
);
|
||||
} catch (Throwable $error) {
|
||||
@@ -442,11 +450,11 @@ final class SPP_REST_Controller
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
if ($method === 'delete_vm' && !$this->user_can_delete_deployment($id)) {
|
||||
if ($method === 'delete' && !$this->user_can_delete_deployment($id)) {
|
||||
return new WP_Error('spp_forbidden', 'Only the owner or a deployment manager can delete this deployment.', ['status' => 403]);
|
||||
}
|
||||
|
||||
if ($method === 'start_vm' && $record['status'] === 'EXPIRED') {
|
||||
if ($method === 'start' && $record['status'] === 'EXPIRED') {
|
||||
return new WP_Error(
|
||||
'spp_expired_deployment',
|
||||
'This deployment is expired. Prolong its TTL before starting it again.',
|
||||
@@ -455,27 +463,33 @@ final class SPP_REST_Controller
|
||||
}
|
||||
|
||||
if (empty($record['proxmox_vm_id'])) {
|
||||
return new WP_Error('spp_missing_vm_id', 'Deployment is missing a Proxmox VM id.', ['status' => 409]);
|
||||
return new WP_Error('spp_missing_vm_id', 'Deployment is missing a Proxmox resource id.', ['status' => 409]);
|
||||
}
|
||||
|
||||
$provisioning_type = $this->normalise_template_type((string) ($record['provisioning_type'] ?? 'qemu'));
|
||||
|
||||
try {
|
||||
$this->proxmox->{$method}((int) $record['proxmox_vm_id']);
|
||||
if ($method === 'start_vm') {
|
||||
if ($method === 'start') {
|
||||
$this->proxmox->start_instance($provisioning_type, (int) $record['proxmox_vm_id']);
|
||||
$this->repository->update_deployment_status_and_ips(
|
||||
$id,
|
||||
$status,
|
||||
$this->safe_ip_addresses((int) $record['proxmox_vm_id']),
|
||||
$this->safe_ip_addresses($provisioning_type, (int) $record['proxmox_vm_id']),
|
||||
$audit_action,
|
||||
get_current_user_id()
|
||||
);
|
||||
} elseif ($method === 'stop') {
|
||||
$this->proxmox->stop_instance($provisioning_type, (int) $record['proxmox_vm_id']);
|
||||
$this->repository->update_deployment_status($id, $status, $audit_action, get_current_user_id());
|
||||
} else {
|
||||
$this->proxmox->delete_instance($provisioning_type, (int) $record['proxmox_vm_id']);
|
||||
$this->repository->update_deployment_status($id, $status, $audit_action, get_current_user_id());
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
return new WP_Error('spp_proxmox_error', $error->getMessage(), ['status' => 502]);
|
||||
}
|
||||
|
||||
if ($method === 'delete_vm') {
|
||||
if ($method === 'delete') {
|
||||
return rest_ensure_response(['deleted' => true]);
|
||||
}
|
||||
|
||||
@@ -553,12 +567,32 @@ final class SPP_REST_Controller
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function safe_ip_addresses(int $vm_id): array
|
||||
private function safe_ip_addresses(string $provisioning_type, int $vm_id): array
|
||||
{
|
||||
try {
|
||||
return $this->proxmox->get_ip_addresses($vm_id);
|
||||
return $this->proxmox->get_ip_addresses($provisioning_type, $vm_id);
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function deployment_tags(WP_User $user): array
|
||||
{
|
||||
$login = $user->user_login !== '' ? $user->user_login : $user->display_name;
|
||||
$user_tag = strtolower(sanitize_title($login));
|
||||
|
||||
if ($user_tag === '') {
|
||||
$user_tag = (string) $user->ID;
|
||||
}
|
||||
|
||||
return ['support-portal', 'user-' . $user_tag];
|
||||
}
|
||||
|
||||
private function normalise_template_type(string $type): string
|
||||
{
|
||||
return strtolower($type) === 'lxc' ? 'lxc' : 'qemu';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,26 @@ if (!defined('ABSPATH')) {
|
||||
interface SPP_Proxmox_Client
|
||||
{
|
||||
/**
|
||||
* @return array<int, array{vmId:int,name:string,cpuCores:int,memoryMb:int,diskGb:int,status:string}>
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function list_templates(): array;
|
||||
|
||||
/**
|
||||
* @param array<string, int|string> $input
|
||||
* @param array<string, mixed> $input
|
||||
* @return array{vm_id:int}
|
||||
*/
|
||||
public function clone_vm(array $input): array;
|
||||
public function provision_instance(array $input): array;
|
||||
|
||||
public function start_vm(int $vm_id): void;
|
||||
public function start_instance(string $type, int $vm_id): void;
|
||||
|
||||
public function stop_vm(int $vm_id): void;
|
||||
public function stop_instance(string $type, int $vm_id): void;
|
||||
|
||||
public function delete_vm(int $vm_id): void;
|
||||
public function delete_instance(string $type, int $vm_id): void;
|
||||
|
||||
public function get_status(int $vm_id): string;
|
||||
public function get_status(string $type, int $vm_id): string;
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function get_ip_addresses(int $vm_id): array;
|
||||
public function get_ip_addresses(string $type, int $vm_id): array;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Support Provisioning Portal
|
||||
* Description: Internal self-service portal for provisioning standardized Proxmox VE VMs.
|
||||
* Version: 0.6.0
|
||||
* Description: Internal self-service portal for provisioning standardized Proxmox VE VMs and LXC containers.
|
||||
* Version: 0.7.0
|
||||
* Author: Internal Support
|
||||
* Requires PHP: 8.0
|
||||
* Requires at least: 6.2
|
||||
@@ -13,7 +13,7 @@ if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define('SPP_VERSION', '0.6.0');
|
||||
define('SPP_VERSION', '0.7.0');
|
||||
define('SPP_PLUGIN_FILE', __FILE__);
|
||||
define('SPP_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('SPP_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
Reference in New Issue
Block a user