order->status !== CustomerHostingOrder::STATUS_APPROVED) { return; } $this->order->markAsProvisioning(); $product = $this->order->product; $queueItem = $this->getOrCreateQueueItem(); try { $queueItem->markAsProcessing(); if ($product->isSharedHosting()) { $this->provisionSharedHosting($capacityService, $resourcePolicyService, $sharedProvider, $queueItem); } elseif ($product->isVps()) { $this->provisionVps($contaboProvider, $serverOrders, $serverAgentBootstrap, $serverAgentInstaller, $queueItem); } elseif ($product->isDedicated()) { $this->provisionDedicated($contaboProvider, $serverOrders, $serverAgentBootstrap, $serverAgentInstaller, $queueItem); } else { throw new \Exception("Unknown product category: {$product->category}"); } $queueItem->markAsCompleted(); } catch (\Throwable $e) { if ($this->shouldHoldForAdmin($e)) { $message = $e->getMessage(); $queueItem->markAsFailed($message); $this->order->fresh()->holdForAdminProvisioning( $message, $e instanceof ContaboApiException ? $e->httpStatus : null, ); return; } $queueItem->markAsFailed($e->getMessage()); if ($queueItem->canRetry()) { $this->release($this->backoff); } else { $this->order->markAsFailed($e->getMessage()); } throw $e; } } private function shouldHoldForAdmin(\Throwable $e): bool { if ($e instanceof ContaboApiException) { return $e->isPaymentRequired(); } $product = $this->order->product; if ($product?->requiresContabo() !== true) { return false; } return ContaboApiException::messageIndicatesPaymentRequired($e->getMessage()); } private function provisionSharedHosting( NodeCapacityService $capacityService, HostingResourcePolicyService $resourcePolicyService, SharedNodeProvider $sharedProvider, ProvisioningQueueItem $queueItem ): void { $queueItem->addLog('Finding available shared hosting node...'); $product = $this->order->product; $requestedDiskGb = (int) ($product->disk_gb ?? 0); $segment = $product->preferredNodeSegment(); $node = $capacityService->findAvailableNodeForProduct($product); if (!$node) { throw new \Exception('No available shared hosting nodes. Please provision a new node.'); } $queueItem->addLog("Selected node: {$node->name} ({$node->ip_address}) [segment={$segment}, requested_disk={$requestedDiskGb}GB]"); // Generate username $username = $this->generateUsername($this->order->domain_name); $password = Str::password(16, true, true, false, false); $queueItem->addLog("Creating account: {$username}"); $resourceLimits = $resourcePolicyService->defaultLimitsForProduct($product); // Create account on node via SSH $result = $sharedProvider->createAccountOnNode($node, [ 'username' => $username, 'password' => $password, 'domain' => $this->order->domain_name, 'email' => $this->order->user->email ?: $this->order->guest_email, 'disk_quota_mb' => max($requestedDiskGb, 1) * 1024, 'bandwidth_mb' => (($product->bandwidth_gb ?? 100) > 0 ? $product->bandwidth_gb : 100) * 1024, ]); // Create hosting account record $primaryDomain = \App\Services\Hosting\PlaceholderPrimaryDomainService::isPlaceholderDomain($this->order->domain_name) ? null : $this->order->domain_name; $account = HostingAccount::create([ 'user_id' => $this->order->user_id, 'hosting_node_id' => $node->id, 'hosting_product_id' => $product->id, 'username' => $username, 'primary_domain' => $primaryDomain, 'type' => HostingAccount::TYPE_SHARED, 'status' => HostingAccount::STATUS_ACTIVE, 'home_directory' => $result['home_directory'] ?? "/home/{$username}", 'allocated_disk_gb' => $requestedDiskGb, 'disk_used_bytes' => 0, 'bandwidth_used_bytes' => 0, 'cpu_limit_percent' => $resourceLimits['cpu_limit_percent'], 'memory_limit_mb' => $resourceLimits['memory_limit_mb'], 'process_limit' => $resourceLimits['process_limit'], 'io_limit_mb' => $resourceLimits['io_limit_mb'], 'inode_limit' => $resourceLimits['inode_limit'], 'resource_status' => HostingAccount::RESOURCE_STATUS_ACTIVE, 'uploads_restricted' => false, 'is_flagged' => false, 'warning_count' => 0, 'suspended_at' => null, 'provisioned_at' => now(), 'expires_at' => $this->order->calculateExpiryDate(), 'metadata' => [ 'assigned_duration_months' => $this->order->getCycleLengthInMonths(), 'initial_password' => $password, 'provisioned_from_order_id' => $this->order->id, 'provisioned_on_node' => $node->name, ], 'resource_limits' => array_merge([ 'disk_gb' => $requestedDiskGb, 'bandwidth_gb' => $product->bandwidth_gb, 'max_domains' => $product->max_domains, 'max_databases' => $product->max_databases, ], $resourceLimits), ]); if ($primaryDomain) { HostedSite::create([ 'hosting_account_id' => $account->id, 'domain_id' => $this->order->domain_id, 'domain' => $primaryDomain, 'document_root' => ($result['document_root'] ?? "/home/{$username}/public_html"), 'type' => 'primary', 'status' => 'active', 'php_version' => '8.4', 'ssl_enabled' => false, ]); } $sharedProvider->applyAccountResourceProfile($account, false); $node = $capacityService->refreshNodeResourceTotals($node); $capacityService->checkNode($node); // Install WordPress if it's a WordPress hosting plan $wpCredentials = []; if ($product->type === 'wordpress') { $queueItem->addLog('Installing WordPress...'); $docRoot = $result['document_root'] ?? "/home/{$username}/public_html"; $wpCredentials = $sharedProvider->installWordPress($node, $username, $this->order->domain_name, [ 'document_root' => $docRoot, 'admin_email' => $this->order->user->email ?: $this->order->guest_email, 'site_title' => $this->order->domain_name, ]); } $queueItem->addLog('Provisioning completed successfully.'); // Update order with provisioning details $orderMeta = array_merge($this->order->meta ?? [], [ 'provisioned_on_node' => $node->name, 'node_segment' => $node->segment, 'requested_disk_gb' => $requestedDiskGb, 'initial_password' => $password, ]); // Add WordPress credentials if installed if (!empty($wpCredentials)) { $orderMeta['wp_admin_user'] = $wpCredentials['admin_user'] ?? 'admin'; $orderMeta['wp_admin_password'] = $wpCredentials['admin_password'] ?? null; $orderMeta['wp_admin_email'] = $wpCredentials['admin_email'] ?? null; $orderMeta['wp_db_name'] = $wpCredentials['db_name'] ?? null; $orderMeta['wp_db_user'] = $wpCredentials['db_user'] ?? null; $orderMeta['wp_db_password'] = $wpCredentials['db_password'] ?? null; } $this->order->markAsActive([ 'hosting_node_id' => $node->id, 'hosting_account_id' => $account->id, 'server_username' => $username, 'server_ip' => $node->ip_address, 'control_panel_url' => "https://{$this->order->domain_name}/cpanel", 'expires_at' => $this->order->calculateExpiryDate(), 'meta' => $orderMeta, ]); } private function provisionVps(ContaboProvider $contaboProvider, ServerOrderService $serverOrders, ServerAgentBootstrapService $serverAgentBootstrap, ServerAgentInstallerService $serverAgentInstaller, ProvisioningQueueItem $queueItem): void { $queueItem->addLog('Provisioning VPS via Contabo API...'); $product = $this->order->product; if (!$product->contabo_product_id) { throw new \Exception('Product does not have a Contabo product ID configured.'); } $provisioning = $this->serverProvisioningConfig($serverOrders, 'VPS'); if ((bool) ($provisioning['panel_snapshot']['managed_stack_candidate'] ?? false)) { $bootstrap = $serverAgentBootstrap->ensureBootstrap($this->order->fresh()); $provisioning = $serverAgentInstaller->augmentProvisioningConfig($this->order->fresh(), $provisioning, $bootstrap); $queueItem->addLog('Managed stack bootstrap and Ladill server agent install prepared.'); } $result = $contaboProvider->createInstance($provisioning); $queueItem->addLog("VPS created with instance ID: {$result['instance_id']}"); foreach ($provisioning['manual_follow_up'] ?? [] as $item) { $queueItem->addLog("Manual follow-up required for {$item}."); } $this->order->markAsActive([ 'server_ip' => $result['ip_address'] ?? null, 'server_username' => (string) ($provisioning['default_user'] ?? 'root'), 'control_panel_url' => $this->serverControlPanelUrl($provisioning), 'expires_at' => $this->order->calculateExpiryDate(), 'meta' => array_merge($this->order->meta ?? [], [ 'contabo_instance_id' => $result['instance_id'], 'server_panel_runtime' => $provisioning['panel_snapshot'] ?? data_get($this->order->meta, 'server_panel_runtime', []), 'server_panel' => [ 'panel' => $provisioning['panel'] ?? null, 'instance_status' => $result['status'] ?? null, 'power_status' => 'on', 'region' => $result['region'] ?? null, 'datacenter' => $result['datacenter'] ?? null, 'image' => $result['image'] ?? null, 'last_synced_at' => now()->toIso8601String(), ], ]), ]); if ((bool) ($provisioning['panel_snapshot']['managed_stack_candidate'] ?? false)) { $serverAgentBootstrap->ensureBootstrap($this->order->fresh()); } } private function provisionDedicated(ContaboProvider $contaboProvider, ServerOrderService $serverOrders, ServerAgentBootstrapService $serverAgentBootstrap, ServerAgentInstallerService $serverAgentInstaller, ProvisioningQueueItem $queueItem): void { $queueItem->addLog('Provisioning Dedicated Server via Contabo API...'); $product = $this->order->product; if (!$product->contabo_product_id) { throw new \Exception('Product does not have a Contabo product ID configured.'); } $provisioning = $this->serverProvisioningConfig($serverOrders, 'Dedicated'); if ((bool) ($provisioning['panel_snapshot']['managed_stack_candidate'] ?? false)) { $bootstrap = $serverAgentBootstrap->ensureBootstrap($this->order->fresh()); $provisioning = $serverAgentInstaller->augmentProvisioningConfig($this->order->fresh(), $provisioning, $bootstrap); $queueItem->addLog('Managed stack bootstrap and Ladill server agent install prepared.'); } $result = $contaboProvider->createInstance($provisioning); $queueItem->addLog("Dedicated server created with instance ID: {$result['instance_id']}"); foreach ($provisioning['manual_follow_up'] ?? [] as $item) { $queueItem->addLog("Manual follow-up required for {$item}."); } $this->order->markAsActive([ 'server_ip' => $result['ip_address'] ?? null, 'server_username' => (string) ($provisioning['default_user'] ?? 'root'), 'control_panel_url' => $this->serverControlPanelUrl($provisioning), 'expires_at' => $this->order->calculateExpiryDate(), 'meta' => array_merge($this->order->meta ?? [], [ 'contabo_instance_id' => $result['instance_id'], 'server_panel_runtime' => $provisioning['panel_snapshot'] ?? data_get($this->order->meta, 'server_panel_runtime', []), 'server_panel' => [ 'panel' => $provisioning['panel'] ?? null, 'instance_status' => $result['status'] ?? null, 'power_status' => 'on', 'region' => $result['region'] ?? null, 'datacenter' => $result['datacenter'] ?? null, 'image' => $result['image'] ?? null, 'last_synced_at' => now()->toIso8601String(), ], ]), ]); if ((bool) ($provisioning['panel_snapshot']['managed_stack_candidate'] ?? false)) { $serverAgentBootstrap->ensureBootstrap($this->order->fresh()); } } /** * @return array */ private function serverProvisioningConfig(ServerOrderService $serverOrders, string $prefix): array { $meta = (array) ($this->order->meta ?? []); $serverOrder = (array) ($meta['server_order'] ?? []); $selection = (array) ($serverOrder['selection'] ?? []); $passwordEncrypted = $serverOrder['server_password_encrypted'] ?? null; $config = $serverOrders->provisioningConfig( $this->order->product, $selection, "{$prefix}-{$this->order->id}-{$this->order->domain_name}", $this->order->billing_cycle ); if (is_string($passwordEncrypted) && $passwordEncrypted !== '') { $config['root_password'] = decrypt($passwordEncrypted); } return $config; } /** * @param array $provisioning */ private function serverControlPanelUrl(array $provisioning): ?string { if (($provisioning['panel'] ?? null) !== 'ladill') { return null; } return route('hosting.server-panel.show', $this->order); } private function getOrCreateQueueItem(): ProvisioningQueueItem { $existing = ProvisioningQueueItem::where('customer_hosting_order_id', $this->order->id)->first(); if ($existing) { return $existing; } $provider = match ($this->order->product->category) { 'shared' => ProvisioningQueueItem::PROVIDER_SHARED_NODE, 'vps' => ProvisioningQueueItem::PROVIDER_CONTABO_VPS, 'dedicated' => ProvisioningQueueItem::PROVIDER_CONTABO_DEDICATED, default => ProvisioningQueueItem::PROVIDER_SHARED_NODE, }; return ProvisioningQueueItem::create([ 'customer_hosting_order_id' => $this->order->id, 'status' => ProvisioningQueueItem::STATUS_QUEUED, 'provider' => $provider, ]); } private function generateUsername(string $domain): string { // Remove TLD and special characters $base = preg_replace('/[^a-z0-9]/', '', strtolower(explode('.', $domain)[0])); $base = substr($base, 0, 8); // Add random suffix to ensure uniqueness return $base . Str::random(4); } }