Files
Virtnet.bond/extensions/Servers/Proxmox/Livewire/Overview.php
Joseph.Rawlings 0c64561b4f fix: add legacy showIconModal property to prevent stale session errors
Users with cached Livewire state from before rollback were getting errors
because their browser was trying to set showIconModal which no longer existed.
Adding an empty property prevents these errors until they refresh.
2025-12-06 07:56:04 -05:00

534 lines
18 KiB
PHP

<?php
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use Livewire\Attributes\On;
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
use Paymenter\Extensions\Servers\Proxmox\Models\ServerStatistics;
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
use Carbon\Carbon;
class Overview extends Component
{
public Server $server;
public $hostname = '';
public $isEditingHostname = false;
public $hostnameError = '';
public $password = '';
public $password_confirmation = '';
public $isEditingPassword = false;
public $passwordError = '';
// Legacy property - kept to prevent errors for users with stale Livewire state
public bool $showIconModal = false;
public string $cpuTimeRange = 'hour';
public string $cpuAggregation = 'average';
public string $memoryTimeRange = 'hour';
public string $memoryAggregation = 'average';
public string $networkTimeRange = 'hour';
public string $networkAggregation = 'average';
public string $diskTimeRange = 'hour';
public string $diskAggregation = 'average';
public $notes = '';
public $isEditingNotes = false;
public $notesError = '';
public function mount(Server $server): void
{
parent::mount($server);
if (is_null($this->status)) {
$this->loadVmStatus();
}
$this->hostname = $server->hostname ?? ($this->status->name ?? '');
// Load notes from Proxmox VM description
try {
$proxmox = new Proxmox;
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'];
$this->notes = $config['description'] ?? '';
} catch (\Exception $e) {
$this->notes = '';
}
}
public function checkStatus()
{
$currentStatus = $this->status->status;
$this->loadVmStatus();
if ($this->status->status != $currentStatus) {
$this->fastPolling = false;
}
}
#[On('proxmox:stats')]
public function stats()
{
$this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation);
$this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation);
$this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation);
$this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation);
}
protected function fetchGraphData(string $graphType, string $timeRange, string $aggregation)
{
$cacheKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation;
$data = Cache::remember($cacheKey, 60, function () use ($timeRange, $aggregation) {
$proxmox = new Proxmox;
$stats = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/rrddata', data: [
'timeframe' => $timeRange,
'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE',
]);
return $stats->json()['data'];
});
$this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $data);
}
public function updatedCpuTimeRange()
{
Cache::forget('proxmox-stats-' . $this->server->id . '-cpu-' . $this->cpuTimeRange . '-' . $this->cpuAggregation);
$this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation);
}
public function updatedCpuAggregation()
{
Cache::forget('proxmox-stats-' . $this->server->id . '-cpu-' . $this->cpuTimeRange . '-' . $this->cpuAggregation);
$this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation);
}
public function updatedMemoryTimeRange()
{
Cache::forget('proxmox-stats-' . $this->server->id . '-memory-' . $this->memoryTimeRange . '-' . $this->memoryAggregation);
$this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation);
}
public function updatedMemoryAggregation()
{
Cache::forget('proxmox-stats-' . $this->server->id . '-memory-' . $this->memoryTimeRange . '-' . $this->memoryAggregation);
$this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation);
}
public function updatedNetworkTimeRange()
{
Cache::forget('proxmox-stats-' . $this->server->id . '-network-' . $this->networkTimeRange . '-' . $this->networkAggregation);
$this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation);
}
public function updatedNetworkAggregation()
{
Cache::forget('proxmox-stats-' . $this->server->id . '-network-' . $this->networkTimeRange . '-' . $this->networkAggregation);
$this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation);
}
public function updatedDiskTimeRange()
{
Cache::forget('proxmox-stats-' . $this->server->id . '-disk-' . $this->diskTimeRange . '-' . $this->diskAggregation);
$this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation);
}
public function updatedDiskAggregation()
{
Cache::forget('proxmox-stats-' . $this->server->id . '-disk-' . $this->diskTimeRange . '-' . $this->diskAggregation);
$this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation);
}
protected function formatSeconds(?int $seconds): string
{
if (is_null($seconds) || $seconds < 0) {
return 'N/A';
}
if ($seconds < 60) {
return $seconds . 's';
}
if ($seconds < 3600) {
$minutes = floor($seconds / 60);
$remaining = $seconds % 60;
return sprintf('%dm %ds', $minutes, $remaining);
}
if ($seconds < 86400) {
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
return sprintf('%dh %dm', $hours, $minutes);
}
$days = floor($seconds / 86400);
$hours = floor(($seconds % 86400) / 3600);
return sprintf('%dd %dh', $days, $hours);
}
protected function getTaskEventCounts(): array
{
$counts = [
'starts' => 0,
'shutdowns' => 0,
'reboots' => 0,
];
try {
$proxmox = new Proxmox;
$response = $proxmox->request(
'/nodes/' . $this->server->node->name . '/tasks',
'get',
[
'vmid' => $this->server->vm_id,
'limit' => 200,
]
);
$tasks = $response->json()['data'] ?? [];
foreach ($tasks as $task) {
$type = strtolower($task['type'] ?? '');
if (in_array($type, ['qmstart', 'qmresume'], true)) {
$counts['starts']++;
} elseif (in_array($type, ['qmstop', 'qmshutdown'], true)) {
$counts['shutdowns']++;
} elseif (in_array($type, ['qmreboot', 'qmreset'], true)) {
$counts['reboots']++;
}
}
} catch (\Throwable $e) {
// If task fetch fails we simply return zeros
}
return $counts;
}
public function getPowerEventStatsProperty(): array
{
$stats = ServerStatistics::where('server_id', $this->server->id)
->orderBy('DateTime')
->limit(500)
->get(['vmstatus', 'uptime']);
$counts = [
'starts' => 0,
'shutdowns' => 0,
'reboots' => 0,
'samples' => $stats->count(),
'running_samples' => 0,
'uptime_percent' => 0,
];
$previousStatus = null;
$previousUptime = null;
foreach ($stats as $stat) {
$status = strtolower($stat->vmstatus ?? '');
if ($status === 'running') {
$counts['running_samples']++;
if ($previousStatus !== 'running') {
$counts['starts']++;
} elseif (!is_null($stat->uptime) && !is_null($previousUptime) && $stat->uptime < $previousUptime) {
$counts['reboots']++;
}
} elseif (in_array($status, ['stopped', 'shutdown', 'offline', 'stopped - shutdown', 'suspended'])) {
if ($previousStatus === 'running') {
$counts['shutdowns']++;
}
}
$previousStatus = $status;
$previousUptime = $stat->uptime;
}
if ($counts['samples'] > 0) {
$counts['uptime_percent'] = round(($counts['running_samples'] / $counts['samples']) * 100, 1);
} elseif ($this->status) {
$counts['uptime_percent'] = strtolower($this->status->status ?? '') === 'running' ? 100 : 0;
}
$taskCounts = $this->getTaskEventCounts();
$counts['starts'] = $taskCounts['starts'];
$counts['shutdowns'] = $taskCounts['shutdowns'];
$counts['reboots'] = $taskCounts['reboots'];
return $counts;
}
public function getNetworkRateStatsProperty(): array
{
$stats = ServerStatistics::where('server_id', $this->server->id)
->orderByDesc('DateTime')
->take(2)
->get(['DateTime', 'netin', 'netout']);
if ($stats->count() < 2) {
return ['in_rate' => 0, 'out_rate' => 0];
}
$current = $stats->first();
$previous = $stats->last();
$currentTime = $current->DateTime ? Carbon::parse($current->DateTime) : null;
$previousTime = $previous->DateTime ? Carbon::parse($previous->DateTime) : null;
if (!$currentTime || !$previousTime) {
return ['in_rate' => 0, 'out_rate' => 0];
}
$seconds = max($currentTime->diffInSeconds($previousTime), 1);
$inDelta = max(0, ($current->netin ?? 0) - ($previous->netin ?? 0));
$outDelta = max(0, ($current->netout ?? 0) - ($previous->netout ?? 0));
return [
'in_rate' => $inDelta / $seconds,
'out_rate' => $outDelta / $seconds,
];
}
public function startEditingHostname()
{
$this->isEditingHostname = true;
$this->hostname = $this->server->hostname ?? $this->status->name ?? '';
$this->hostnameError = '';
}
public function cancelEditingHostname()
{
$this->isEditingHostname = false;
$this->hostname = $this->server->hostname ?? $this->status->name ?? '';
$this->hostnameError = '';
}
public function updateHostname()
{
// Validate hostname format (RFC-1035 compliant)
$validator = Validator::make(
['hostname' => $this->hostname],
['hostname' => 'required|regex:/^(?!.*\.\.)[a-z0-9]([a-z0-9.-]{0,126}[a-z0-9])?$/']
);
if ($validator->fails()) {
$this->hostnameError = 'Hostname must be RFC-1035 compliant (lowercase letters, numbers, hyphens, and dots only).';
return false;
}
try {
// Update hostname in database
$this->server->hostname = $this->hostname;
$this->server->save();
// Refresh the server model to get latest data
$this->server->refresh();
// Update service property for the order
if ($this->server->service) {
$this->server->service->properties()->updateOrCreate(
['key' => 'hostname'],
[
'value' => $this->hostname,
'name' => 'Hostname',
]
);
}
$this->notify('Hostname saved to database successfully', 'success');
$this->isEditingHostname = false;
$this->hostnameError = '';
return true;
} catch (\Exception $e) {
$this->hostnameError = 'Failed to save hostname: ' . $e->getMessage();
$this->notify('Failed to save hostname', 'error');
return false;
}
}
public function applyHostname()
{
// First, save the hostname if it hasn't been saved
if ($this->server->hostname !== $this->hostname || !$this->server->hostname) {
if (!$this->updateHostname()) {
return; // Exit if save failed
}
}
try {
$proxmox = new Proxmox;
// Update VM name - Proxmox uses the VM name as the hostname for cloud-init
// Cloud-init will pick up the new VM name on the next boot
$response = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [
'name' => $this->hostname,
]);
// The request() method throws on error, so if we get here, it was successful
$this->notify('Hostname applied to Proxmox successfully. The VM name has been updated. The new hostname will be configured on the next boot via cloud-init.', 'success');
$this->isEditingHostname = false;
$this->hostnameError = '';
// Refresh status to get updated name
$this->status = (object) $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/status/current')->json()['data'];
} catch (\Exception $e) {
$this->hostnameError = 'Failed to apply hostname: ' . $e->getMessage();
$this->notify('Failed to apply hostname to Proxmox', 'error');
}
}
public function startEditingPassword()
{
$this->isEditingPassword = true;
$this->password = '';
$this->password_confirmation = '';
$this->passwordError = '';
}
public function cancelEditingPassword()
{
$this->isEditingPassword = false;
$this->password = '';
$this->password_confirmation = '';
$this->passwordError = '';
}
public function updatePassword()
{
// Validate password using same rules as registration
$validator = Validator::make(
[
'password' => $this->password,
'password_confirmation' => $this->password_confirmation,
],
[
'password' => 'required|string|min:8|confirmed',
]
);
if ($validator->fails()) {
$errors = $validator->errors()->all();
$this->passwordError = implode(' ', $errors);
return false;
}
try {
// Update password in database
$this->server->password = $this->password;
$this->server->save();
// Refresh the server model to get latest data
$this->server->refresh();
$this->notify('Password saved to database successfully', 'success');
$this->isEditingPassword = false;
$this->password = '';
$this->password_confirmation = '';
$this->passwordError = '';
return true;
} catch (\Exception $e) {
$this->passwordError = 'Failed to save password: ' . $e->getMessage();
$this->notify('Failed to save password', 'error');
return false;
}
}
public function applyPassword()
{
// Validate password first
$validator = Validator::make(
[
'password' => $this->password,
'password_confirmation' => $this->password_confirmation,
],
[
'password' => 'required|string|min:8|confirmed',
]
);
if ($validator->fails()) {
$errors = $validator->errors()->all();
$this->passwordError = implode(' ', $errors);
return;
}
try {
// Update password in database first
$this->server->password = $this->password;
$this->server->save();
// Refresh the server model to get latest data
$this->server->refresh();
// Apply password via Proxmox API using cloud-init
$proxmox = new Proxmox;
// The cipassword parameter sets the password that will be applied via cloud-init on next boot
$response = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [
'cipassword' => $this->password,
]);
// The request() method throws on error, so if we get here, it was successful
$this->notify('Password applied to Proxmox successfully. The new password will be configured on the next boot via cloud-init. Please reboot your server for the changes to take effect.', 'success');
$this->isEditingPassword = false;
$this->password = '';
$this->password_confirmation = '';
$this->passwordError = '';
} catch (\Exception $e) {
$this->passwordError = 'Failed to apply password: ' . $e->getMessage();
$this->notify('Failed to apply password to Proxmox', 'error');
}
}
public function startEditingNotes()
{
$this->isEditingNotes = true;
$this->notesError = '';
}
public function cancelEditingNotes()
{
$this->isEditingNotes = false;
$this->notesError = '';
// Reload notes from Proxmox
try {
$proxmox = new Proxmox;
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'];
$this->notes = $config['description'] ?? '';
} catch (\Exception $e) {
$this->notes = '';
}
}
public function saveNotes()
{
try {
$proxmox = new Proxmox;
// Save notes to Proxmox VM description
$proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [
'description' => $this->notes ?? '',
]);
$this->notify('Notes saved successfully', 'success');
$this->isEditingNotes = false;
$this->notesError = '';
} catch (\Exception $e) {
$this->notesError = 'Failed to save notes: ' . $e->getMessage();
$this->notify('Failed to save notes', 'error');
}
}
public function render()
{
return view('proxmox::overview')->layoutData([
'sidebar' => true,
]);
}
}