Files
Virtnet.bond/extensions/Servers/Proxmox/Livewire/Overview.php

538 lines
19 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 = '';
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)
{
$allowedRanges = ['hour', 'day', 'week', 'month', 'year'];
$allowedAggs = ['average', 'maximum'];
if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) {
return;
}
$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 (limit to 8KB to avoid bloating Proxmox config)
$notes = mb_substr($this->notes ?? '', 0, 8192);
$proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [
'description' => $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,
]);
}
}