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, ]); } }