- 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

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