- 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.
599 lines
22 KiB
PHP
599 lines
22 KiB
PHP
<?php
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
final class SPP_REST_Controller
|
|
{
|
|
private const NAMESPACE = 'support-provisioning/v1';
|
|
|
|
public function __construct(
|
|
private SPP_Repository $repository,
|
|
private SPP_Proxmox_Client $proxmox,
|
|
private SPP_Expiration_Service $expiration_service,
|
|
private SPP_Permissions $permissions
|
|
) {
|
|
}
|
|
|
|
public function register_hooks(): void
|
|
{
|
|
add_action('rest_api_init', [$this, 'register_routes']);
|
|
}
|
|
|
|
public function register_routes(): void
|
|
{
|
|
register_rest_route(self::NAMESPACE, '/templates', [
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [$this, 'list_templates'],
|
|
'permission_callback' => [$this, 'can_read'],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/quota', [
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [$this, 'get_quota'],
|
|
'permission_callback' => [$this, 'can_read'],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/deployments', [
|
|
[
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [$this, 'list_deployments'],
|
|
'permission_callback' => [$this, 'can_read'],
|
|
],
|
|
[
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'create_deployment'],
|
|
'permission_callback' => [$this, 'can_create_deployments'],
|
|
],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)', [
|
|
[
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [$this, 'get_deployment'],
|
|
'permission_callback' => [$this, 'can_read'],
|
|
],
|
|
[
|
|
'methods' => WP_REST_Server::DELETABLE,
|
|
'callback' => [$this, 'delete_deployment'],
|
|
'permission_callback' => [$this, 'can_delete_deployments'],
|
|
],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/shares', [
|
|
[
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [$this, 'list_deployment_shares'],
|
|
'permission_callback' => [$this, 'can_read'],
|
|
],
|
|
[
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'share_deployment'],
|
|
'permission_callback' => [$this, 'can_read'],
|
|
],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/shares/(?P<user_id>\d+)', [
|
|
'methods' => WP_REST_Server::DELETABLE,
|
|
'callback' => [$this, 'unshare_deployment'],
|
|
'permission_callback' => [$this, 'can_read'],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/start', [
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'start_deployment'],
|
|
'permission_callback' => [$this, 'can_start_deployments'],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/stop', [
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'stop_deployment'],
|
|
'permission_callback' => [$this, 'can_stop_deployments'],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/prolong', [
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'prolong_deployment'],
|
|
'permission_callback' => [$this, 'can_prolong_deployments'],
|
|
]);
|
|
|
|
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/refresh-ips', [
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'refresh_deployment_ips'],
|
|
'permission_callback' => [$this, 'can_refresh_deployment_ips'],
|
|
]);
|
|
}
|
|
|
|
public function can_read(): bool
|
|
{
|
|
return is_user_logged_in() && $this->permissions->current_user_has(SPP_Permissions::VIEW_PORTAL);
|
|
}
|
|
|
|
public function can_create_deployments(): bool
|
|
{
|
|
return $this->can_use_portal_action(SPP_Permissions::CREATE_DEPLOYMENTS);
|
|
}
|
|
|
|
public function can_start_deployments(): bool
|
|
{
|
|
return $this->can_use_portal_action(SPP_Permissions::START_DEPLOYMENTS);
|
|
}
|
|
|
|
public function can_stop_deployments(): bool
|
|
{
|
|
return $this->can_use_portal_action(SPP_Permissions::STOP_DEPLOYMENTS);
|
|
}
|
|
|
|
public function can_prolong_deployments(): bool
|
|
{
|
|
return $this->can_use_portal_action(SPP_Permissions::PROLONG_DEPLOYMENTS);
|
|
}
|
|
|
|
public function can_refresh_deployment_ips(): bool
|
|
{
|
|
return $this->can_use_portal_action(SPP_Permissions::REFRESH_DEPLOYMENT_IPS);
|
|
}
|
|
|
|
public function can_delete_deployments(): bool
|
|
{
|
|
return $this->can_use_portal_action(SPP_Permissions::DELETE_DEPLOYMENTS);
|
|
}
|
|
|
|
private function can_use_portal_action(string $permission): bool
|
|
{
|
|
return $this->can_read() && $this->permissions->current_user_has($permission);
|
|
}
|
|
|
|
private function can_manage_all_deployments(): bool
|
|
{
|
|
return $this->permissions->current_user_has(SPP_Permissions::MANAGE_ALL_DEPLOYMENTS);
|
|
}
|
|
|
|
public function list_templates(): WP_REST_Response
|
|
{
|
|
$this->sync_expirations();
|
|
|
|
return rest_ensure_response($this->repository->templates());
|
|
}
|
|
|
|
public function get_quota(): WP_REST_Response
|
|
{
|
|
$this->sync_expirations();
|
|
|
|
return rest_ensure_response($this->repository->quota(get_current_user_id()));
|
|
}
|
|
|
|
public function list_deployments(): WP_REST_Response
|
|
{
|
|
$this->sync_expirations();
|
|
|
|
return rest_ensure_response($this->repository->deployments(
|
|
get_current_user_id(),
|
|
$this->can_manage_all_deployments()
|
|
));
|
|
}
|
|
|
|
public function get_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
|
{
|
|
$this->sync_expirations();
|
|
$deployment = $this->repository->deployment_for_user(
|
|
(int) $request['id'],
|
|
get_current_user_id(),
|
|
$this->can_manage_all_deployments()
|
|
);
|
|
|
|
if ($deployment === null) {
|
|
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
|
}
|
|
|
|
return rest_ensure_response($this->deployment_response($deployment));
|
|
}
|
|
|
|
public function create_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
|
{
|
|
$this->sync_expirations();
|
|
$template_id = (int) $request->get_param('templateId');
|
|
$name = sanitize_text_field((string) $request->get_param('name'));
|
|
$ttl_hours = (int) $request->get_param('ttlHours');
|
|
$never_expire = (bool) $request->get_param('neverExpire');
|
|
|
|
if ($template_id < 1 || strlen($name) < 3 || strlen($name) > 160) {
|
|
return new WP_Error('spp_invalid_request', 'Template and a deployment name of 3-160 characters are required.', ['status' => 400]);
|
|
}
|
|
|
|
$template = $this->repository->template($template_id);
|
|
if ($template === null) {
|
|
return new WP_Error('spp_template_not_found', 'Template not found.', ['status' => 404]);
|
|
}
|
|
|
|
if ($never_expire) {
|
|
$ttl_hours = null;
|
|
} elseif ($ttl_hours < 1) {
|
|
$ttl_hours = (int) $template['default_ttl_hours'];
|
|
}
|
|
|
|
if ($ttl_hours !== null && $ttl_hours > 720) {
|
|
return new WP_Error('spp_ttl_too_long', 'TTL cannot exceed 720 hours.', ['status' => 400]);
|
|
}
|
|
|
|
$quota_error = $this->validate_memory_quota((int) $template['memory_mb'], get_current_user_id());
|
|
if ($quota_error instanceof WP_Error) {
|
|
return $quota_error;
|
|
}
|
|
|
|
$provisioning_type = $this->normalise_template_type((string) ($template['provisioning_type'] ?? 'qemu'));
|
|
|
|
try {
|
|
$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) $instance['vm_id'];
|
|
$deployment = $this->repository->create_deployment(
|
|
$template,
|
|
$name,
|
|
$ttl_hours,
|
|
$vm_id,
|
|
$this->safe_ip_addresses($provisioning_type, $vm_id),
|
|
get_current_user_id()
|
|
);
|
|
} catch (Throwable $error) {
|
|
return new WP_Error('spp_proxmox_error', $error->getMessage(), ['status' => 502]);
|
|
}
|
|
|
|
$deployment = $this->repository->deployment_for_user(
|
|
(int) $deployment['id'],
|
|
get_current_user_id(),
|
|
$this->can_manage_all_deployments()
|
|
);
|
|
|
|
return new WP_REST_Response($this->deployment_response($deployment), 201);
|
|
}
|
|
|
|
private function validate_memory_quota(int $requested_memory_mb, int $actor_id): ?WP_Error
|
|
{
|
|
$quota = $this->repository->quota($actor_id);
|
|
|
|
if ($quota['userLimitMb'] > 0 && ($quota['userUsedMb'] + $requested_memory_mb) > $quota['userLimitMb']) {
|
|
return new WP_Error(
|
|
'spp_user_quota_exceeded',
|
|
'This deployment would exceed your RAM contingent.',
|
|
['status' => 403, 'quota' => $quota]
|
|
);
|
|
}
|
|
|
|
if ($quota['globalLimitMb'] > 0 && ($quota['globalUsedMb'] + $requested_memory_mb) > $quota['globalLimitMb']) {
|
|
return new WP_Error(
|
|
'spp_global_quota_exceeded',
|
|
'This deployment would exceed the global RAM contingent.',
|
|
['status' => 403, 'quota' => $quota]
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
public function prolong_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
|
{
|
|
$this->sync_expirations();
|
|
$id = (int) $request['id'];
|
|
$record = $this->repository->deployment_record($id);
|
|
|
|
if ($record === null || $record['status'] === 'DELETED') {
|
|
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
|
}
|
|
|
|
if (!$this->repository->user_can_access_deployment($id, get_current_user_id(), $this->can_manage_all_deployments())) {
|
|
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
|
}
|
|
|
|
$ttl_hours = (int) $request->get_param('ttlHours');
|
|
$never_expire = (bool) $request->get_param('neverExpire');
|
|
$deployment = $this->repository->deployment_for_user($id, get_current_user_id(), $this->can_manage_all_deployments());
|
|
|
|
if ($deployment === null) {
|
|
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
|
}
|
|
|
|
if ($never_expire) {
|
|
$ttl_hours = null;
|
|
} elseif ($ttl_hours < 1) {
|
|
return new WP_Error('spp_invalid_ttl', 'Choose a TTL in hours or select never expire.', ['status' => 400]);
|
|
}
|
|
|
|
if ($ttl_hours !== null && $ttl_hours > 720) {
|
|
return new WP_Error('spp_ttl_too_long', 'TTL cannot exceed 720 hours.', ['status' => 400]);
|
|
}
|
|
|
|
if ($record['status'] === 'EXPIRED') {
|
|
$quota_error = $this->validate_memory_quota((int) $deployment['memoryMb'], (int) $record['requested_by']);
|
|
if ($quota_error instanceof WP_Error) {
|
|
return $quota_error;
|
|
}
|
|
}
|
|
|
|
$this->repository->prolong_deployment($id, $ttl_hours, get_current_user_id());
|
|
$deployment = $this->repository->deployment_for_user($id, get_current_user_id(), $this->can_manage_all_deployments());
|
|
|
|
return rest_ensure_response($this->deployment_response($deployment));
|
|
}
|
|
|
|
public function refresh_deployment_ips(WP_REST_Request $request): WP_REST_Response|WP_Error
|
|
{
|
|
$this->sync_expirations();
|
|
$id = (int) $request['id'];
|
|
$record = $this->repository->deployment_record($id);
|
|
|
|
if ($record === null || $record['status'] === 'DELETED') {
|
|
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
|
}
|
|
|
|
if (!$this->repository->user_can_access_deployment($id, get_current_user_id(), $this->can_manage_all_deployments())) {
|
|
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
|
}
|
|
|
|
if (empty($record['proxmox_vm_id'])) {
|
|
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(
|
|
(string) ($record['provisioning_type'] ?? 'qemu'),
|
|
(int) $record['proxmox_vm_id']
|
|
),
|
|
get_current_user_id()
|
|
);
|
|
} catch (Throwable $error) {
|
|
return new WP_Error('spp_proxmox_error', $error->getMessage(), ['status' => 502]);
|
|
}
|
|
|
|
$deployment = $this->repository->deployment_for_user($id, get_current_user_id(), $this->can_manage_all_deployments());
|
|
|
|
return rest_ensure_response($this->deployment_response($deployment));
|
|
}
|
|
|
|
public function list_deployment_shares(WP_REST_Request $request): WP_REST_Response|WP_Error
|
|
{
|
|
$this->sync_expirations();
|
|
$id = (int) $request['id'];
|
|
|
|
if (!$this->user_can_share_deployment($id)) {
|
|
return new WP_Error('spp_forbidden', 'Only the owner or a deployment manager can view shares.', ['status' => 403]);
|
|
}
|
|
|
|
return rest_ensure_response($this->repository->deployment_shares($id));
|
|
}
|
|
|
|
public function share_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
|
{
|
|
$this->sync_expirations();
|
|
$id = (int) $request['id'];
|
|
|
|
if (!$this->user_can_share_deployment($id)) {
|
|
return new WP_Error('spp_forbidden', 'Only the owner or a deployment manager can share this deployment.', ['status' => 403]);
|
|
}
|
|
|
|
$identifier = sanitize_text_field((string) $request->get_param('user'));
|
|
$target = $this->find_share_target_user($identifier);
|
|
|
|
if (!$target instanceof WP_User) {
|
|
return new WP_Error('spp_user_not_found', 'User not found.', ['status' => 404]);
|
|
}
|
|
|
|
if ((int) $target->ID === get_current_user_id()) {
|
|
return new WP_Error('spp_invalid_share_target', 'You already have access to this deployment.', ['status' => 400]);
|
|
}
|
|
|
|
$record = $this->repository->deployment_record($id);
|
|
if ($record === null || (int) $record['requested_by'] === (int) $target->ID) {
|
|
return new WP_Error('spp_invalid_share_target', 'The owner already has access to this deployment.', ['status' => 400]);
|
|
}
|
|
|
|
if (!$this->permissions->user_has((int) $target->ID, SPP_Permissions::VIEW_PORTAL)) {
|
|
return new WP_Error('spp_user_without_portal_access', 'That user does not have portal access yet.', ['status' => 400]);
|
|
}
|
|
|
|
$this->repository->share_deployment($id, (int) $target->ID, get_current_user_id());
|
|
|
|
return rest_ensure_response($this->repository->deployment_shares($id));
|
|
}
|
|
|
|
public function unshare_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
|
{
|
|
$this->sync_expirations();
|
|
$id = (int) $request['id'];
|
|
|
|
if (!$this->user_can_share_deployment($id)) {
|
|
return new WP_Error('spp_forbidden', 'Only the owner or a deployment manager can change shares.', ['status' => 403]);
|
|
}
|
|
|
|
$this->repository->unshare_deployment($id, (int) $request['user_id'], get_current_user_id());
|
|
|
|
return rest_ensure_response($this->repository->deployment_shares($id));
|
|
}
|
|
|
|
private function apply_lifecycle_action(int $id, string $status, string $audit_action, string $method): WP_REST_Response|WP_Error
|
|
{
|
|
$this->sync_expirations();
|
|
$record = $this->repository->deployment_record($id);
|
|
|
|
if ($record === null || $record['status'] === 'DELETED') {
|
|
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
|
}
|
|
|
|
if (!$this->repository->user_can_access_deployment($id, get_current_user_id(), $this->can_manage_all_deployments())) {
|
|
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
|
}
|
|
|
|
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' && $record['status'] === 'EXPIRED') {
|
|
return new WP_Error(
|
|
'spp_expired_deployment',
|
|
'This deployment is expired. Prolong its TTL before starting it again.',
|
|
['status' => 409]
|
|
);
|
|
}
|
|
|
|
if (empty($record['proxmox_vm_id'])) {
|
|
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 {
|
|
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($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') {
|
|
return rest_ensure_response(['deleted' => true]);
|
|
}
|
|
|
|
$deployment = $this->repository->deployment_for_user($id, get_current_user_id(), $this->can_manage_all_deployments());
|
|
|
|
return rest_ensure_response($this->deployment_response($deployment));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $deployment
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function deployment_response(?array $deployment): array
|
|
{
|
|
if ($deployment === null) {
|
|
return [];
|
|
}
|
|
|
|
$id = (int) $deployment['id'];
|
|
$deployment['canShare'] = $this->user_can_share_deployment($id);
|
|
$deployment['canDelete'] = $this->user_can_delete_deployment($id);
|
|
|
|
if ($deployment['canShare']) {
|
|
$deployment['shares'] = $this->repository->deployment_shares($id);
|
|
}
|
|
|
|
return $deployment;
|
|
}
|
|
|
|
private function user_can_share_deployment(int $deployment_id): bool
|
|
{
|
|
$record = $this->repository->deployment_record($deployment_id);
|
|
if ($record === null || $record['status'] === 'DELETED') {
|
|
return false;
|
|
}
|
|
|
|
return $this->repository->user_owns_deployment($deployment_id, get_current_user_id())
|
|
|| $this->can_manage_all_deployments();
|
|
}
|
|
|
|
private function user_can_delete_deployment(int $deployment_id): bool
|
|
{
|
|
$record = $this->repository->deployment_record($deployment_id);
|
|
if ($record === null || $record['status'] === 'DELETED') {
|
|
return false;
|
|
}
|
|
|
|
return $this->permissions->current_user_has(SPP_Permissions::DELETE_DEPLOYMENTS)
|
|
&& (
|
|
$this->repository->user_owns_deployment($deployment_id, get_current_user_id())
|
|
|| $this->can_manage_all_deployments()
|
|
);
|
|
}
|
|
|
|
private function find_share_target_user(string $identifier): ?WP_User
|
|
{
|
|
if ($identifier === '') {
|
|
return null;
|
|
}
|
|
|
|
if (is_email($identifier)) {
|
|
$user = get_user_by('email', $identifier);
|
|
} else {
|
|
$user = get_user_by('login', $identifier);
|
|
}
|
|
|
|
return $user instanceof WP_User ? $user : null;
|
|
}
|
|
|
|
private function sync_expirations(): void
|
|
{
|
|
$this->expiration_service->expire_due_deployments();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function safe_ip_addresses(string $provisioning_type, int $vm_id): array
|
|
{
|
|
try {
|
|
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';
|
|
}
|
|
}
|