diff --git a/app/Jobs/Server/UpdatePasswordJob.php b/app/Jobs/Server/UpdatePasswordJob.php index 92536937694..8271710ce80 100644 --- a/app/Jobs/Server/UpdatePasswordJob.php +++ b/app/Jobs/Server/UpdatePasswordJob.php @@ -16,10 +16,16 @@ class UpdatePasswordJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public int $tries = 3; + public int $tries = 15; public int $timeout = 10; + + public function backoff(): int + { + return 30; // Not blocking — Horizon waits asynchronously + } + public function __construct(protected int $serverId, protected string $password) { // diff --git a/app/Services/Servers/AllocationService.php b/app/Services/Servers/AllocationService.php index 761e871ca41..4ac133de746 100644 --- a/app/Services/Servers/AllocationService.php +++ b/app/Services/Servers/AllocationService.php @@ -109,10 +109,24 @@ public function setBootOrder(Server $server, array $disks) public function updateHardware(Server $server, int $cpu, int $memory) { - $payload = [ - 'cores' => $cpu, - 'memory' => $memory / 1048576, - ]; + $desiredCores = $cpu; + $desiredMemMiB = (int) ($memory / 1048576); + + $raw = collect($this->repository->setServer($server)->getConfig()); + $currentCores = $raw->where('key', '=', 'cores')->first()['value'] ?? null; + $currentMem = $raw->where('key', '=', 'memory')->first()['value'] ?? null; + + $payload = []; + if ((string) $currentCores !== (string) $desiredCores) { + $payload['cores'] = $desiredCores; + } + if ((string) $currentMem !== (string) $desiredMemMiB) { + $payload['memory'] = $desiredMemMiB; + } + + if (count($payload) === 0) { + return; + } return $this->repository->setServer($server)->update($payload); } diff --git a/app/Services/Servers/CloudinitService.php b/app/Services/Servers/CloudinitService.php index ea92dc4131e..308873df37e 100644 --- a/app/Services/Servers/CloudinitService.php +++ b/app/Services/Servers/CloudinitService.php @@ -37,11 +37,23 @@ public function getSSHKeys(Server $server): string */ public function updateHostname(Server $server, string $hostname) { - $this->configRepository->setServer($server)->update([ - 'name' => $hostname, - ]); + $raw = collect($this->configRepository->setServer($server)->getConfig()); + $currentName = $raw->where('key', '=', 'name')->first()['value'] ?? null; + $currentSearch = $raw->where('key', '=', 'searchdomain')->first()['value'] ?? null; - $this->configRepository->setServer($server)->update(['searchdomain' => $hostname]); + $payload = []; + if ($currentName !== $hostname) { + $payload['name'] = $hostname; + } + if ($currentSearch !== $hostname) { + $payload['searchdomain'] = $hostname; + } + + if (count($payload) === 0) { + return; + } + + $this->configRepository->setServer($server)->update($payload); } public function getNameservers(Server $server) @@ -120,8 +132,20 @@ public function updateIpConfig(Server $server, CloudinitAddressConfigData $addre $payload[] = 'gw6='.$ipv6->gateway; } - return $this->configRepository->setServer($server)->update([ - 'ipconfig0' => Arr::join($payload, ','), - ]); + $desired = Arr::join($payload, ','); + + $raw = collect($this->configRepository->setServer($server)->getConfig()); + $current = $raw->where('key', '=', 'ipconfig0')->first()['value'] ?? null; + + if ($current === $desired) { + return; + } + if (($desired === '' || $desired === null) && ($current === '' || $current === null)) return; + if ($desired === '' || $desired === null) { + return $this->configRepository->setServer($server)->update([ + 'delete' => 'ipconfig0', + ]); + } + return $this->configRepository->setServer($server)->update(['ipconfig0' => $desired]); } } diff --git a/app/Services/Servers/NetworkService.php b/app/Services/Servers/NetworkService.php index 44ff6893e22..07038c780f7 100644 --- a/app/Services/Servers/NetworkService.php +++ b/app/Services/Servers/NetworkService.php @@ -18,6 +18,68 @@ class NetworkService { + private function ensureNet0BaseConfig(Server $server, string $macAddress): void + { + $rawConfig = $this->allocationRepository->setServer($server)->getConfig(); + $net0 = collect($rawConfig)->where('key', '=', 'net0')->first(); + + $parsedConfig = $net0 ? $this->parseConfig($net0['value']) : []; + + $models = [ + 'e1000', 'e1000-82540em', 'e1000-82544gc', 'e1000-82545em', + 'e1000e', 'i82551', 'i82557b', 'i82559er', + 'ne2k_isa', 'ne2k_pci', 'pcnet', 'rtl8139', + 'virtio', 'vmxnet3', + ]; + + $modelFound = false; + foreach ($parsedConfig as $item) { + if (in_array($item->key, $models, true)) { + $item->value = $macAddress; + $modelFound = true; + break; + } + } + + if (!$modelFound) { + $parsedConfig[] = (object) ['key' => 'virtio', 'value' => $macAddress]; + } + + $bridgeFound = false; + foreach ($parsedConfig as $item) { + if ($item->key === 'bridge') { + $item->value = $server->node->network; + $bridgeFound = true; + break; + } + } + + if (!$bridgeFound) { + $parsedConfig[] = (object) ['key' => 'bridge', 'value' => $server->node->network]; + } + + $firewallFound = false; + foreach ($parsedConfig as $item) { + if ($item->key === 'firewall') { + $item->value = 1; + $firewallFound = true; + break; + } + } + + if (!$firewallFound) { + $parsedConfig[] = (object) ['key' => 'firewall', 'value' => 1]; + } + + $newConfig = implode(',', array_map(fn ($item) => "{$item->key}={$item->value}", $parsedConfig)); + + if ($net0 && $this->normalizeNetConfigForComparison($net0['value']) === $this->normalizeNetConfigForComparison($newConfig)) { + return; + } + + $this->allocationRepository->setServer($server)->update(['net0' => $newConfig]); + } + public function __construct( private AddressRepository $repository, private ProxmoxFirewallRepository $firewallRepository, @@ -67,8 +129,8 @@ public function getMacAddresses(Server $server, bool $eloquent = true, bool $pro if ($eloquent) { $addresses = $this->getAddresses($server); - $eloquentMacAddress = $addresses->ipv4->first( - )?->mac_address ?? $addresses->ipv6->first()?->mac_address; + $eloquentMacAddress = $addresses->ipv4->first() + ?->mac_address ?? $addresses->ipv6->first()?->mac_address; } if ($proxmox) { @@ -86,7 +148,7 @@ public function getMacAddresses(Server $server, bool $eloquent = true, bool $pro return MacAddressData::from([ 'eloquent' => $eloquentMacAddress ?? null, - 'proxmox' => $proxmoxMacAddress ?? null, + 'proxmox' => $proxmoxMacAddress ?? null, ]); } @@ -125,17 +187,15 @@ public function syncSettings(Server $server): void ]); $macAddress = $macAddresses->eloquent ?? $macAddresses->proxmox; - - $this->allocationRepository->setServer($server)->update( - ['net0' => "virtio={$macAddress},bridge={$server->node->network},firewall=1"], - ); + $this->ensureNet0BaseConfig($server, $macAddress); } public function updateRateLimit(Server $server, ?int $mebibytes = null): void { $macAddresses = $this->getMacAddresses($server, true, true); - $macAddress = $macAddresses->eloquent ?? $macAddresses->proxmox; - $rawConfig = $this->allocationRepository->setServer($server)->getConfig(); + $macAddress = $macAddresses->eloquent ?? $macAddresses->proxmox; + + $rawConfig = $this->allocationRepository->setServer($server)->getConfig(); $networkConfig = collect($rawConfig)->where('key', '=', 'net0')->first(); if (is_null($networkConfig)) { @@ -147,22 +207,19 @@ public function updateRateLimit(Server $server, ?int $mebibytes = null): void // List of possible models $models = ['e1000', 'e1000-82540em', 'e1000-82544gc', 'e1000-82545em', 'e1000e', 'i82551', 'i82557b', 'i82559er', 'ne2k_isa', 'ne2k_pci', 'pcnet', 'rtl8139', 'virtio', 'vmxnet3']; - // Update the model with the new MAC address $modelFound = false; foreach ($parsedConfig as $item) { - if (in_array($item->key, $models)) { + if (in_array($item->key, $models, true)) { $item->value = $macAddress; - $modelFound = true; + $modelFound = true; break; } } - // If no model key exists, add the default model with the MAC address if (!$modelFound) { $parsedConfig[] = (object) ['key' => 'virtio', 'value' => $macAddress]; } - // Update or create the bridge value $bridgeFound = false; foreach ($parsedConfig as $item) { if ($item->key === 'bridge') { @@ -176,7 +233,6 @@ public function updateRateLimit(Server $server, ?int $mebibytes = null): void $parsedConfig[] = (object) ['key' => 'bridge', 'value' => $server->node->network]; } - // Update or create the firewall key $firewallFound = false; foreach ($parsedConfig as $item) { if ($item->key === 'firewall') { @@ -190,12 +246,11 @@ public function updateRateLimit(Server $server, ?int $mebibytes = null): void $parsedConfig[] = (object) ['key' => 'firewall', 'value' => 1]; } - // Handle the rate limit if (is_null($mebibytes)) { - // Remove the 'rate' key if $mebibytes is null - $parsedConfig = array_filter($parsedConfig, fn ($item) => $item->key !== 'rate'); + $parsedConfig = array_values( + array_filter($parsedConfig, fn ($item) => $item->key !== 'rate') + ); } else { - // Add or update the 'rate' key $rateUpdated = false; foreach ($parsedConfig as $item) { if ($item->key === 'rate') { @@ -210,26 +265,54 @@ public function updateRateLimit(Server $server, ?int $mebibytes = null): void } } - // Rebuild the configuration string - $newConfig = implode(',', array_map(fn ($item) => "{$item->key}={$item->value}", $parsedConfig)); + $newConfig = implode( + ',', + array_map(fn ($item) => "{$item->key}={$item->value}", $parsedConfig) + ); + + if ( + $this->normalizeNetConfigForComparison($networkConfig['value']) === + $this->normalizeNetConfigForComparison($newConfig) + ) { + return; + } - // Update the Proxmox configuration $this->allocationRepository->setServer($server)->update(['net0' => $newConfig]); } + private function normalizeNetConfigForComparison(string $config): array + { + $parsed = $this->parseConfig($config); + + $normalized = []; + foreach ($parsed as $item) { + $key = strtolower(trim((string) $item->key)); + $value = trim((string) $item->value); + + if (preg_match('/^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i', $value)) { + $value = strtolower($value); + } + + $normalized[$key] = $value; + } + + ksort($normalized); + + return $normalized; + } + private function parseConfig(string $config): array { - // Split components by commas $components = explode(',', $config); - - // Array to hold the parsed objects $parsedObjects = []; foreach ($components as $component) { - // Split each component into key and value - [$key, $value] = explode('=', $component); + $component = trim($component); + if ($component === '') { + continue; + } - // Create an associative array (or object) for key-value pairs + [$key, $value] = array_pad(explode('=', $component, 2), 2, ''); $parsedObjects[] = (object) ['key' => $key, 'value' => $value]; }