Reduce security and reliability risk across the Proxmox extension by removing password exposure from Livewire state, hardening console auth/cookie handling, and improving sanitized error paths. Improve firewall and router performance by adding safer lazy loading, bounded pagination, cross-server compatibility guards, and lower-overhead API workflows. Made-with: Cursor
436 lines
15 KiB
PHP
436 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
|
|
use Illuminate\Support\Facades\Validator;
|
|
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 $isEditingPassword = false;
|
|
public $passwordError = '';
|
|
|
|
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 ?? '');
|
|
|
|
$notice = request()->query('notice');
|
|
if ($notice === 'display_unavailable') {
|
|
$this->notify('Display settings are only available for virtual machines.', 'info');
|
|
}
|
|
|
|
// Load notes from Proxmox VM description
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
$vmType = $this->server->node->vmType();
|
|
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $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;
|
|
}
|
|
}
|
|
|
|
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', 'pctstart', 'pctresume'], true)) {
|
|
$counts['starts']++;
|
|
} elseif (in_array($type, ['qmstop', 'qmshutdown', 'pctstop', 'pctshutdown'], true)) {
|
|
$counts['shutdowns']++;
|
|
} elseif (in_array($type, ['qmreboot', 'qmreset', 'pctreboot', 'pctreset'], true)) {
|
|
$counts['reboots']++;
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// If task fetch fails we simply return zeros
|
|
}
|
|
|
|
return $counts;
|
|
}
|
|
|
|
public function getPowerEventStatsProperty(): array
|
|
{
|
|
// Compute uptime percentage from DB statistics
|
|
$stats = ServerStatistics::where('server_id', $this->server->id)
|
|
->orderBy('DateTime')
|
|
->limit(500)
|
|
->get(['vmstatus']);
|
|
|
|
$samples = $stats->count();
|
|
$runningSamples = 0;
|
|
|
|
foreach ($stats as $stat) {
|
|
if (strtolower($stat->vmstatus ?? '') === 'running') {
|
|
$runningSamples++;
|
|
}
|
|
}
|
|
|
|
$uptimePercent = 0;
|
|
if ($samples > 0) {
|
|
$uptimePercent = round(($runningSamples / $samples) * 100, 1);
|
|
} elseif ($this->status) {
|
|
$uptimePercent = strtolower($this->status->status ?? '') === 'running' ? 100 : 0;
|
|
}
|
|
|
|
// Get start/shutdown/reboot counts from Proxmox API (authoritative source)
|
|
$taskCounts = $this->getTaskEventCounts();
|
|
|
|
return [
|
|
'starts' => $taskCounts['starts'],
|
|
'shutdowns' => $taskCounts['shutdowns'],
|
|
'reboots' => $taskCounts['reboots'],
|
|
'samples' => $samples,
|
|
'running_samples' => $runningSamples,
|
|
'uptime_percent' => $uptimePercent,
|
|
];
|
|
}
|
|
|
|
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
|
|
$vmType = $this->server->node->vmType();
|
|
$response = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config', 'put', [
|
|
$this->server->node->is_lxc ? 'hostname' : '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 . '/' . $vmType . '/' . $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->passwordError = '';
|
|
}
|
|
|
|
public function cancelEditingPassword()
|
|
{
|
|
$this->isEditingPassword = false;
|
|
$this->passwordError = '';
|
|
}
|
|
|
|
protected function validatePasswordInput(string $password, string $passwordConfirmation): bool
|
|
{
|
|
$validator = Validator::make(
|
|
[
|
|
'password' => $password,
|
|
'password_confirmation' => $passwordConfirmation,
|
|
],
|
|
[
|
|
'password' => 'required|string|min:8|confirmed',
|
|
]
|
|
);
|
|
|
|
if ($validator->fails()) {
|
|
$errors = $validator->errors()->all();
|
|
$this->passwordError = implode(' ', $errors);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function closePasswordEditor(): void
|
|
{
|
|
$this->isEditingPassword = false;
|
|
$this->passwordError = '';
|
|
}
|
|
|
|
public function updatePassword(string $password = '', string $passwordConfirmation = '')
|
|
{
|
|
if (!$this->validatePasswordInput($password, $passwordConfirmation)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Update password in database
|
|
$this->server->password = $password;
|
|
$this->server->save();
|
|
|
|
// Refresh the server model to get latest data
|
|
$this->server->refresh();
|
|
|
|
$this->notify('Password saved to database successfully', 'success');
|
|
$this->closePasswordEditor();
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
report($e);
|
|
$this->passwordError = 'Failed to save password. Please try again.';
|
|
$this->notify('Failed to save password', 'error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function applyPassword(string $password = '', string $passwordConfirmation = '')
|
|
{
|
|
if (!$this->validatePasswordInput($password, $passwordConfirmation)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Update password in database first
|
|
$this->server->password = $password;
|
|
$this->server->save();
|
|
|
|
// Refresh the server model to get latest data
|
|
$this->server->refresh();
|
|
|
|
// LXC containers don't support password change via config API
|
|
if ($this->server->node->is_lxc) {
|
|
$this->notify('Password saved to database. Note: LXC container passwords cannot be changed via API - use console access to change root password if needed.', 'warning');
|
|
$this->closePasswordEditor();
|
|
return;
|
|
}
|
|
|
|
// Apply password via Proxmox API using cloud-init (KVM only)
|
|
$proxmox = new Proxmox;
|
|
$vmType = $this->server->node->vmType();
|
|
|
|
// For KVM: cipassword sets the password via cloud-init on next boot
|
|
$response = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config', 'put', [
|
|
'cipassword' => $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->closePasswordEditor();
|
|
} catch (\Exception $e) {
|
|
report($e);
|
|
$this->passwordError = 'Failed to apply password. Please try again.';
|
|
$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;
|
|
$vmType = $this->server->node->vmType();
|
|
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
|
|
$this->notes = $config['description'] ?? '';
|
|
} catch (\Exception $e) {
|
|
$this->notes = '';
|
|
}
|
|
}
|
|
|
|
public function saveNotes()
|
|
{
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
$vmType = $this->server->node->vmType();
|
|
|
|
// 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 . '/' . $vmType . '/' . $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,
|
|
]);
|
|
}
|
|
}
|