- 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:
Sven Steinert
2026-04-24 17:11:39 +02:00
parent 2c1949bf1e
commit 118809bfae
13 changed files with 672 additions and 126 deletions

View File

@@ -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,
],
];

View File

@@ -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>
*/

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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';
}
}

View File

@@ -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'),
]);
}

View File

@@ -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'],

View File

@@ -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';
}
}

View File

@@ -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;
}