232 lines
7.3 KiB
PHP
232 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Services;
|
|
|
|
use App\Helpers\ExtensionHelper;
|
|
use App\Livewire\Component;
|
|
use App\Models\Service;
|
|
use Paymenter\Extensions\Servers\Proxmox\Models\Server as ProxmoxServer;
|
|
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
|
use Livewire\Attributes\Locked;
|
|
use Livewire\Attributes\Url;
|
|
|
|
class Show extends Component
|
|
{
|
|
public Service $service;
|
|
|
|
public ?ProxmoxServer $proxmoxServer = null;
|
|
|
|
/**
|
|
* Actual VM status from Proxmox API (includes maxmem, maxdisk, cpus, etc.)
|
|
*/
|
|
public ?array $vmStatus = null;
|
|
|
|
#[Locked]
|
|
public $buttons = [];
|
|
|
|
#[Locked]
|
|
public $views = [];
|
|
|
|
#[Url('tab', except: false), Locked]
|
|
public $currentView;
|
|
|
|
#[Url('cancel', except: false)]
|
|
public bool $showCancel = false;
|
|
|
|
public function mount()
|
|
{
|
|
// Attach Proxmox server (if any) so we can show hostname on the service page
|
|
$this->proxmoxServer = ProxmoxServer::with(['os', 'node', 'ipAddresses'])->where('service_id', $this->service->id)->first();
|
|
|
|
// Fetch actual VM status from Proxmox API if server exists
|
|
if ($this->proxmoxServer) {
|
|
$this->loadVmStatus();
|
|
}
|
|
|
|
// Only fetch the actions if the service is active
|
|
if ($this->service->status == Service::STATUS_ACTIVE) {
|
|
$actions = [];
|
|
try {
|
|
$actions = ExtensionHelper::getActions($this->service);
|
|
} catch (\Exception $e) {
|
|
}
|
|
// separate the actions into buttons and views
|
|
foreach ($actions as $action) {
|
|
if ($action['type'] == 'button') {
|
|
$this->buttons[] = $action;
|
|
} elseif ($action['type'] == 'view') {
|
|
$this->views[] = $action;
|
|
}
|
|
}
|
|
$this->currentView = $this->currentView ?? ($this->views[0]['name'] ?? null);
|
|
}
|
|
}
|
|
|
|
public function changeView($view)
|
|
{
|
|
if (!$view) {
|
|
return;
|
|
}
|
|
if ($this->currentView === $view || !in_array($view, array_column($this->views, 'name'))) {
|
|
return $this->skipRender();
|
|
}
|
|
$this->currentView = $view;
|
|
}
|
|
|
|
public function updatedShowCancel($value)
|
|
{
|
|
if (!$this->service->cancellable) {
|
|
$this->notify('This service cannot be cancelled', 'error');
|
|
$this->showCancel = false;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
public function goto($function)
|
|
{
|
|
// Check if function is allowed
|
|
if (!in_array($function, array_column($this->buttons, 'function'))) {
|
|
$this->notify('This action is not allowed', 'error');
|
|
|
|
return;
|
|
}
|
|
$result = ExtensionHelper::callService($this->service, $function);
|
|
// If its a response, return it
|
|
if (!is_string($result)) {
|
|
return $result;
|
|
}
|
|
$this->redirect($result);
|
|
}
|
|
|
|
/**
|
|
* Load the actual VM status from Proxmox API.
|
|
*/
|
|
protected function loadVmStatus(): void
|
|
{
|
|
try {
|
|
if (!$this->proxmoxServer || !$this->proxmoxServer->node) {
|
|
return;
|
|
}
|
|
|
|
$proxmox = new Proxmox();
|
|
$data = $proxmox->request(
|
|
'/nodes/' . $this->proxmoxServer->node->name . '/qemu/' . $this->proxmoxServer->vm_id . '/status/current'
|
|
)->json()['data'] ?? null;
|
|
|
|
$this->vmStatus = $data;
|
|
|
|
// Also fetch VM config to calculate accurate total storage
|
|
// The status endpoint's maxdisk is often unreliable
|
|
if ($this->vmStatus) {
|
|
$config = $proxmox->request(
|
|
'/nodes/' . $this->proxmoxServer->node->name . '/qemu/' . $this->proxmoxServer->vm_id . '/config'
|
|
)->json()['data'] ?? null;
|
|
|
|
if ($config) {
|
|
$totalDiskGB = $this->calculateTotalStorage($config);
|
|
if ($totalDiskGB > 0) {
|
|
// Store as bytes for consistency with other values
|
|
$this->vmStatus['total_disk_bytes'] = $totalDiskGB * 1024 * 1024 * 1024;
|
|
}
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->vmStatus = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate total storage from VM config (excluding cloud-init, EFI, TPM).
|
|
* Returns size in GB.
|
|
*/
|
|
protected function calculateTotalStorage(array $config): float
|
|
{
|
|
$totalSize = 0;
|
|
|
|
foreach ($config as $key => $value) {
|
|
// Check for attached drives (ide, sata, scsi, virtio)
|
|
if (preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $key)) {
|
|
// Skip excluded disks (cloud-init, EFI, TPM)
|
|
if ($this->isExcludedDisk($value)) {
|
|
continue;
|
|
}
|
|
// Skip CD-ROM drives
|
|
if (str_contains($value, 'media=cdrom')) {
|
|
continue;
|
|
}
|
|
$totalSize += $this->extractDiskSize($value);
|
|
}
|
|
}
|
|
|
|
return $totalSize;
|
|
}
|
|
|
|
/**
|
|
* Check if a disk should be excluded (cloud-init, EFI, TPM).
|
|
*/
|
|
protected function isExcludedDisk(string $value): bool
|
|
{
|
|
$excludePatterns = ['cloudinit', 'vm-\d+-cloudinit', 'efidisk', 'tpmstate'];
|
|
foreach ($excludePatterns as $pattern) {
|
|
if (preg_match('/' . $pattern . '/i', $value)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Extract disk size in GB from a disk configuration string.
|
|
*/
|
|
protected function extractDiskSize(string $value): float
|
|
{
|
|
// Example: cephpool:vm-100-disk-0,size=32G,cache=none,ssd=1
|
|
$parts = explode(',', $value);
|
|
|
|
foreach ($parts as $part) {
|
|
if (preg_match('/^size=(\d+)([KMGT]?)$/i', $part, $sizeMatch)) {
|
|
$size = (float) $sizeMatch[1];
|
|
$unit = strtoupper($sizeMatch[2] ?? 'G');
|
|
|
|
switch ($unit) {
|
|
case 'K': return $size / (1024 * 1024);
|
|
case 'M': return $size / 1024;
|
|
case 'G':
|
|
case '': return $size;
|
|
case 'T': return $size * 1024;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$view = null;
|
|
$previousView = $this->currentView;
|
|
|
|
if ($this->currentView) {
|
|
try {
|
|
// Search array for the current view
|
|
$currentViewObj = $this->views[array_search($this->currentView, array_column($this->views, 'name'))] ?? null;
|
|
if (!$currentViewObj) {
|
|
throw new \Exception('View not found');
|
|
}
|
|
$view = ExtensionHelper::getView($this->service, $currentViewObj);
|
|
} catch (\Exception $e) {
|
|
if ($previousView !== $this->views[0]['name'] ?? null) {
|
|
$this->notify('Got an error while trying to load the view', 'error');
|
|
}
|
|
$this->currentView = $this->views[0]['name'] ?? null;
|
|
}
|
|
}
|
|
|
|
return view('services.show', ['extensionView' => $view])->layoutData([
|
|
'title' => 'Services',
|
|
'sidebar' => true,
|
|
]);
|
|
}
|
|
}
|