mirror of
https://github.com/ConvoyPanel/panel.git
synced 2026-06-02 22:39:31 +08:00
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
188 lines
6.4 KiB
PHP
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);
|
|
}
|
|
}
|