- 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

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