Files
panel/app/Services/Servers/AllocationService.php
Eric Wang ac13cefcf4 fix: skip no-op Proxmox Configure tasks and increase password job retries
Port of PR #149 by ShiSHcat, refactored:
- NetworkService: extract NIC_MODELS constant, setConfigField() helper, and
  applyBaseNet0Fields() to eliminate the duplicated model/bridge/firewall
  update-or-insert pattern shared by ensureNet0BaseConfig() and updateRateLimit();
  add normalizeNetConfigForComparison() to skip no-op net0 writes; harden
  parseConfig() against empty components and values without '='
- AllocationService: skip updateHardware() call when cores/memory already match
- CloudinitService: skip updateHostname() and updateIpConfig() when values
  already match; delete ipconfig0 key when desired config is empty
- UpdatePasswordJob: raise tries to 15 with 30 s backoff to survive slow
  disk-resize operations during VM creation
2026-05-04 00:41:00 -04:00

188 lines
6.4 KiB
PHP

<?php
namespace Convoy\Services\Servers;
use Convoy\Models\ISO;
use Convoy\Models\Server;
use Illuminate\Support\Arr;
use Convoy\Enums\Server\DiskInterface;
use Convoy\Data\Server\Proxmox\Config\DiskData;
use Convoy\Repositories\Proxmox\Server\ProxmoxConfigRepository;
use Convoy\Exceptions\Service\Server\Allocation\IsoAlreadyMountedException;
use Convoy\Exceptions\Service\Server\Allocation\IsoAlreadyUnmountedException;
use Convoy\Exceptions\Service\Server\Allocation\NoAvailableDiskInterfaceException;
use Illuminate\Support\Collection;
class AllocationService
{
public function __construct(protected ProxmoxConfigRepository $repository)
{
}
public function syncSettings(Server $server)
{
return $this->updateHardware($server, $server->cpu, $server->memory);
}
/**
* @return Collection<int, DiskData>
*/
public function getDisks(Server $server): Collection
{
$isos = $server->node->isos;
$disks = array_values(array_filter($this->repository->setServer($server)->getConfig(), function ($disk) {
return in_array($disk['key'], array_column(DiskInterface::cases(), 'value'));
}));
return collect(Arr::map($disks, function ($rawDisk) use ($isos, $server) {
$disk = [
'interface' => DiskInterface::from(Arr::get($rawDisk, 'key')),
'is_primary_disk' => false,
'is_media' => false,
'media_name' => null,
'size' => 0,
];
$value = Arr::get($rawDisk, 'pending') ?? Arr::get($rawDisk, 'value');
preg_match("/size=(\d+\w?)/s", $value, $sizeMatches);
if (array_key_exists(1, $sizeMatches)) {
$disk['size'] = $this->convertToBytes($sizeMatches[1]);
}
if (str_contains($value, 'media')) {
$disk['is_media'] = true;
// this piece of code adds the name of the mounted ISO
if (preg_match("/\/(.*\.iso)/s", $value, $fileNameMatches)) {
if ($iso = $isos->where('file_name', $fileNameMatches[1])->first()) {
$disk['media_name'] = $iso->name;
}
} elseif (str_contains($value, 'cloudinit')) {
$disk['media_name'] = 'Cloudinit';
}
} else {
// if its not the ISO, we'll check if its the boot disk by comparing the size to the disk size on the eloquent record of the server
$upperBound = $server->disk + 1024;
$lowerBound = $server->disk - 1024;
if ($disk['size'] < $upperBound && $disk['size'] > $lowerBound) {
$disk['is_primary_disk'] = true;
}
}
return DiskData::from($disk);
}));
}
/**
* @return Collection<int, DiskData>
*/
public function getBootOrder(Server $server): Collection
{
$disks = $this->getDisks($server);
$raw = collect($this->repository->setServer($server)->getConfig())->where('key', 'boot')->firstOrFail();
$untaggedDisks = array_values(array_filter(explode(';', Arr::last(explode('=', $raw['pending'] ?? $raw['value']))), function ($disk) {
return ! ctype_space($disk) && in_array($disk, array_column(DiskInterface::cases(), 'value')); // filter literally whitespace entries because Proxmox keeps empty strings for some reason >:(
}));
$taggedDisks = [];
foreach ($untaggedDisks as $untaggedDisk) {
if ($disk = $disks->where('interface', '=', DiskInterface::from($untaggedDisk))->first()) {
array_push($taggedDisks, $disk);
}
}
return collect($taggedDisks);
}
public function setBootOrder(Server $server, array $disks)
{
return $this->repository->setServer($server)->update([
'boot' => count($disks) > 0 ? 'order='.Arr::join($disks, ';') : '',
]);
}
public function updateHardware(Server $server, int $cpu, int $memory)
{
$memMiB = (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) $cpu) {
$payload['cores'] = $cpu;
}
if ((string) $currentMem !== (string) $memMiB) {
$payload['memory'] = $memMiB;
}
if (empty($payload)) {
return;
}
return $this->repository->setServer($server)->update($payload);
}
public function mountIso(Server $server, ISO $iso)
{
// we'll be using IDE by default for now
$ideIndex = 0; // max IDE index is '3'
$disks = $this->getDisks($server);
if ($disks->where('media_name', '=', $iso->name)->first()) {
throw new IsoAlreadyMountedException();
}
$arrayToCheckForAvailableIdeIndex = Arr::pluck($this->repository->setServer($server)->getConfig(), 'key');
for ($i = 0; $i <= 4; $i++) {
if ($i === 4) {
throw new NoAvailableDiskInterfaceException();
}
if (! in_array("ide$i", $arrayToCheckForAvailableIdeIndex)) {
$ideIndex = $i;
break;
}
}
$this->repository->update([
"ide$ideIndex" => "{$server->node->iso_storage}:iso/{$iso->file_name},media=cdrom",
]);
}
public function unmountIso(Server $server, ISO $iso)
{
$disks = $this->getDisks($server);
if ($disk = $disks->where('media_name', '=', $iso->name)->first()) {
$this->repository->update(['delete' => $disk->interface->value]);
} else {
throw new IsoAlreadyUnmountedException();
}
}
public function convertToBytes(string $from): ?int
{
$units = ['B', 'K', 'M', 'G', 'T', 'P'];
$number = (int) substr($from, 0, -1);
$suffix = strtoupper(substr($from, -1));
//B or no suffix
if (is_numeric(substr($suffix, 0, 1))) {
return preg_replace('/[^\d]/', '', $from);
}
$exponent = array_flip($units)[$suffix] ?? null;
if ($exponent === null) {
return null;
}
return $number * (1024 ** $exponent);
}
}